mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-03-12 05:25:24 +00:00
Compare commits
91 Commits
v2.4.2
...
upgrade/sv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d128700c1 | ||
|
|
6743750d8f | ||
|
|
640e957065 | ||
|
|
d4f11c0412 | ||
|
|
01acc6d6e8 | ||
|
|
e89bb53941 | ||
|
|
009c4686ee | ||
|
|
33dc5fc177 | ||
|
|
faf047e25f | ||
|
|
b4322c5f9c | ||
|
|
0b4884652f | ||
|
|
82aeae8699 | ||
|
|
a7333653b0 | ||
|
|
24e63d5e32 | ||
|
|
eb7c266378 | ||
|
|
cf8c48ab99 | ||
|
|
78a1cd78ea | ||
|
|
fdfa6c1b52 | ||
|
|
4f1790a464 | ||
|
|
ca4cef5233 | ||
|
|
a0d7fd0d95 | ||
|
|
489dbf9254 | ||
|
|
a81aa11558 | ||
|
|
2e4a0fc0a3 | ||
|
|
fc6e9e8085 | ||
|
|
ad73821f1c | ||
|
|
98bf5b958f | ||
|
|
f323c5a4f6 | ||
|
|
ea91248e67 | ||
|
|
271ce2081f | ||
|
|
8438020dbd | ||
|
|
9252a810df | ||
|
|
c0c696a55c | ||
|
|
ef70d39f70 | ||
|
|
1cf890dc26 | ||
|
|
9d307e3192 | ||
|
|
61f0356a10 | ||
|
|
c648546b61 | ||
|
|
ffd8d46f2e | ||
|
|
eefbc08222 | ||
|
|
1a5b9542f4 | ||
|
|
19ff70782f | ||
|
|
0dfd2d9022 | ||
|
|
7a4ab77a83 | ||
|
|
46cd8c6e68 | ||
|
|
c307103605 | ||
|
|
d9ec111458 | ||
|
|
e3a1aa78a9 | ||
|
|
b06aa5f71b | ||
|
|
6a75b0fe71 | ||
|
|
e11fac3d11 | ||
|
|
031422f783 | ||
|
|
2ff8fddc14 | ||
|
|
e5d260ae3e | ||
|
|
633671851e | ||
|
|
69da9f9d48 | ||
|
|
19f78126d6 | ||
|
|
d3cc92949a | ||
|
|
f1089faab5 | ||
|
|
4a3ad6ab9b | ||
|
|
86449949c5 | ||
|
|
9bd9826835 | ||
|
|
94ff9d6da7 | ||
|
|
9518d1811b | ||
|
|
983426b36c | ||
|
|
bcb3c3b2ec | ||
|
|
9fd383c1ef | ||
|
|
a931f4cef8 | ||
|
|
fddecaab39 | ||
|
|
e5eab82d68 | ||
|
|
8ae1d46b2a | ||
|
|
99ccb03b45 | ||
|
|
b8f2d501a5 | ||
|
|
e042806619 | ||
|
|
16f9ed7ecb | ||
|
|
3eaefefd26 | ||
|
|
03c8c3ddbc | ||
|
|
08371b9078 | ||
|
|
a6ae25f3e7 | ||
|
|
8051db6a9b | ||
|
|
d0bfdae5d8 | ||
|
|
4d681ed2e2 | ||
|
|
8ee3f53714 | ||
|
|
e8cf8a98ed | ||
|
|
9153a98694 | ||
|
|
37aa6ae816 | ||
|
|
8a35346fcf | ||
|
|
792ae4c935 | ||
|
|
a7324d828a | ||
|
|
fe739c51d3 | ||
|
|
795d2d0375 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -27,6 +27,7 @@ jobs:
|
||||
run: |
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
|
||||
- name: Cache Python dependencies
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19.x'
|
||||
node-version: '22.x'
|
||||
- name: Build with node
|
||||
run: |
|
||||
cd lib/SvelteUi/app
|
||||
|
||||
41
.github/workflows/localazy.yml
vendored
Normal file
41
.github/workflows/localazy.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Deploy language files from localazy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- 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: Generate localazy-keys.json
|
||||
run: |
|
||||
echo '{"writeKey": "", "readKey": "${{secrets.LOCALAZY_READ_KEY}}"}' > localazy/localazy-keys.json
|
||||
|
||||
- name: Create localazy language folder
|
||||
run: mkdir -p localazy/language
|
||||
|
||||
- name: Install Localazy CLI
|
||||
run: npm install -g @localazy/cli
|
||||
|
||||
- name: Download translations
|
||||
working-directory: localazy
|
||||
run: localazy download -k localazy-keys.json
|
||||
|
||||
- name: Upload translations to S3
|
||||
run: aws s3 sync ./localazy/language/ s3://${{ secrets.AWS_S3_BUCKET }}/language/
|
||||
85
.github/workflows/prerelease.yml
vendored
Normal file
85
.github/workflows/prerelease.yml
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
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
|
||||
136
.github/workflows/release-deploy-env.yml
vendored
Normal file
136
.github/workflows/release-deploy-env.yml
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
274
.github/workflows/release.yml
vendored
274
.github/workflows/release.yml
vendored
@@ -1,217 +1,79 @@
|
||||
name: Release
|
||||
name: Release build and upload
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
- name: Get release version for code
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
|
||||
- name: Inject secrets into ini file
|
||||
run: |
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
|
||||
- name: Create release with release notes
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release v${{ steps.release_tag.outputs.tag }}
|
||||
generateReleaseNotes: true
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
|
||||
- name: Cache PlatformIO dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pio/libdeps
|
||||
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio css_html_js_minify
|
||||
outputs:
|
||||
version: ${{ steps.release_tag.outputs.tag }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19.x'
|
||||
- name: Build with node
|
||||
run: |
|
||||
cd lib/SvelteUi/app
|
||||
npm ci
|
||||
npm run build
|
||||
cd -
|
||||
env:
|
||||
CI: false
|
||||
|
||||
- name: PlatformIO lib install
|
||||
run: pio lib install
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Build esp8266 firmware
|
||||
run: pio run -e esp8266
|
||||
- name: Create esp8266 zip file
|
||||
run: /bin/sh scripts/esp8266/mkzip.sh
|
||||
- name: Upload esp8266 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp8266/firmware.bin
|
||||
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp8266 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp8266.zip
|
||||
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32 firmware
|
||||
run: pio run -e esp32
|
||||
- name: Create esp32 zip file
|
||||
run: /bin/sh scripts/esp32/mkzip.sh
|
||||
- name: Upload esp32 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32/firmware.bin
|
||||
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32.zip
|
||||
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32s2 firmware
|
||||
run: pio run -e esp32s2
|
||||
- name: Create esp32s2 zip file
|
||||
run: /bin/sh scripts/esp32s2/mkzip.sh
|
||||
- name: Upload esp32s2 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32s2/firmware.bin
|
||||
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32s2 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32s2.zip
|
||||
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32s3 firmware
|
||||
run: pio run -e esp32s3
|
||||
- name: Create esp32s3 zip file
|
||||
run: /bin/sh scripts/esp32s3/mkzip.sh
|
||||
- name: Upload esp32s3 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32s3/firmware.bin
|
||||
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32s3 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32s3.zip
|
||||
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32solo firmware
|
||||
run: pio run -e esp32solo
|
||||
- name: Create esp32solo zip file
|
||||
run: /bin/sh scripts/esp32solo/mkzip.sh
|
||||
- name: Upload esp32solo binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32solo/firmware.bin
|
||||
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32solo zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32solo.zip
|
||||
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32c3 firmware
|
||||
run: pio run -e esp32c3
|
||||
- name: Create esp32c3 zip file
|
||||
run: /bin/sh scripts/esp32c3/mkzip.sh
|
||||
- name: Upload esp32c3 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32c3/firmware.bin
|
||||
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32c3 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32c3.zip
|
||||
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
esp32s2:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s2
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32s3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32c3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32c3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32solo:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32solo
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp8266:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp8266
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
is_esp32: false
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,3 +19,5 @@ platformio-user.ini
|
||||
node_modules
|
||||
/gui/dist
|
||||
/scripts/*dev
|
||||
localazy-keys.json
|
||||
localazy/language
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# AMS Reader
|
||||
This code is designed to decode data from electric smart meters installed in many countries in Europe these days. The data is presented in a graphical web interface and can also send the data to a MQTT broker which makes it suitable for home automation project. Originally it was only designed to work with Norwegian meters, but has since been adapter to read any IEC-62056-7-5 or IEC-62056-21 compliant meters.
|
||||
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://amsleser.no/).
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://www.amsleser.no/).
|
||||
|
||||
|
||||
<img src="images/dashboard.png">
|
||||
|
||||
Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases).
|
||||
## Installing pre-built firmware
|
||||
If you have a device already running this firmware and you for some reason need to upgrade via USB port, you can use a [this web-based tool](https://www.amsleser.cloud/flasher)
|
||||
|
||||
If you are using a development board and want to flash a pre-built firmware manually, get the necessary files from the [release](https://github.com/UtilitechAS/amsreader-firmware/releases) section and visit the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) and have a look at the [Flashing](https://github.com/UtilitechAS/amsreader-firmware/wiki/flashinghttps://github.com/UtilitechAS/amsreader-firmware/wiki/flashing) section
|
||||
|
||||
## Building this project with PlatformIO
|
||||
To build this project, you need [PlatformIO](https://platformio.org/) installed.
|
||||
|
||||
BIN
doc/Norway/Norwegian_payload_breakdown.docx
Normal file
BIN
doc/Norway/Norwegian_payload_breakdown.docx
Normal file
Binary file not shown.
BIN
doc/Poland/Stoen/HAN_Interface_Stoen_Operator_EN.pdf
Normal file
BIN
doc/Poland/Stoen/HAN_Interface_Stoen_Operator_EN.pdf
Normal file
Binary file not shown.
BIN
doc/Poland/Stoen/interfejs-sieci-han-w-stoen-operator-v1-0-1.pdf
Normal file
BIN
doc/Poland/Stoen/interfejs-sieci-han-w-stoen-operator-v1-0-1.pdf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
doc/Poland/Stoen/opis-profili-rejestrowane-kody-obis-v1-0-2.pdf
Normal file
BIN
doc/Poland/Stoen/opis-profili-rejestrowane-kody-obis-v1-0-2.pdf
Normal file
Binary file not shown.
1
doc/Poland/Stoen/placeholder.txt
Normal file
1
doc/Poland/Stoen/placeholder.txt
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1375
frames/KamstrupOmnipower_Bornholm_DK_5digitVoltage_9sep25.log
Normal file
1375
frames/KamstrupOmnipower_Bornholm_DK_5digitVoltage_9sep25.log
Normal file
File diff suppressed because it is too large
Load Diff
682
frames/L&G-E350_Norway.raw
Normal file
682
frames/L&G-E350_Norway.raw
Normal file
@@ -0,0 +1,682 @@
|
||||
*** Remote debug - over telnet - for ESP32 - version 3.0.5
|
||||
* Host name: ams-e603 IP:10.10.10.62 Mac address:D8:3B:DA:C4:03:E6
|
||||
* Free Heap RAM: 87952
|
||||
* ESP SDK version: 4.4.5.230722
|
||||
******************************************************
|
||||
* Commands:
|
||||
? or help -> display these help of commands
|
||||
q -> quit (close this connection)
|
||||
m -> display memory available
|
||||
v -> set debug level to verbose
|
||||
d -> set debug level to debug
|
||||
i -> set debug level to info
|
||||
w -> set debug level to warning
|
||||
e -> set debug level to errors
|
||||
s -> set debug silence on/off
|
||||
l -> show debug level
|
||||
t -> show time (millis)
|
||||
profiler:
|
||||
p -> show time between actual and last message (in millis)
|
||||
p min -> show only if time is this minimal
|
||||
P time -> set debug level to profiler
|
||||
c -> show colors
|
||||
filter:
|
||||
filter <string> -> show only debugs with this
|
||||
nofilter -> disable the filter
|
||||
* Please type the command and press enter to execute.(? or h for this help)
|
||||
***
|
||||
(V) HDLC frame:
|
||||
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
|
||||
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
|
||||
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
|
||||
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
|
||||
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
|
||||
(V) FF 06 00 00 24 F4 02 02 0F 00 16 1B 02 03 09 06
|
||||
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
|
||||
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 31
|
||||
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
|
||||
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
|
||||
(V) 00 1F 07 00 FF 10 0C 6C 02 02 0F FE 16 21 02 03
|
||||
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
|
||||
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 68 02 02
|
||||
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
|
||||
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
|
||||
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
|
||||
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 2F AF 7E
|
||||
(V)
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
|
||||
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
|
||||
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
|
||||
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
|
||||
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
|
||||
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 F4 02 02 0F
|
||||
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
|
||||
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
|
||||
(V) 00 FF 06 00 00 00 31 02 02 0F 00 16 1D 02 03 09
|
||||
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
|
||||
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 6C 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
|
||||
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
|
||||
(V) 00 FF 10 0D 68 02 02 0F FE 16 21 02 03 09 06 01
|
||||
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
|
||||
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
|
||||
(V) 0F FF 16 23
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
|
||||
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
|
||||
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
|
||||
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
|
||||
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
|
||||
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
|
||||
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
|
||||
(V) 00 00 00 31 02 02 0F 00 16 1D 02 03 09 06 01 00
|
||||
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
|
||||
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 6C 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
|
||||
(V) 0D 68 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
|
||||
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
|
||||
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
|
||||
(V) 23
|
||||
(D) Received valid DLMS at 18 +267
|
||||
(V) Using application data:
|
||||
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
|
||||
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
|
||||
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
|
||||
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
|
||||
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
|
||||
(V) 00 00 24 F4 02 02 0F 00 16 1B 02 03 09 06 01 00
|
||||
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
|
||||
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 31 02 02
|
||||
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
|
||||
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
|
||||
(V) 07 00 FF 10 0C 6C 02 02 0F FE 16 21 02 03 09 06
|
||||
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
|
||||
(V) 03 09 06 01 00 47 07 00 FF 10 0D 68 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
|
||||
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
|
||||
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
|
||||
(V) 00 FF 12 09 10 02 02 0F FF 16 23
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:06 UTC, meter clock: 00:00:00, list type 2, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 F4 02 02 0F 00 16 1B A7 7F 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) F4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:07 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 F4 02 02 0F 00 16 1B A7 7F 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 F4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) F4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:10 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 E0 02 02 0F 00 16 1B 18 A5 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) E0 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:12 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
|
||||
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
|
||||
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
|
||||
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
|
||||
(V) FF 0A 04 00 00 00 00 02 03 09 06 01 00 01 07 00
|
||||
(V) FF 06 00 00 24 E0 02 02 0F 00 16 1B 02 03 09 06
|
||||
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
|
||||
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 28
|
||||
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
|
||||
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
|
||||
(V) 00 1F 07 00 FF 10 0C 68 02 02 0F FE 16 21 02 03
|
||||
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
|
||||
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02
|
||||
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
|
||||
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
|
||||
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
|
||||
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 AE 9D 7E
|
||||
(V)
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
|
||||
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
|
||||
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
|
||||
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
|
||||
(V) 09 06 00 00 60 01 07 FF 0A 04 00 00 00 00 02 03
|
||||
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 E0 02 02 0F
|
||||
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
|
||||
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
|
||||
(V) 00 FF 06 00 00 00 28 02 02 0F 00 16 1D 02 03 09
|
||||
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
|
||||
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 68 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
|
||||
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
|
||||
(V) 00 FF 10 0D 5D 02 02 0F FE 16 21 02 03 09 06 01
|
||||
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
|
||||
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
|
||||
(V) 0F FF 16 23
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
|
||||
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
|
||||
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
|
||||
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
|
||||
(V) 00 60 01 07 FF 0A 04 00 00 00 00 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
|
||||
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
|
||||
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
|
||||
(V) 00 00 00 28 02 02 0F 00 16 1D 02 03 09 06 01 00
|
||||
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
|
||||
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 68 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
|
||||
(V) 0D 5D 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
|
||||
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
|
||||
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
|
||||
(V) 23
|
||||
(D) Received valid DLMS at 18 +267
|
||||
(V) Using application data:
|
||||
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
|
||||
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
|
||||
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
|
||||
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
|
||||
(V) 04 00 00 00 00 02 03 09 06 01 00 01 07 00 FF 06
|
||||
(V) 00 00 24 E0 02 02 0F 00 16 1B 02 03 09 06 01 00
|
||||
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
|
||||
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 28 02 02
|
||||
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
|
||||
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
|
||||
(V) 07 00 FF 10 0C 68 02 02 0F FE 16 21 02 03 09 06
|
||||
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
|
||||
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
|
||||
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
|
||||
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
|
||||
(V) 00 FF 12 09 10 02 02 0F FF 16 23
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:16 UTC, meter clock: 00:00:00, list type 2, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 E0 02 02 0F 00 16 1B 18 A5 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 E0 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) E0 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:17 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) A4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:20 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) A4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:22 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
|
||||
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
|
||||
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
|
||||
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
|
||||
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
|
||||
(V) FF 06 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06
|
||||
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
|
||||
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 31
|
||||
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
|
||||
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
|
||||
(V) 00 1F 07 00 FF 10 0C 6E 02 02 0F FE 16 21 02 03
|
||||
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
|
||||
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5C 02 02
|
||||
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
|
||||
(V) 38 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
|
||||
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
|
||||
(V) 48 07 00 FF 12 09 10 02 02 0F FF 16 23 4A DF 7E
|
||||
(V)
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
|
||||
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
|
||||
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
|
||||
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
|
||||
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
|
||||
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 A4 02 02 0F
|
||||
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
|
||||
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
|
||||
(V) 00 FF 06 00 00 00 31 02 02 0F 00 16 1D 02 03 09
|
||||
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
|
||||
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 6E 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
|
||||
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
|
||||
(V) 00 FF 10 0D 5C 02 02 0F FE 16 21 02 03 09 06 01
|
||||
(V) 00 20 07 00 FF 12 09 38 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
|
||||
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 10 02 02
|
||||
(V) 0F FF 16 23
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
|
||||
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
|
||||
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
|
||||
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
|
||||
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
|
||||
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
|
||||
(V) 00 00 00 31 02 02 0F 00 16 1D 02 03 09 06 01 00
|
||||
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
|
||||
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 6E 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
|
||||
(V) 0D 5C 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
|
||||
(V) 00 FF 12 09 38 02 02 0F FF 16 23 02 03 09 06 01
|
||||
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 48 07 00 FF 12 09 10 02 02 0F FF 16
|
||||
(V) 23
|
||||
(D) Received valid DLMS at 18 +267
|
||||
(V) Using application data:
|
||||
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
|
||||
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
|
||||
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
|
||||
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
|
||||
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
|
||||
(V) 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06 01 00
|
||||
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
|
||||
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 31 02 02
|
||||
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
|
||||
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
|
||||
(V) 07 00 FF 10 0C 6E 02 02 0F FE 16 21 02 03 09 06
|
||||
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
|
||||
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5C 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 38 02
|
||||
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
|
||||
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
|
||||
(V) 00 FF 12 09 10 02 02 0F FF 16 23
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:26 UTC, meter clock: 00:00:00, list type 2, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) B8 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:27 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) B8 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:29 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) B8 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:32 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
q(V) HDLC frame:
|
||||
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
|
||||
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
|
||||
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
|
||||
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
|
||||
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
|
||||
(V) FF 06 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06
|
||||
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
|
||||
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 46
|
||||
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
|
||||
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
|
||||
(V) 00 1F 07 00 FF 10 0C 67 02 02 0F FE 16 21 02 03
|
||||
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
|
||||
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02
|
||||
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
|
||||
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
|
||||
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
|
||||
(V) 48 07 00 FF 12 09 06 02 02 0F FF 16 23 3A 49 7E
|
||||
(V)
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
|
||||
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
|
||||
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
|
||||
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
|
||||
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
|
||||
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 A4 02 02 0F
|
||||
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
|
||||
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
|
||||
(V) 00 FF 06 00 00 00 46 02 02 0F 00 16 1D 02 03 09
|
||||
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
|
||||
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 67 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
|
||||
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
|
||||
(V) 00 FF 10 0D 5D 02 02 0F FE 16 21 02 03 09 06 01
|
||||
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
|
||||
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 06 02 02
|
||||
(V) 0F FF 16 23
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
|
||||
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
|
||||
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
|
||||
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
|
||||
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
|
||||
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
|
||||
(V) 00 00 00 46 02 02 0F 00 16 1D 02 03 09 06 01 00
|
||||
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
|
||||
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 67 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
|
||||
(V) 0D 5D 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
|
||||
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
|
||||
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 48 07 00 FF 12 09 06 02 02 0F FF 16
|
||||
(V) 23
|
||||
(D) Received valid DLMS at 18 +267
|
||||
(V) Using application data:
|
||||
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
|
||||
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
|
||||
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
|
||||
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
|
||||
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
|
||||
(V) 00 00 24 A4 02 02 0F 00 16 1B 02 03 09 06 01 00
|
||||
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
|
||||
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 46 02 02
|
||||
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
|
||||
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
|
||||
(V) 07 00 FF 10 0C 67 02 02 0F FE 16 21 02 03 09 06
|
||||
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
|
||||
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5D 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
|
||||
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
|
||||
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
|
||||
(V) 00 FF 12 09 06 02 02 0F FF 16 23
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:36 UTC, meter clock: 00:00:00, list type 2, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) A4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:37 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 A4 02 02 0F 00 16 1B 68 0D 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 A4 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) A4 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:39 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 D5 02 02 0F 00 16 1B F1 83 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) D5 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:42 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B
|
||||
(V) 45 4D 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00
|
||||
(V) 00 60 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00
|
||||
(V) 00 00 00 00 00 00 00 02 02 09 06 00 00 60 01 07
|
||||
(V) FF 0A 04 5A 46 46 31 02 03 09 06 01 00 01 07 00
|
||||
(V) FF 06 00 00 24 D5 02 02 0F 00 16 1B 02 03 09 06
|
||||
(V) 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16
|
||||
(V) 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 3C
|
||||
(V) 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF
|
||||
(V) 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01
|
||||
(V) 00 1F 07 00 FF 10 0C 77 02 02 0F FE 16 21 02 03
|
||||
(V) 09 06 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16
|
||||
(V) 21 02 03 09 06 01 00 47 07 00 FF 10 0D 5F 02 02
|
||||
(V) 0F FE 16 21 02 03 09 06 01 00 20 07 00 FF 12 09
|
||||
(V) 2E 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00
|
||||
(V) FF 12 00 00 02 02 0F FF 16 23 02 03 09 06 01 00
|
||||
(V) 48 07 00 FF 12 09 06 02 02 0F FF 16 23 4A 9D 7E
|
||||
(V)
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01
|
||||
(V) 01 00 02 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31
|
||||
(V) 5F 31 02 02 09 06 00 00 60 01 00 FF 0A 10 31 33
|
||||
(V) 39 34 33 35 39 37 00 00 00 00 00 00 00 00 02 02
|
||||
(V) 09 06 00 00 60 01 07 FF 0A 04 5A 46 46 31 02 03
|
||||
(V) 09 06 01 00 01 07 00 FF 06 00 00 24 D5 02 02 0F
|
||||
(V) 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00
|
||||
(V) 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07
|
||||
(V) 00 FF 06 00 00 00 3C 02 02 0F 00 16 1D 02 03 09
|
||||
(V) 06 01 00 04 07 00 FF 06 00 00 00 00 02 02 0F 00
|
||||
(V) 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 0C 77 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 33 07 00 FF 10
|
||||
(V) 00 00 02 02 0F FE 16 21 02 03 09 06 01 00 47 07
|
||||
(V) 00 FF 10 0D 5F 02 02 0F FE 16 21 02 03 09 06 01
|
||||
(V) 00 20 07 00 FF 12 09 2E 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 34 07 00 FF 12 00 00 02 02 0F FF 16
|
||||
(V) 23 02 03 09 06 01 00 48 07 00 FF 12 09 06 02 02
|
||||
(V) 0F FF 16 23
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02
|
||||
(V) 81 FF 0A 0B 45 4D 42 52 49 51 5F 56 31 5F 31 02
|
||||
(V) 02 09 06 00 00 60 01 00 FF 0A 10 31 33 39 34 33
|
||||
(V) 35 39 37 00 00 00 00 00 00 00 00 02 02 09 06 00
|
||||
(V) 00 60 01 07 FF 0A 04 5A 46 46 31 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
|
||||
(V) 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02
|
||||
(V) 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06
|
||||
(V) 00 00 00 3C 02 02 0F 00 16 1D 02 03 09 06 01 00
|
||||
(V) 04 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02
|
||||
(V) 03 09 06 01 00 1F 07 00 FF 10 0C 77 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 00 02
|
||||
(V) 02 0F FE 16 21 02 03 09 06 01 00 47 07 00 FF 10
|
||||
(V) 0D 5F 02 02 0F FE 16 21 02 03 09 06 01 00 20 07
|
||||
(V) 00 FF 12 09 2E 02 02 0F FF 16 23 02 03 09 06 01
|
||||
(V) 00 34 07 00 FF 12 00 00 02 02 0F FF 16 23 02 03
|
||||
(V) 09 06 01 00 48 07 00 FF 12 09 06 02 02 0F FF 16
|
||||
(V) 23
|
||||
(D) Received valid DLMS at 18 +267
|
||||
(V) Using application data:
|
||||
(V) 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 45 4D
|
||||
(V) 42 52 49 51 5F 56 31 5F 31 02 02 09 06 00 00 60
|
||||
(V) 01 00 FF 0A 10 31 33 39 34 33 35 39 37 00 00 00
|
||||
(V) 00 00 00 00 00 02 02 09 06 00 00 60 01 07 FF 0A
|
||||
(V) 04 5A 46 46 31 02 03 09 06 01 00 01 07 00 FF 06
|
||||
(V) 00 00 24 D5 02 02 0F 00 16 1B 02 03 09 06 01 00
|
||||
(V) 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02
|
||||
(V) 03 09 06 01 00 03 07 00 FF 06 00 00 00 3C 02 02
|
||||
(V) 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00
|
||||
(V) 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 1F
|
||||
(V) 07 00 FF 10 0C 77 02 02 0F FE 16 21 02 03 09 06
|
||||
(V) 01 00 33 07 00 FF 10 00 00 02 02 0F FE 16 21 02
|
||||
(V) 03 09 06 01 00 47 07 00 FF 10 0D 5F 02 02 0F FE
|
||||
(V) 16 21 02 03 09 06 01 00 20 07 00 FF 12 09 2E 02
|
||||
(V) 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12
|
||||
(V) 00 00 02 02 0F FF 16 23 02 03 09 06 01 00 48 07
|
||||
(V) 00 FF 12 09 06 02 02 0F FF 16 23
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:46 UTC, meter clock: 00:00:00, list type 2, est: 1)
|
||||
qq(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 D5 02 02 0F 00 16 1B F1 83 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 D5 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) D5 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:47 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
q
|
||||
* Debug: Command received: qqqqq
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) B8 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:49 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
(V) HDLC frame:
|
||||
(V) 7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00
|
||||
(V) 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00
|
||||
(V) 00 24 B8 02 02 0F 00 16 1B 3B 09 7E
|
||||
(V) LLC frame:
|
||||
(V) E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01
|
||||
(V) 00 01 07 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(V)
|
||||
(V) DLMS frame:
|
||||
(V) 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07
|
||||
(V) 00 FF 06 00 00 24 B8 02 02 0F 00 16 1B
|
||||
(D) Received valid DLMS at 18 +23
|
||||
(V) Using application data:
|
||||
(V) 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 24
|
||||
(V) B8 02 02 0F 00 16 1B
|
||||
(V) DLMS
|
||||
(D) NOT Ready to update (internal clock 12:32:51 UTC, meter clock: 00:00:00, list type 1, est: 1)
|
||||
q
|
||||
* Debug: Command received: q
|
||||
* Closing client connection ...
|
||||
48
frames/iskra_croatia.txt
Normal file
48
frames/iskra_croatia.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
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: 173 KiB After Width: | Height: | Size: 199 KiB |
@@ -13,7 +13,6 @@
|
||||
#define EEPROM_CHECK_SUM 104 // Used to check if config is stored. Change if structure changes
|
||||
#define EEPROM_CLEARED_INDICATOR 0xFC
|
||||
#define EEPROM_CONFIG_ADDRESS 0
|
||||
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
|
||||
|
||||
#define CONFIG_SYSTEM_START 8
|
||||
#define CONFIG_NETWORK_START 40
|
||||
@@ -30,6 +29,7 @@
|
||||
#define CONFIG_UI_START 1720
|
||||
#define CONFIG_CLOUD_START 1742
|
||||
#define CONFIG_UPGRADE_INFO_START 1934
|
||||
#define CONFIG_ZC_START 2000
|
||||
|
||||
#define CONFIG_METER_START_103 32
|
||||
#define CONFIG_UPGRADE_INFO_START_103 216
|
||||
@@ -50,6 +50,22 @@
|
||||
#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;
|
||||
@@ -63,7 +79,8 @@ struct SystemConfig {
|
||||
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
|
||||
char country[3];
|
||||
uint8_t energyspeedometer;
|
||||
}; // 8
|
||||
uint8_t firmwareChannel;
|
||||
}; // 9
|
||||
|
||||
struct NetworkConfig {
|
||||
char ssid[32];
|
||||
@@ -97,7 +114,8 @@ struct MqttConfig {
|
||||
uint16_t stateUpdateInterval;
|
||||
uint16_t timeout;
|
||||
uint8_t keepalive;
|
||||
}; // 685
|
||||
uint8_t rebootMinutes;
|
||||
}; // 684
|
||||
|
||||
struct WebConfig {
|
||||
uint8_t security;
|
||||
@@ -157,7 +175,8 @@ struct GpioConfig {
|
||||
uint16_t vccResistorVcc;
|
||||
uint8_t ledDisablePin;
|
||||
uint8_t ledBehaviour;
|
||||
}; // 21
|
||||
uint8_t powersaving;
|
||||
}; // 22
|
||||
|
||||
struct GpioConfig103 {
|
||||
uint8_t hanPin;
|
||||
@@ -206,10 +225,12 @@ struct PriceServiceConfig {
|
||||
char entsoeToken[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint32_t unused1;
|
||||
bool enabled;
|
||||
uint8_t resolutionInMinutes;
|
||||
uint16_t unused2;
|
||||
}; // 64
|
||||
uint16_t unused3;
|
||||
bool enabled;
|
||||
uint16_t unused6;
|
||||
};
|
||||
|
||||
struct EnergyAccountingConfig {
|
||||
uint16_t thresholds[10];
|
||||
@@ -236,14 +257,14 @@ struct UiConfig {
|
||||
}; // 15
|
||||
|
||||
struct UpgradeInformation {
|
||||
char fromVersion[8];
|
||||
char toVersion[8];
|
||||
char fromVersion[16];
|
||||
char toVersion[16];
|
||||
uint32_t size;
|
||||
uint16_t block_position;
|
||||
uint8_t retry_count;
|
||||
uint8_t reboot_count;
|
||||
int8_t errorCode;
|
||||
}; // 25
|
||||
}; // 41+3
|
||||
|
||||
struct CloudConfig {
|
||||
bool enabled;
|
||||
@@ -254,6 +275,12 @@ struct CloudConfig {
|
||||
uint8_t proto;
|
||||
}; // 88
|
||||
|
||||
struct ZmartChargeConfig {
|
||||
bool enabled;
|
||||
char token[21];
|
||||
char baseUrl[64];
|
||||
}; // 86
|
||||
|
||||
class AmsConfiguration {
|
||||
public:
|
||||
bool hasConfig();
|
||||
@@ -283,6 +310,8 @@ public:
|
||||
bool getWebConfig(WebConfig&);
|
||||
bool setWebConfig(WebConfig&);
|
||||
void clearWebConfig(WebConfig&);
|
||||
bool isWebChanged();
|
||||
void ackWebChange();
|
||||
|
||||
bool getMeterConfig(MeterConfig&);
|
||||
bool setMeterConfig(MeterConfig&);
|
||||
@@ -345,6 +374,13 @@ public:
|
||||
void clearCloudConfig(CloudConfig&);
|
||||
bool isCloudChanged();
|
||||
void ackCloudConfig();
|
||||
|
||||
bool getZmartChargeConfig(ZmartChargeConfig&);
|
||||
bool setZmartChargeConfig(ZmartChargeConfig&);
|
||||
void clearZmartChargeConfig(ZmartChargeConfig&);
|
||||
bool isZmartChargeConfigChanged();
|
||||
void ackZmartChargeConfig();
|
||||
|
||||
|
||||
void clear();
|
||||
|
||||
@@ -353,7 +389,7 @@ protected:
|
||||
private:
|
||||
uint8_t configVersion = 0;
|
||||
|
||||
bool sysChanged = false, networkChanged, mqttChanged, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false;
|
||||
bool sysChanged = false, networkChanged = false, mqttChanged = false, webChanged = false, meterChanged = true, ntpChanged = true, priceChanged = false, energyAccountingChanged = true, cloudChanged = true, uiLanguageChanged = false, zcChanged = true;
|
||||
|
||||
bool relocateConfig103(); // 2.2.12, until, but not including 2.3
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
String toHex(uint8_t* in);
|
||||
String toHex(uint8_t* in, uint16_t size);
|
||||
void fromHex(uint8_t *out, String in, uint16_t size);
|
||||
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended = false);
|
||||
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended = false, bool trim = true);
|
||||
void debugPrint(uint8_t *buffer, uint16_t start, uint16_t length, Print* debugger);
|
||||
|
||||
#endif
|
||||
@@ -13,15 +13,26 @@
|
||||
bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
|
||||
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
|
||||
EEPROM.get(CONFIG_SYSTEM_START, config);
|
||||
EEPROM.end();
|
||||
EEPROM.get(CONFIG_SYSTEM_START, config);
|
||||
EEPROM.end();
|
||||
|
||||
if(config.firmwareChannel > 3) {
|
||||
config.firmwareChannel = 0;
|
||||
}
|
||||
|
||||
if(configVersion == EEPROM_CHECK_SUM) {
|
||||
return true;
|
||||
} else {
|
||||
config.boardType = 0xFF;
|
||||
config.vendorConfigured = false;
|
||||
if(configVersion == EEPROM_CLEARED_INDICATOR && config.boardType > 0 && config.boardType < 250) {
|
||||
config.vendorConfigured = true;
|
||||
} else {
|
||||
config.vendorConfigured = false;
|
||||
config.boardType = 0xFF;
|
||||
clear();
|
||||
}
|
||||
config.userConfigured = false;
|
||||
config.dataCollectionConsent = 0;
|
||||
config.firmwareChannel = 0;
|
||||
config.energyspeedometer = 0;
|
||||
memset(config.country, 0, 3);
|
||||
return false;
|
||||
@@ -37,6 +48,9 @@ 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);
|
||||
@@ -91,7 +105,7 @@ bool AmsConfiguration::setNetworkConfig(NetworkConfig& config) {
|
||||
}
|
||||
|
||||
stripNonAscii((uint8_t*) config.ssid, 32, true);
|
||||
stripNonAscii((uint8_t*) config.psk, 64, true);
|
||||
stripNonAscii((uint8_t*) config.psk, 64, true, false);
|
||||
stripNonAscii((uint8_t*) config.ip, 16);
|
||||
stripNonAscii((uint8_t*) config.gateway, 16);
|
||||
stripNonAscii((uint8_t*) config.subnet, 16);
|
||||
@@ -147,14 +161,17 @@ bool AmsConfiguration::getMqttConfig(MqttConfig& config) {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_MQTT_START, config);
|
||||
EEPROM.end();
|
||||
if(config.magic != 0x9C) {
|
||||
if(config.magic != 0x7B) {
|
||||
config.stateUpdate = false;
|
||||
config.stateUpdateInterval = 10;
|
||||
if(config.magic != 0xA5) { // New magic for 2.4.11
|
||||
if(config.magic != 0x9C) {
|
||||
if(config.magic != 0x7B) {
|
||||
config.stateUpdate = false;
|
||||
config.stateUpdateInterval = 10;
|
||||
}
|
||||
config.timeout = 1000;
|
||||
config.keepalive = 60;
|
||||
}
|
||||
config.timeout = 1000;
|
||||
config.keepalive = 60;
|
||||
config.magic = 0x9C;
|
||||
config.rebootMinutes = config.ssl ? 5 : 0;
|
||||
config.magic = 0xA5;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
@@ -177,6 +194,9 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
|
||||
mqttChanged |= config.ssl != existing.ssl;
|
||||
mqttChanged |= config.stateUpdate != existing.stateUpdate;
|
||||
mqttChanged |= config.stateUpdateInterval != existing.stateUpdateInterval;
|
||||
mqttChanged |= config.timeout != existing.timeout;
|
||||
mqttChanged |= config.keepalive != existing.keepalive;
|
||||
mqttChanged |= config.rebootMinutes != existing.rebootMinutes;
|
||||
} else {
|
||||
mqttChanged = true;
|
||||
}
|
||||
@@ -186,11 +206,12 @@ bool AmsConfiguration::setMqttConfig(MqttConfig& config) {
|
||||
stripNonAscii((uint8_t*) config.publishTopic, 64);
|
||||
stripNonAscii((uint8_t*) config.subscribeTopic, 64);
|
||||
stripNonAscii((uint8_t*) config.username, 128, true);
|
||||
stripNonAscii((uint8_t*) config.password, 256, true);
|
||||
stripNonAscii((uint8_t*) config.password, 256, true, false);
|
||||
if(config.timeout < 500) config.timeout = 1000;
|
||||
if(config.timeout > 10000) config.timeout = 1000;
|
||||
if(config.keepalive < 5) config.keepalive = 60;
|
||||
if(config.keepalive > 240) config.keepalive = 60;
|
||||
if(config.rebootMinutes > 240) config.rebootMinutes = 0;
|
||||
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.put(CONFIG_MQTT_START, config);
|
||||
@@ -215,6 +236,7 @@ void AmsConfiguration::clearMqtt(MqttConfig& config) {
|
||||
config.stateUpdateInterval = 10;
|
||||
config.timeout = 1000;
|
||||
config.keepalive = 60;
|
||||
config.rebootMinutes = 0;
|
||||
}
|
||||
|
||||
void AmsConfiguration::setMqttChanged() {
|
||||
@@ -242,9 +264,17 @@ bool AmsConfiguration::getWebConfig(WebConfig& config) {
|
||||
}
|
||||
|
||||
bool AmsConfiguration::setWebConfig(WebConfig& config) {
|
||||
WebConfig existing;
|
||||
if(getWebConfig(existing)) {
|
||||
webChanged |= strcmp(config.username, existing.username) != 0;
|
||||
webChanged |= strcmp(config.password, existing.password) != 0;
|
||||
webChanged |= strcmp(config.context, existing.context) != 0;
|
||||
} else {
|
||||
webChanged = true;
|
||||
}
|
||||
|
||||
stripNonAscii((uint8_t*) config.username, 37);
|
||||
stripNonAscii((uint8_t*) config.password, 37);
|
||||
stripNonAscii((uint8_t*) config.password, 37, false, false);
|
||||
stripNonAscii((uint8_t*) config.context, 37);
|
||||
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
@@ -261,9 +291,18 @@ void AmsConfiguration::clearWebConfig(WebConfig& config) {
|
||||
memset(config.context, 0, 37);
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isWebChanged() {
|
||||
return webChanged;
|
||||
}
|
||||
|
||||
void AmsConfiguration::ackWebChange() {
|
||||
webChanged = false;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::getMeterConfig(MeterConfig& config) {
|
||||
if(hasConfig()) {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
|
||||
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
|
||||
EEPROM.get(CONFIG_METER_START, config);
|
||||
EEPROM.end();
|
||||
if(config.bufferSize < 1 || config.bufferSize > 64) {
|
||||
@@ -478,6 +517,7 @@ 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);
|
||||
@@ -559,6 +599,7 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
|
||||
config.vccResistorGnd = 0;
|
||||
config.vccResistorVcc = 0;
|
||||
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
|
||||
config.powersaving = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +666,9 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
|
||||
clearPriceServiceConfig(config);
|
||||
return false;
|
||||
}
|
||||
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
|
||||
config.resolutionInMinutes = 60;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
clearPriceServiceConfig(config);
|
||||
@@ -639,6 +683,7 @@ bool AmsConfiguration::setPriceServiceConfig(PriceServiceConfig& config) {
|
||||
priceChanged |= strcmp(config.area, existing.area) != 0;
|
||||
priceChanged |= strcmp(config.currency, existing.currency) != 0;
|
||||
priceChanged |= config.enabled != existing.enabled;
|
||||
priceChanged |= config.resolutionInMinutes != existing.resolutionInMinutes;
|
||||
} else {
|
||||
priceChanged = true;
|
||||
}
|
||||
@@ -658,9 +703,8 @@ void AmsConfiguration::clearPriceServiceConfig(PriceServiceConfig& config) {
|
||||
memset(config.entsoeToken, 0, 37);
|
||||
memset(config.area, 0, 17);
|
||||
memset(config.currency, 0, 4);
|
||||
config.unused1 = 1000;
|
||||
config.enabled = false;
|
||||
config.unused2 = 0;
|
||||
config.resolutionInMinutes = 60;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isPriceServiceChanged() {
|
||||
@@ -788,8 +832,8 @@ void AmsConfiguration::ackUiLanguageChange() {
|
||||
}
|
||||
|
||||
bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
stripNonAscii((uint8_t*) upinfo.fromVersion, 8);
|
||||
stripNonAscii((uint8_t*) upinfo.toVersion, 8);
|
||||
stripNonAscii((uint8_t*) upinfo.fromVersion, 16);
|
||||
stripNonAscii((uint8_t*) upinfo.toVersion, 16);
|
||||
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
|
||||
@@ -803,7 +847,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, 8) || stripNonAscii((uint8_t*) upinfo.toVersion, 8)) {
|
||||
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 16) || stripNonAscii((uint8_t*) upinfo.toVersion, 16)) {
|
||||
clearUpgradeInformation(upinfo);
|
||||
return false;
|
||||
}
|
||||
@@ -815,8 +859,8 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
}
|
||||
|
||||
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
memset(upinfo.fromVersion, 0, 8);
|
||||
memset(upinfo.toVersion, 0, 8);
|
||||
memset(upinfo.fromVersion, 0, 16);
|
||||
memset(upinfo.toVersion, 0, 16);
|
||||
upinfo.errorCode = 0;
|
||||
upinfo.size = 0;
|
||||
upinfo.block_position = 0;
|
||||
@@ -876,6 +920,65 @@ void AmsConfiguration::ackCloudConfig() {
|
||||
cloudChanged = false;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::getZmartChargeConfig(ZmartChargeConfig& config) {
|
||||
if(hasConfig()) {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_ZC_START, config);
|
||||
EEPROM.end();
|
||||
stripNonAscii((uint8_t*) config.token, 21);
|
||||
stripNonAscii((uint8_t*) config.baseUrl, 64);
|
||||
if(strlen(config.token) != 20 || !config.enabled) {
|
||||
config.enabled = false;
|
||||
memset(config.token, 0, 64);
|
||||
memset(config.baseUrl, 0, 64);
|
||||
}
|
||||
if(strlen(config.baseUrl) == 0 || strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
|
||||
memset(config.baseUrl, 0, 64);
|
||||
snprintf_P(config.baseUrl, 64, PSTR("https://main.zmartcharge.com/api"));
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
clearZmartChargeConfig(config);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AmsConfiguration::setZmartChargeConfig(ZmartChargeConfig& config) {
|
||||
ZmartChargeConfig existing;
|
||||
if(getZmartChargeConfig(existing)) {
|
||||
zcChanged |= config.enabled != existing.enabled;
|
||||
zcChanged |= strcmp(config.token, existing.token) != 0;
|
||||
zcChanged |= strcmp(config.baseUrl, existing.baseUrl) != 0;
|
||||
} else {
|
||||
zcChanged = true;
|
||||
}
|
||||
|
||||
stripNonAscii((uint8_t*) config.token, 21);
|
||||
stripNonAscii((uint8_t*) config.baseUrl, 64);
|
||||
if(strncmp_P(config.baseUrl, PSTR("https"), 5) != 0) {
|
||||
memset(config.baseUrl, 0, 64);
|
||||
}
|
||||
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.put(CONFIG_ZC_START, config);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AmsConfiguration::clearZmartChargeConfig(ZmartChargeConfig& config) {
|
||||
config.enabled = false;
|
||||
memset(config.token, 0, 21);
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isZmartChargeConfigChanged() {
|
||||
return zcChanged;
|
||||
}
|
||||
|
||||
void AmsConfiguration::ackZmartChargeConfig() {
|
||||
zcChanged = false;
|
||||
}
|
||||
|
||||
void AmsConfiguration::setUiLanguageChanged() {
|
||||
uiLanguageChanged = true;
|
||||
}
|
||||
@@ -887,6 +990,7 @@ 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);
|
||||
@@ -943,6 +1047,10 @@ void AmsConfiguration::clear() {
|
||||
clearCloudConfig(cloud);
|
||||
EEPROM.put(CONFIG_CLOUD_START, cloud);
|
||||
|
||||
ZmartChargeConfig zc;
|
||||
clearZmartChargeConfig(zc);
|
||||
EEPROM.put(CONFIG_ZC_START, zc);
|
||||
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CLEARED_INDICATOR);
|
||||
EEPROM.commit();
|
||||
EEPROM.end();
|
||||
@@ -1045,7 +1153,8 @@ bool AmsConfiguration::relocateConfig103() {
|
||||
gpio103.vccResistorGnd,
|
||||
gpio103.vccResistorVcc,
|
||||
gpio103.ledDisablePin,
|
||||
gpio103.ledBehaviour
|
||||
gpio103.ledBehaviour,
|
||||
0
|
||||
};
|
||||
|
||||
WebConfig web = {web103.security};
|
||||
@@ -1077,6 +1186,10 @@ bool AmsConfiguration::relocateConfig103() {
|
||||
clearCloudConfig(cloud);
|
||||
EEPROM.put(CONFIG_CLOUD_START, cloud);
|
||||
|
||||
ZmartChargeConfig zcc;
|
||||
clearZmartChargeConfig(zcc);
|
||||
EEPROM.put(CONFIG_ZC_START, zcc);
|
||||
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 104);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
@@ -1085,6 +1198,7 @@ bool AmsConfiguration::relocateConfig103() {
|
||||
|
||||
bool AmsConfiguration::save() {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
|
||||
bool success = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
@@ -1156,6 +1270,9 @@ void AmsConfiguration::print(Print* debugger)
|
||||
}
|
||||
debugger->printf_P(PSTR("Payload format: %i\r\n"), mqtt.payloadFormat);
|
||||
debugger->printf_P(PSTR("SSL: %s\r\n"), mqtt.ssl ? "Yes" : "No");
|
||||
debugger->printf_P(PSTR("Timeout: %i\r\n"), mqtt.timeout);
|
||||
debugger->printf_P(PSTR("Keep-alive: %i\r\n"), mqtt.keepalive);
|
||||
debugger->printf_P(PSTR("Auto reboot minutes: %i\r\n"), mqtt.rebootMinutes);
|
||||
} else {
|
||||
debugger->printf_P(PSTR("Enabled: No\r\n"));
|
||||
}
|
||||
@@ -1209,6 +1326,7 @@ 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);
|
||||
}
|
||||
@@ -1264,10 +1382,10 @@ void AmsConfiguration::print(Print* debugger)
|
||||
debugger->printf_P(PSTR("Area: %s\r\n"), price.area);
|
||||
debugger->printf_P(PSTR("Currency: %s\r\n"), price.currency);
|
||||
debugger->printf_P(PSTR("ENTSO-E Token: %s\r\n"), price.entsoeToken);
|
||||
debugger->println(F(""));
|
||||
delay(10);
|
||||
debugger->flush();
|
||||
}
|
||||
debugger->println(F(""));
|
||||
delay(10);
|
||||
debugger->flush();
|
||||
}
|
||||
|
||||
UiConfig ui;
|
||||
@@ -1285,9 +1403,29 @@ void AmsConfiguration::print(Print* debugger)
|
||||
String uuid = ESPRandom::uuidToString(cc.clientId);;
|
||||
debugger->println(F("--Cloud configuration--"));
|
||||
debugger->printf_P(PSTR("Enabled: %s\r\n"), cc.enabled ? "Yes" : "No");
|
||||
debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname);
|
||||
debugger->printf_P(PSTR("Client ID: %s\r\n"), uuid.c_str());
|
||||
debugger->printf_P(PSTR("Interval: %d\r\n"), cc.interval);
|
||||
if(cc.enabled) {
|
||||
debugger->printf_P(PSTR("Hostname: %s\r\n"), cc.hostname);
|
||||
debugger->printf_P(PSTR("Client ID: %s\r\n"), uuid.c_str());
|
||||
debugger->printf_P(PSTR("Interval: %d\r\n"), cc.interval);
|
||||
}
|
||||
debugger->println(F(""));
|
||||
delay(10);
|
||||
debugger->flush();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ZMART_CHARGE)
|
||||
ZmartChargeConfig zc;
|
||||
if(getZmartChargeConfig(zc)) {
|
||||
debugger->println(F("--ZmartCharge configuration--"));
|
||||
debugger->printf_P(PSTR("Enabled: %s\r\n"), zc.enabled ? "Yes" : "No");
|
||||
if(zc.enabled) {
|
||||
debugger->printf_P(PSTR("Base URL: '%s'\r\n"), zc.baseUrl);
|
||||
debugger->printf_P(PSTR("Token: '%s'\r\n"), zc.token);
|
||||
}
|
||||
debugger->println(F(""));
|
||||
delay(10);
|
||||
debugger->flush();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ void fromHex(uint8_t *out, String in, uint16_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
|
||||
bool stripNonAscii(uint8_t* in, uint16_t size, bool extended, bool trim) {
|
||||
bool ret = false;
|
||||
for(uint16_t i = 0; i < size; i++) {
|
||||
if(in[i] == 0) { // Clear the rest with null-terminator
|
||||
@@ -43,6 +43,22 @@ bool stripNonAscii(uint8_t* in, uint16_t size, bool extended) {
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
if(trim) {
|
||||
// Strip leading spaces
|
||||
while(in[0] == ' ') {
|
||||
for(uint16_t i = 0; i < size; i++) {
|
||||
in[i] = in[i+1];
|
||||
}
|
||||
}
|
||||
// Strip trailing spaces
|
||||
for(int i = size-1; i > 0; i--) {
|
||||
if(in[i] == ' ' || in[i] == 0) {
|
||||
memset(in+i, 0, 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
memset(in+size-1, 0, 1); // Make sure the last character is null-terminator
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#define DATA_PARSE_OK 0
|
||||
#define DATA_PARSE_FAIL -1
|
||||
#define DATA_PARSE_INCOMPLETE -2
|
||||
#define DATA_PARSE_BOUNDRY_FLAG_MISSING -3
|
||||
#define DATA_PARSE_BOUNDARY_FLAG_MISSING -3
|
||||
#define DATA_PARSE_HEADER_CHECKSUM_ERROR -4
|
||||
#define DATA_PARSE_FOOTER_CHECKSUM_ERROR -5
|
||||
#define DATA_PARSE_INTERMEDIATE_SEGMENT -6
|
||||
|
||||
@@ -19,8 +19,6 @@ time_t decodeCosemDateTime(CosemDateTime timestamp) {
|
||||
tm.Minute = timestamp.minute;
|
||||
tm.Second = timestamp.second;
|
||||
|
||||
//Serial.printf("\nY: %d, M: %d, D: %d, h: %d, m: %d, s: %d, deviation: 0x%2X, status: 0x%1X\n", tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, timestamp.deviation, timestamp.status);
|
||||
|
||||
time_t time = makeTime(tm);
|
||||
int16_t deviation = ntohs(timestamp.deviation);
|
||||
if(deviation >= -720 && deviation <= 720) {
|
||||
|
||||
@@ -17,7 +17,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
|
||||
uint8_t lastByte = 0x00;
|
||||
for(uint16_t pos = 0; pos < ctx.length; pos++) {
|
||||
uint8_t b = *(buf+pos);
|
||||
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
if(pos == 0 && b != '/') return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
if(pos > 0 && b == '!') crcPos = pos+1;
|
||||
if(crcPos > 0 && b == 0x0A && lastByte == 0x0D) {
|
||||
reachedEnd = true;
|
||||
@@ -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 != crc_calc) {
|
||||
if(crc > 0 && crc != crc_calc) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ int8_t GBTParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
GBTHeader* h = (GBTHeader*) (d);
|
||||
uint16_t sequence = ntohs(h->sequence);
|
||||
|
||||
if(h->flag != GBT_TAG) return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
if(h->flag != GBT_TAG) return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
|
||||
if(sequence == 1) {
|
||||
if(buf == NULL) buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?
|
||||
|
||||
@@ -23,7 +23,7 @@ int8_t GCMParser::parse(uint8_t *d, DataParserContext &ctx, bool hastag) {
|
||||
uint32_t headersize = 0;
|
||||
uint8_t* ptr = (uint8_t*) d;
|
||||
if(hastag) {
|
||||
if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
if(*ptr != GCM_TAG) return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
ptr++;
|
||||
headersize++;
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
|
||||
// First and last byte should be HDLC_FLAG
|
||||
if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG)
|
||||
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
|
||||
// Verify FCS
|
||||
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
|
||||
if(f->fcs > 0 && 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(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
|
||||
if(t3->hcs > 0 && ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
|
||||
return DATA_PARSE_HEADER_CHECKSUM_ERROR;
|
||||
ptr += 3;
|
||||
|
||||
@@ -69,7 +69,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
|
||||
if(buf == NULL) return DATA_PARSE_FAIL;
|
||||
|
||||
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
|
||||
if((*ptr) == DATA_TAG_LLC) {
|
||||
ptr += 3; // Skip LLC
|
||||
ctx.length -= 3;
|
||||
}
|
||||
|
||||
memcpy(buf + pos, ptr, ctx.length);
|
||||
pos += ctx.length;
|
||||
|
||||
lastSequenceNumber++;
|
||||
@@ -78,7 +83,12 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
lastSequenceNumber = 0;
|
||||
if(buf == NULL) return DATA_PARSE_FAIL;
|
||||
|
||||
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
|
||||
if((*ptr) == DATA_TAG_LLC) {
|
||||
ptr += 3; // Skip LLC
|
||||
ctx.length -= 3;
|
||||
}
|
||||
|
||||
memcpy(buf + pos, ptr, ctx.length);
|
||||
pos += ctx.length;
|
||||
|
||||
memcpy((uint8_t *) d, buf, pos);
|
||||
|
||||
@@ -19,7 +19,7 @@ int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
|
||||
MbusHeader* mh = (MbusHeader*) d;
|
||||
if(mh->flag1 != MBUS_START || mh->flag2 != MBUS_START)
|
||||
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
|
||||
// First two bytes is 1-byte length value repeated. Only used for last segment
|
||||
if(mh->len1 != mh->len2)
|
||||
@@ -40,7 +40,7 @@ int8_t MBUSParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
|
||||
MbusFooter* mf = (MbusFooter*) (d + len + headersize);
|
||||
if(mf->flag != MBUS_END)
|
||||
return DATA_PARSE_BOUNDRY_FLAG_MISSING;
|
||||
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
if(checksum(d + headersize, len) != mf->fcs)
|
||||
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#define AMS_PARTITION_MIN_SPIFFS_SIZE 0x20000
|
||||
#elif defined(ESP8266)
|
||||
#include <ESP8266HTTPClient.h>
|
||||
|
||||
#define AMS_FLASH_SKETCH_SIZE 0xFEFF0
|
||||
#define AMS_FLASH_OTA_START AMS_FLASH_OTA_SIZE
|
||||
#endif
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -36,6 +39,8 @@
|
||||
#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:
|
||||
@@ -57,6 +62,13 @@ 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);
|
||||
@@ -92,10 +104,11 @@ private:
|
||||
String md5;
|
||||
|
||||
uint32_t lastVersionCheck = 0;
|
||||
uint8_t firmwareVariant;
|
||||
uint8_t firmwareChannel;
|
||||
bool autoUpgrade;
|
||||
char nextVersion[10];
|
||||
char nextVersion[17];
|
||||
|
||||
void getChannelName(char * buffer);
|
||||
|
||||
bool fetchNextVersion();
|
||||
bool fetchVersionDetails();
|
||||
|
||||
@@ -22,7 +22,7 @@ this->debugger = debugger;
|
||||
this->hw = hw;
|
||||
this->meterState = meterState;
|
||||
memset(nextVersion, 0, sizeof(nextVersion));
|
||||
firmwareVariant = 0;
|
||||
firmwareChannel = 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++ < 8) {
|
||||
if(updateStatus.reboot_count++ < UPDATE_MAX_REBOOT_RETRY) {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_OK;
|
||||
} else {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT;
|
||||
@@ -97,6 +97,11 @@ float AmsFirmwareUpdater::getProgress() {
|
||||
}
|
||||
|
||||
void AmsFirmwareUpdater::loop() {
|
||||
if(millis() < 30000) {
|
||||
// Wait 30 seconds before starting upgrade. This allows the device to deal with other tasks first
|
||||
// It will also allow BUS powered devices to reach a stable voltage so that hw->isVoltageOptimal will behave properly
|
||||
return;
|
||||
}
|
||||
if(strlen(updateStatus.toVersion) > 0 && updateStatus.errorCode == AMS_UPDATE_ERR_OK) {
|
||||
if(!hw->isVoltageOptimal(0.1)) {
|
||||
writeUpdateStatus();
|
||||
@@ -124,7 +129,7 @@ void AmsFirmwareUpdater::loop() {
|
||||
HTTPClient http;
|
||||
start = millis();
|
||||
if(!fetchFirmwareChunk(http)) {
|
||||
if(updateStatus.retry_count++ == 3) {
|
||||
if(updateStatus.retry_count++ > UPDATE_MAX_BLOCK_RETRY) {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_FETCH;
|
||||
updateStatusChanged = true;
|
||||
}
|
||||
@@ -203,15 +208,33 @@ 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 firmwareVariant[10] = "stable";
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, firmwareVariant);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, channel);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -225,6 +248,16 @@ bool AmsFirmwareUpdater::fetchNextVersion() {
|
||||
http.setUserAgent("AMS-Firmware-Updater");
|
||||
http.addHeader(F("Cache-Control"), "no-cache");
|
||||
http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString);
|
||||
http.addHeader(F("x-AMS-STA-MAC"), WiFi.macAddress());
|
||||
http.addHeader(F("x-AMS-AP-MAC"), WiFi.softAPmacAddress());
|
||||
http.addHeader(F("x-AMS-chip-size"), String(ESP.getFlashChipSize()));
|
||||
http.addHeader(F("x-AMS-board-type"), String(hw->getBoardType(), 10));
|
||||
if(meterState->getMeterType() != AmsTypeAutodetect) {
|
||||
http.addHeader(F("x-AMS-meter-mfg"), String(meterState->getMeterType(), 10));
|
||||
}
|
||||
if(!meterState->getMeterModel().isEmpty()) {
|
||||
http.addHeader(F("x-AMS-meter-model"), meterState->getMeterModel());
|
||||
}
|
||||
int status = http.GET();
|
||||
if(status == 204) {
|
||||
String nextVersion = http.header("x-version");
|
||||
@@ -248,10 +281,11 @@ bool AmsFirmwareUpdater::fetchVersionDetails() {
|
||||
const char * headerkeys[] = { "x-size" };
|
||||
http.collectHeaders(headerkeys, 1);
|
||||
|
||||
char firmwareVariant[10] = "stable";
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, firmwareVariant, updateStatus.toVersion);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, channel, updateStatus.toVersion);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -304,10 +338,11 @@ bool AmsFirmwareUpdater::fetchFirmwareChunk(HTTPClient& http) {
|
||||
char range[24];
|
||||
snprintf_P(range, 24, PSTR("bytes=%lu-%lu"), start, end);
|
||||
|
||||
char firmwareVariant[10] = "stable";
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, firmwareVariant, updateStatus.toVersion);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, channel, updateStatus.toVersion);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -1128,7 +1163,7 @@ bool AmsFirmwareUpdater::moveLittleFsFromApp1ToNew() {
|
||||
}
|
||||
#elif defined(ESP8266)
|
||||
uintptr_t AmsFirmwareUpdater::getFirmwareUpdateStart() {
|
||||
return FS_start - 0x40200000;
|
||||
return (AMS_FLASH_SKETCH_SIZE + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
|
||||
}
|
||||
|
||||
bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
|
||||
@@ -1137,6 +1172,14 @@ bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Checking if we can upgrade\n"));
|
||||
|
||||
if(FS_PHYS_ADDR < (getFirmwareUpdateStart() + AMS_FLASH_SKETCH_SIZE)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("No room for OTA update\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!ESP.checkFlashConfig(false)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
@@ -1145,24 +1188,12 @@ bool AmsFirmwareUpdater::isFlashReadyForNextUpdateVersion(uint32_t size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//size of current sketch rounded to a sector
|
||||
size_t currentSketchSize = (ESP.getSketchSize() + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
|
||||
|
||||
//size of the update rounded to a sector
|
||||
size_t roundedSize = (size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
|
||||
|
||||
//address of the end of the space available for sketch and update
|
||||
uintptr_t updateEndAddress = FS_start - 0x40200000;
|
||||
|
||||
uintptr_t updateStartAddress = (updateEndAddress > roundedSize) ? (updateEndAddress - roundedSize) : 0;
|
||||
|
||||
//make sure that the size of both sketches is less than the total space (updateEndAddress)
|
||||
if(updateStartAddress < currentSketchSize) {
|
||||
if(size > AMS_FLASH_SKETCH_SIZE) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("New firmware does not fit flash\n"));
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
@@ -1180,14 +1211,28 @@ bool AmsFirmwareUpdater::writeBufferToFlash() {
|
||||
uint32_t offset = updateStatus.block_position * UPDATE_BUF_SIZE;
|
||||
uintptr_t currentAddress = getFirmwareUpdateStart() + offset;
|
||||
uint32_t sector = currentAddress/FLASH_SECTOR_SIZE;
|
||||
if(!ESP.flashEraseSector(sector)) {
|
||||
|
||||
if (currentAddress % FLASH_SECTOR_SIZE == 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("flashEraseSector(%lu) failed\n"), sector);
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_ERASE;
|
||||
return false;
|
||||
debugger->printf_P(PSTR("flashEraseSector(%lu)\n"), sector);
|
||||
yield();
|
||||
if(!ESP.flashEraseSector(sector)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("flashEraseSector(%lu) failed\n"), sector);
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_ERASE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("flashWrite(%lu)\n"), sector);
|
||||
yield();
|
||||
if(!ESP.flashWrite(currentAddress, buf, UPDATE_BUF_SIZE)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
|
||||
9
lib/AmsJsonGenerator/include/AmsJsonGenerator.h
Normal file
9
lib/AmsJsonGenerator/include/AmsJsonGenerator.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "AmsDataStorage.h"
|
||||
|
||||
class AmsJsonGenerator {
|
||||
public:
|
||||
static void generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
|
||||
static void generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
|
||||
};
|
||||
17
lib/AmsJsonGenerator/src/AmsJsonGenerator.cpp
Normal file
17
lib/AmsJsonGenerator/src/AmsJsonGenerator.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "AmsJsonGenerator.h"
|
||||
|
||||
void AmsJsonGenerator::generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
|
||||
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 0; i < 24; i++) {
|
||||
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
|
||||
}
|
||||
|
||||
void AmsJsonGenerator::generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
|
||||
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 1; i < 32; i++) {
|
||||
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
|
||||
}
|
||||
@@ -24,31 +24,31 @@ class AmsMqttHandler {
|
||||
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) {
|
||||
#endif
|
||||
this->mqttConfig = mqttConfig;
|
||||
this->mqttConfigChanged = true;
|
||||
this->debugger = debugger;
|
||||
this->json = buf;
|
||||
this->updater = updater;
|
||||
mqtt.dropOverflow(true);
|
||||
|
||||
pubTopic = String(mqttConfig.publishTopic);
|
||||
subTopic = String(mqttConfig.subscribeTopic);
|
||||
if(subTopic.isEmpty()) subTopic = pubTopic+"/command";
|
||||
};
|
||||
#else
|
||||
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) {
|
||||
this->mqttConfig = mqttConfig;
|
||||
this->mqttConfigChanged = true;
|
||||
this->debugger = debugger;
|
||||
this->json = buf;
|
||||
mqtt.dropOverflow(true);
|
||||
};
|
||||
#endif
|
||||
|
||||
void setCaVerification(bool);
|
||||
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,11 +57,16 @@ 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(String data) { return false; };
|
||||
virtual bool publishRaw(uint8_t* raw, size_t length) { 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;
|
||||
@@ -81,11 +86,17 @@ protected:
|
||||
bool caVerification = true;
|
||||
WiFiClient *mqttClient = NULL;
|
||||
WiFiClientSecure *mqttSecureClient = NULL;
|
||||
boolean _connected = false;
|
||||
char* json;
|
||||
uint16_t BufferSize = 2048;
|
||||
uint64_t lastStateUpdate = 0;
|
||||
uint64_t lastSuccessfulLoop = 0;
|
||||
|
||||
String pubTopic;
|
||||
String subTopic;
|
||||
|
||||
AmsFirmwareUpdater* updater = NULL;
|
||||
bool rebootSuggested = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "FirmwareVersion.h"
|
||||
#include "AmsStorage.h"
|
||||
#include "LittleFS.h"
|
||||
#include "Uptime.h"
|
||||
|
||||
void AmsMqttHandler::setCaVerification(bool caVerification) {
|
||||
this->caVerification = caVerification;
|
||||
@@ -33,15 +34,18 @@ bool AmsMqttHandler::connect() {
|
||||
if(epoch < FirmwareVersion::BuildEpoch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applySslConfiguration = mqttConfigChanged;
|
||||
if(mqttSecureClient == NULL) {
|
||||
mqttSecureClient = new WiFiClientSecure();
|
||||
#if defined(ESP8266)
|
||||
mqttSecureClient->setBufferSizes(512, 512);
|
||||
return false;
|
||||
#endif
|
||||
applySslConfiguration = true;
|
||||
}
|
||||
|
||||
if(mqttConfigChanged) {
|
||||
if(applySslConfiguration) {
|
||||
if(caVerification && LittleFS.begin()) {
|
||||
File file;
|
||||
|
||||
@@ -99,6 +103,17 @@ 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);
|
||||
@@ -121,20 +136,9 @@ bool AmsMqttHandler::connect() {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
|
||||
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
|
||||
if(strlen(mqttConfig.subscribeTopic) > 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
|
||||
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
|
||||
}
|
||||
}
|
||||
mqtt.publish(statusTopic, "online", true, 0);
|
||||
_connected = mqtt.publish(statusTopic, "online", true, 0);
|
||||
mqtt.loop();
|
||||
defaultSubscribe();
|
||||
postConnect();
|
||||
return true;
|
||||
} else {
|
||||
@@ -154,13 +158,29 @@ 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();
|
||||
if(mqttSecureClient != NULL) {
|
||||
delete mqttSecureClient;
|
||||
mqttSecureClient = NULL;
|
||||
}
|
||||
_connected = false;
|
||||
delay(10);
|
||||
yield();
|
||||
}
|
||||
@@ -170,12 +190,25 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::connected() {
|
||||
return mqtt.connected();
|
||||
return _connected && mqtt.connected();
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::loop() {
|
||||
bool ret = mqtt.loop();
|
||||
delay(10);
|
||||
uint64_t now = millis64();
|
||||
bool ret = connected() && mqtt.loop();
|
||||
if(ret) {
|
||||
lastSuccessfulLoop = now;
|
||||
} else if(mqttConfig.rebootMinutes > 0) {
|
||||
if(now - lastSuccessfulLoop > (uint64_t) mqttConfig.rebootMinutes * 60000) {
|
||||
// Reboot the device if the MQTT connection is lost for too long
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("MQTT connection lost for over %d minutes, rebooting device\n"), mqttConfig.rebootMinutes);
|
||||
rebootSuggested = true;
|
||||
}
|
||||
}
|
||||
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
|
||||
yield();
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
@@ -183,4 +216,8 @@ bool AmsMqttHandler::loop() {
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::isRebootSuggested() {
|
||||
return rebootSuggested;
|
||||
}
|
||||
@@ -169,9 +169,24 @@ bool CloudConnector::init() {
|
||||
}
|
||||
|
||||
void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
if(!config.enabled) return;
|
||||
unsigned long now = millis();
|
||||
if(now-lastUpdate < config.interval*1000) return;
|
||||
if(now-lastUpdate < ((unsigned long)config.interval)*1000) {
|
||||
return;
|
||||
};
|
||||
bool sendFirst = lastUpdate == 0;
|
||||
lastUpdate = now;
|
||||
if(config.enabled) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(CloudConnector) Enabled\n"));
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(CloudConnector) Not enabled\n"));
|
||||
return;
|
||||
}
|
||||
if(!ESPRandom::isValidV4Uuid(config.clientId)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
@@ -179,15 +194,19 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
debugger->printf_P(PSTR("(CloudConnector) Client ID is not valid\n"));
|
||||
return;
|
||||
}
|
||||
if(data.getListType() < 2) return;
|
||||
if(data.getListType() < 2) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(CloudConnector) List type not enough data\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(!initialized && !init()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Unable to initialize cloud connector\n"));
|
||||
lastUpdate = now;
|
||||
config.enabled = false;
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
@@ -202,7 +221,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
}
|
||||
|
||||
bool sendData = true;
|
||||
if(lastUpdate == 0) {
|
||||
if(sendFirst) {
|
||||
seed.clear();
|
||||
if(mainFuse > 0 && distributionSystem > 0) {
|
||||
int voltage = distributionSystem == 2 ? 400 : 230;
|
||||
@@ -460,7 +479,13 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
uint16_t crc = crc16((uint8_t*) clearBuffer, pos);
|
||||
pos += snprintf_P(clearBuffer+pos, CC_BUF_SIZE-pos, PSTR(",\"crc\":\"%04X\"}"), crc);
|
||||
|
||||
if(rsa == nullptr) return;
|
||||
if(rsa == nullptr) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("RSA is null\n"));
|
||||
return;
|
||||
}
|
||||
int ret = mbedtls_rsa_check_pubkey(rsa);
|
||||
if(ret != 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -481,7 +506,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
Stream *stream = NULL;
|
||||
|
||||
if(config.proto == 0) {
|
||||
udp.beginPacket(config.hostname,7443);
|
||||
udp.beginPacket(config.hostname, config.port);
|
||||
stream = &udp;
|
||||
} else if(config.proto == 1) {
|
||||
if(!tcp.connected()) {
|
||||
@@ -493,7 +518,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
debugger->printf_P(PSTR("tcp.connect(%s, %d) return code: %d\n"), config.hostname, config.port, ret);
|
||||
return;
|
||||
}
|
||||
tcp.setTimeout(config.interval * 2);
|
||||
tcp.setTimeout((config.interval * 1000) / 2);
|
||||
}
|
||||
while(tcp.available()) tcp.read(); // Empty incoming buffer
|
||||
stream = &tcp;
|
||||
@@ -521,6 +546,7 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
if(ret == 0) {
|
||||
if(stream != NULL) {
|
||||
stream->write(encryptedBuffer, rsa->len);
|
||||
stream->flush();
|
||||
} else {
|
||||
memcpy(httpBuffer + sendBytes, encryptedBuffer, rsa->len);
|
||||
}
|
||||
@@ -565,12 +591,11 @@ void CloudConnector::update(AmsData& data, EnergyAccounting& ea) {
|
||||
http.end();
|
||||
}
|
||||
}
|
||||
lastUpdate = now;
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("%d bytes sent to %s:%d from %s\n"), sendBytes, config.hostname, config.proto == 2 ? 80 : config.port, uuid.c_str());
|
||||
debugger->printf_P(PSTR("(CloudConnector) %d bytes sent to %s:%d from %s\n"), sendBytes, config.hostname, config.proto == 2 ? 80 : config.port, uuid.c_str());
|
||||
}
|
||||
|
||||
void CloudConnector::forceUpdate() {
|
||||
|
||||
@@ -114,6 +114,7 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
|
||||
debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n"));
|
||||
}
|
||||
}
|
||||
this->config = config;
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
@@ -147,6 +148,9 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
|
||||
{
|
||||
debugger->printf_P(PSTR("Successfully connected to Ethernet!\n"));
|
||||
}
|
||||
if(config.ipv6 && !ETH.enableIpV6()) {
|
||||
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
this->config = config;
|
||||
};
|
||||
#else
|
||||
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
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(String data);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
bool DomoticzMqttHandler::publishRaw(String data) {
|
||||
bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,35 @@
|
||||
#include "PriceService.h"
|
||||
|
||||
struct EnergyAccountingPeak {
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint16_t value;
|
||||
};
|
||||
|
||||
struct EnergyAccountingPeak6 {
|
||||
uint8_t day;
|
||||
uint16_t value;
|
||||
};
|
||||
|
||||
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;
|
||||
uint32_t lastMonthImport;
|
||||
uint32_t lastMonthExport;
|
||||
uint8_t lastMonthAccuracy;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
time_t lastUpdated;
|
||||
};
|
||||
|
||||
struct EnergyAccountingData6 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
int32_t costYesterday;
|
||||
@@ -29,37 +53,7 @@ struct EnergyAccountingData {
|
||||
uint32_t lastMonthImport;
|
||||
uint32_t lastMonthExport;
|
||||
uint8_t lastMonthAccuracy;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData5 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
uint16_t incomeYesterday;
|
||||
uint16_t incomeThisMonth;
|
||||
uint16_t incomeLastMonth;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData4 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData2 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t maxHour;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
EnergyAccountingPeak6 peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingRealtimeData {
|
||||
@@ -124,7 +118,6 @@ public:
|
||||
void setData(EnergyAccountingData&);
|
||||
|
||||
void setCurrency(String currency);
|
||||
float getPriceForHour(uint8_t d, uint8_t h);
|
||||
|
||||
private:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -137,12 +130,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 };
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
EnergyAccountingRealtimeData* realtimeData = NULL;
|
||||
String currency = "";
|
||||
|
||||
void calcDayCost();
|
||||
bool updateMax(uint16_t val, uint8_t day);
|
||||
bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData*
|
||||
rtd->lastImportUpdateMillis = 0;
|
||||
rtd->lastExportUpdateMillis = 0;
|
||||
}
|
||||
this->realtimeData = rtd;
|
||||
realtimeData = rtd;
|
||||
}
|
||||
|
||||
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
|
||||
@@ -67,59 +67,55 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
breakTime(tz->toLocal(now), local);
|
||||
|
||||
if(!init) {
|
||||
this->realtimeData->lastImportUpdateMillis = 0;
|
||||
this->realtimeData->lastExportUpdateMillis = 0;
|
||||
this->realtimeData->currentHour = local.Hour;
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
realtimeData->lastImportUpdateMillis = 0;
|
||||
realtimeData->lastExportUpdateMillis = 0;
|
||||
realtimeData->currentHour = local.Hour;
|
||||
realtimeData->currentDay = local.Day;
|
||||
if(!load()) {
|
||||
data = { 6, local.Month,
|
||||
0, 0, 0, // Cost
|
||||
0, 0, 0, // Income
|
||||
data = { 7, local.Month,
|
||||
0, 0, 0, 0, // Cost
|
||||
0, 0, 0, 0, // Income
|
||||
0, 0, 0, // Last month import, export and accuracy
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
0, 0, // Peak 4
|
||||
0, 0 // Peak 5
|
||||
0, 0, 0, // Peak 1
|
||||
0, 0, 0, // Peak 2
|
||||
0, 0, 0, // Peak 3
|
||||
0, 0, 0, // Peak 4
|
||||
0, 0, 0 // Peak 5
|
||||
};
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
|
||||
float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
|
||||
if(!initPrice && importPrice != PRICE_NO_VALUE) {
|
||||
if(!initPrice && ps != NULL && ps->hasPrice()) {
|
||||
calcDayCost();
|
||||
}
|
||||
|
||||
if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
tmElements_t oneHrAgo, oneHrAgoLocal;
|
||||
breakTime(now-3600, oneHrAgo);
|
||||
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
|
||||
|
||||
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
|
||||
ret |= updateMax(val, oneHrAgoLocal.Day);
|
||||
ret |= updateMax(val, oneHrAgoLocal.Day, oneHrAgoLocal.Hour);
|
||||
|
||||
this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
if(local.Hour > 0) {
|
||||
calcDayCost();
|
||||
}
|
||||
realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
|
||||
this->realtimeData->use = 0;
|
||||
this->realtimeData->produce = 0;
|
||||
this->realtimeData->costHour = 0;
|
||||
this->realtimeData->incomeHour = 0;
|
||||
realtimeData->use = 0;
|
||||
realtimeData->produce = 0;
|
||||
realtimeData->costHour = 0;
|
||||
realtimeData->incomeHour = 0;
|
||||
|
||||
uint8_t prevDay = this->realtimeData->currentDay;
|
||||
if(local.Day != this->realtimeData->currentDay) {
|
||||
data.costYesterday = this->realtimeData->costDay * 100;
|
||||
data.costThisMonth += this->realtimeData->costDay * 100;
|
||||
this->realtimeData->costDay = 0;
|
||||
uint8_t prevDay = realtimeData->currentDay;
|
||||
if(local.Day != realtimeData->currentDay) {
|
||||
data.costYesterday = realtimeData->costDay * 100;
|
||||
data.costThisMonth += realtimeData->costDay * 100;
|
||||
realtimeData->costDay = 0;
|
||||
|
||||
data.incomeYesterday = this->realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += this->realtimeData->incomeDay * 100;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
data.incomeYesterday = realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += realtimeData->incomeDay * 100;
|
||||
realtimeData->incomeDay = 0;
|
||||
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
realtimeData->currentDay = local.Day;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
@@ -149,42 +145,49 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
data.lastMonthAccuracy = accuracy;
|
||||
|
||||
data.month = local.Month;
|
||||
this->realtimeData->currentThresholdIdx = 0;
|
||||
realtimeData->currentThresholdIdx = 0;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if(ret) {
|
||||
data.costToday = realtimeData->costDay * 100;
|
||||
data.incomeToday = realtimeData->incomeDay * 100;
|
||||
data.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis;
|
||||
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
|
||||
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhi > 0) {
|
||||
this->realtimeData->use += kwhi;
|
||||
realtimeData->use += kwhi;
|
||||
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
|
||||
if(importPrice != PRICE_NO_VALUE) {
|
||||
float cost = importPrice * kwhi;
|
||||
this->realtimeData->costHour += cost;
|
||||
this->realtimeData->costDay += cost;
|
||||
realtimeData->costHour += cost;
|
||||
realtimeData->costDay += cost;
|
||||
}
|
||||
}
|
||||
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis;
|
||||
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
|
||||
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhe > 0) {
|
||||
this->realtimeData->produce += kwhe;
|
||||
float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
realtimeData->produce += kwhe;
|
||||
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
float income = exportPrice * kwhe;
|
||||
this->realtimeData->incomeHour += income;
|
||||
this->realtimeData->incomeDay += income;
|
||||
realtimeData->incomeHour += income;
|
||||
realtimeData->incomeDay += income;
|
||||
}
|
||||
}
|
||||
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(config != NULL) {
|
||||
while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++;
|
||||
while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -192,28 +195,36 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
|
||||
void EnergyAccounting::calcDayCost() {
|
||||
time_t now = time(nullptr);
|
||||
tmElements_t local, utc;
|
||||
tmElements_t local, utc, lastUpdateUtc;
|
||||
if(tz == NULL) return;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
if(ps == NULL) return;
|
||||
|
||||
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
|
||||
if(initPrice) {
|
||||
this->realtimeData->costDay = 0;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
if(ps->hasPrice()) {
|
||||
breakTime(data.lastUpdated, lastUpdateUtc);
|
||||
uint8_t calcFromHour = 0;
|
||||
if(lastUpdateUtc.Day != local.Day || lastUpdateUtc.Month != local.Month || lastUpdateUtc.Year != local.Year) {
|
||||
realtimeData->costDay = 0;
|
||||
realtimeData->incomeDay = 0;
|
||||
calcFromHour = 0;
|
||||
} else {
|
||||
realtimeData->costDay = data.costToday / 100.0;
|
||||
realtimeData->incomeDay = data.incomeToday / 100.0;
|
||||
calcFromHour = lastUpdateUtc.Hour;
|
||||
}
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
|
||||
float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
float priceIn = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
if(priceIn != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourImport(utc.Hour);
|
||||
this->realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
}
|
||||
|
||||
float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
float priceOut = ps->getPriceForRelativeHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
if(priceOut != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourExport(utc.Hour);
|
||||
this->realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
}
|
||||
}
|
||||
initPrice = true;
|
||||
@@ -221,7 +232,7 @@ void EnergyAccounting::calcDayCost() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseThisHour() {
|
||||
return this->realtimeData->use;
|
||||
return realtimeData->use;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseToday() {
|
||||
@@ -231,7 +242,7 @@ float EnergyAccounting::getUseToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourImport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@@ -242,18 +253,20 @@ float EnergyAccounting::getUseThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayImport(i) / 1000.0;
|
||||
}
|
||||
return ret + getUseToday();
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseLastMonth() {
|
||||
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
float ret = (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
if(std::isnan(ret)) return 0.0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedThisHour() {
|
||||
return this->realtimeData->produce;
|
||||
return realtimeData->produce;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedToday() {
|
||||
@@ -263,7 +276,7 @@ float EnergyAccounting::getProducedToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourExport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@@ -274,22 +287,24 @@ float EnergyAccounting::getProducedThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayExport(i) / 1000.0;
|
||||
}
|
||||
return ret + getProducedToday();
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedLastMonth() {
|
||||
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
float ret = (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
if(std::isnan(ret)) return 0.0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostThisHour() {
|
||||
return this->realtimeData->costHour;
|
||||
return realtimeData->costHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostToday() {
|
||||
return this->realtimeData->costDay;
|
||||
return realtimeData->costDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostYesterday() {
|
||||
@@ -305,11 +320,11 @@ float EnergyAccounting::getCostLastMonth() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeThisHour() {
|
||||
return this->realtimeData->incomeHour;
|
||||
return realtimeData->incomeHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeToday() {
|
||||
return this->realtimeData->incomeDay;
|
||||
return realtimeData->incomeDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeYesterday() {
|
||||
@@ -327,7 +342,7 @@ float EnergyAccounting::getIncomeLastMonth() {
|
||||
uint8_t EnergyAccounting::getCurrentThreshold() {
|
||||
if(config == NULL)
|
||||
return 0;
|
||||
return config->thresholds[this->realtimeData->currentThresholdIdx];
|
||||
return config->thresholds[realtimeData->currentThresholdIdx];
|
||||
}
|
||||
|
||||
float EnergyAccounting::getMonthMax() {
|
||||
@@ -407,85 +422,31 @@ bool EnergyAccounting::load() {
|
||||
char buf[file.size()];
|
||||
file.readBytes(buf, file.size());
|
||||
|
||||
if(buf[0] == 6) {
|
||||
if(buf[0] == 7) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
memcpy(&this->data, data, sizeof(this->data));
|
||||
ret = true;
|
||||
} else if(buf[0] == 5) {
|
||||
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
|
||||
this->data = { 6, data->month,
|
||||
((uint32_t) data->costYesterday) * 10,
|
||||
((uint32_t) data->costThisMonth) * 100,
|
||||
((uint32_t) data->costLastMonth) * 100,
|
||||
((uint32_t) data->incomeYesterday) * 10,
|
||||
((uint32_t) data->incomeThisMonth) * 100,
|
||||
((uint32_t) data->incomeLastMonth) * 100,
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else if(buf[0] == 4) {
|
||||
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
|
||||
this->data = { 5, data->month,
|
||||
((uint32_t) data->costYesterday) * 10,
|
||||
((uint32_t) data->costThisMonth) * 100,
|
||||
((uint32_t) data->costLastMonth) * 100,
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else if(buf[0] == 3) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
this->data = { 5, data->month,
|
||||
data->costYesterday * 10,
|
||||
} else if(buf[0] == 6) {
|
||||
EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
|
||||
this->data = { 7, data->month,
|
||||
0, // Cost today
|
||||
data->costYesterday,
|
||||
data->costThisMonth,
|
||||
data->costLastMonth,
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
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
|
||||
};
|
||||
ret = true;
|
||||
} else {
|
||||
data = { 5, 0,
|
||||
0, 0, 0, // Cost
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
0, 0, // Peak 4
|
||||
0, 0 // Peak 5
|
||||
};
|
||||
if(buf[0] == 2) {
|
||||
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
|
||||
this->data.month = data->month;
|
||||
this->data.costYesterday = data->costYesterday * 10;
|
||||
this->data.costThisMonth = data->costThisMonth;
|
||||
this->data.costLastMonth = data->costLastMonth;
|
||||
uint8_t b = 0;
|
||||
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
|
||||
this->data.peaks[b].day = b;
|
||||
memcpy(&this->data.peaks[b].value, buf+i, 2);
|
||||
b++;
|
||||
if(b >= config->hours || b >= 5) break;
|
||||
}
|
||||
ret = true;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -518,11 +479,12 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
|
||||
this->data = data;
|
||||
}
|
||||
|
||||
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
|
||||
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
|
||||
for(uint8_t i = 0; i < 5; i++) {
|
||||
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
|
||||
if(val > data.peaks[i].value) {
|
||||
data.peaks[i].day = day;
|
||||
data.peaks[i].hour = hour;
|
||||
data.peaks[i].value = val;
|
||||
return true;
|
||||
}
|
||||
@@ -550,8 +512,3 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
|
||||
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);
|
||||
}
|
||||
@@ -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) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#else
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#endif
|
||||
this->boardType = boardType;
|
||||
this->hw = hw;
|
||||
setHomeAssistantConfig(config);
|
||||
setHomeAssistantConfig(config, hostname);
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(String data);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishFirmware();
|
||||
|
||||
bool postConnect();
|
||||
@@ -36,14 +36,10 @@ public:
|
||||
|
||||
uint8_t getFormat();
|
||||
|
||||
void setHomeAssistantConfig(HomeAssistantConfig config);
|
||||
void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
|
||||
private:
|
||||
uint8_t boardType;
|
||||
|
||||
String pubTopic;
|
||||
String subTopic;
|
||||
|
||||
|
||||
String deviceName;
|
||||
String deviceModel;
|
||||
String deviceUid;
|
||||
@@ -55,9 +51,9 @@ private:
|
||||
String updateTopic;
|
||||
String sensorNamePrefix;
|
||||
|
||||
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit;
|
||||
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
|
||||
bool tInit[32] = {false};
|
||||
bool prInit[38] = {false};
|
||||
uint8_t priceImportInit = 0, priceExportInit = 0;
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
|
||||
HwTools* hw;
|
||||
@@ -83,6 +79,7 @@ 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"P" : %lu,
|
||||
"t" : "%s"
|
||||
"t" : %s
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"tPO" : %.3f,
|
||||
"tQI" : %.3f,
|
||||
"tQO" : %.3f,
|
||||
"rtc" : "%s",
|
||||
"t" : "%s"
|
||||
"rtc" : %s,
|
||||
"t" : %s
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"U1" : %.2f,
|
||||
"U2" : %.2f,
|
||||
"U3" : %.2f,
|
||||
"t" : "%s"
|
||||
"t" : %s
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"tPO1" : %.3f,
|
||||
"tPO2" : %.3f,
|
||||
"tPO3" : %.3f,
|
||||
"t" : "%s"
|
||||
"t" : %s
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"name" : "%s%s",
|
||||
"stat_t" : "%s%s",
|
||||
"uniq_id" : "%s_%s",
|
||||
"obj_id" : "%s_%s",
|
||||
"unit_of_meas" : "%s",
|
||||
"default_entity_id" : "sensor.%s_%s",
|
||||
"val_tpl" : "{{ value_json.%s | is_defined }}",
|
||||
"expire_after" : %d,
|
||||
"dev" : {
|
||||
@@ -13,5 +12,8 @@
|
||||
"sw" : "%s",
|
||||
"mf" : "%s",
|
||||
"cu" : "%s"
|
||||
}%s%s%s%s%s%s
|
||||
}
|
||||
%s%s%s
|
||||
%s%s%s
|
||||
%s%s%s
|
||||
}
|
||||
@@ -19,12 +19,8 @@
|
||||
#include <esp_task_wdt.h>
|
||||
#endif
|
||||
|
||||
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config) {
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = false;
|
||||
|
||||
pubTopic = String(mqttConfig.publishTopic);
|
||||
subTopic = String(mqttConfig.subscribeTopic);
|
||||
if(subTopic.isEmpty()) subTopic = pubTopic+"/command";
|
||||
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
|
||||
|
||||
if(strlen(config.discoveryNameTag) > 0) {
|
||||
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
|
||||
@@ -32,21 +28,18 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
|
||||
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
|
||||
sensorNamePrefix = String(json);
|
||||
} else {
|
||||
deviceName = F("AMS reader");
|
||||
snprintf_P(json, 128, PSTR("AMS reader"));
|
||||
deviceName = String(json);
|
||||
sensorNamePrefix = "";
|
||||
}
|
||||
deviceModel = boardTypeToString(boardType);
|
||||
manufacturer = boardManufacturerToString(boardType);
|
||||
|
||||
char hostname[32];
|
||||
#if defined(ESP8266)
|
||||
strcpy(hostname, WiFi.hostname().c_str());
|
||||
#elif defined(ESP32)
|
||||
strcpy(hostname, WiFi.getHostname());
|
||||
#endif
|
||||
|
||||
stripNonAscii((uint8_t*) hostname, 32, false);
|
||||
deviceUid = String(hostname); // Maybe configurable in the future?
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Hostname is [%s]\n"), hostname);
|
||||
|
||||
if(strlen(config.discoveryHostname) > 0) {
|
||||
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
|
||||
@@ -60,36 +53,42 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
|
||||
deviceUrl = String(json);
|
||||
}
|
||||
|
||||
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");
|
||||
if(strlen(config.discoveryPrefix) == 0) {
|
||||
snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
|
||||
}
|
||||
|
||||
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() {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
return false;
|
||||
|
||||
if(time(nullptr) < FirmwareVersion::BuildEpoch)
|
||||
@@ -134,12 +133,7 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
|
||||
publishList1Sensors();
|
||||
|
||||
char pt[24];
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
|
||||
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
|
||||
return mqtt.publish(pubTopic + "/power", json);
|
||||
@@ -150,12 +144,7 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
|
||||
|
||||
char pt[24];
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
|
||||
snprintf_P(json, BufferSize, HA3_JSON,
|
||||
data->getListId().c_str(),
|
||||
@@ -181,20 +170,11 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
|
||||
|
||||
char mt[24];
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
|
||||
|
||||
char pt[24];
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
|
||||
snprintf_P(json, BufferSize, HA2_JSON,
|
||||
data->getActiveImportCounter(),
|
||||
@@ -212,12 +192,7 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
|
||||
|
||||
char pt[24];
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
|
||||
snprintf_P(json, BufferSize, HA4_JSON,
|
||||
data->getListId().c_str(),
|
||||
@@ -307,13 +282,8 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
|
||||
|
||||
time_t now = time(nullptr);
|
||||
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);
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
|
||||
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
@@ -343,13 +313,8 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
|
||||
|
||||
time_t now = time(nullptr);
|
||||
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);
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
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);
|
||||
@@ -358,9 +323,9 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
publishPriceSensors(ps);
|
||||
@@ -373,7 +338,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@@ -421,59 +386,73 @@ 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);
|
||||
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
|
||||
}
|
||||
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);
|
||||
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
|
||||
}
|
||||
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);
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
|
||||
}
|
||||
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{"), WiFi.macAddress().c_str());
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":null,"), i);
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":%.4f,"), i, values[i]);
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(rteInit && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}"),
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_EXPORT, now, 0);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":null}"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":%.4f}"), val);
|
||||
}
|
||||
|
||||
char pt[24];
|
||||
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);
|
||||
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
|
||||
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
@@ -484,7 +463,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
return false;
|
||||
|
||||
publishSystemSensors();
|
||||
@@ -492,14 +471,9 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
|
||||
|
||||
time_t now = time(nullptr);
|
||||
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);
|
||||
}
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
|
||||
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),
|
||||
@@ -515,18 +489,22 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
|
||||
}
|
||||
|
||||
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
|
||||
String uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
String uid;
|
||||
if(strlen(sensor.uid) > 0) {
|
||||
uid = String(sensor.uid);
|
||||
} else {
|
||||
uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
}
|
||||
snprintf_P(json, BufferSize, HADISCOVER_JSON,
|
||||
sensorNamePrefix.c_str(),
|
||||
sensor.name,
|
||||
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(),
|
||||
@@ -535,14 +513,21 @@ 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.stacl) > 0 ? "\"" : "",
|
||||
|
||||
strlen_P(sensor.uom) > 0 ? ",\"unit_of_meas\":\"" : "",
|
||||
strlen_P(sensor.uom) > 0 ? (char *) FPSTR(sensor.uom) : "",
|
||||
strlen_P(sensor.uom) > 0 ? "\"" : ""
|
||||
);
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
|
||||
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
|
||||
loop();
|
||||
}
|
||||
|
||||
@@ -631,7 +616,8 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
|
||||
RealtimePeakSensor.ttl,
|
||||
RealtimePeakSensor.uom,
|
||||
RealtimePeakSensor.devcl,
|
||||
RealtimePeakSensor.stacl
|
||||
RealtimePeakSensor.stacl,
|
||||
RealtimePeakSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@@ -670,7 +656,8 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
|
||||
TemperatureSensor.ttl,
|
||||
TemperatureSensor.uom,
|
||||
TemperatureSensor.devcl,
|
||||
TemperatureSensor.stacl
|
||||
TemperatureSensor.stacl,
|
||||
TemperatureSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
tInit[index] = true;
|
||||
@@ -690,45 +677,96 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
|
||||
}
|
||||
pInit = true;
|
||||
}
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
if(prInit[i]) continue;
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) continue;
|
||||
|
||||
char name[strlen(PriceSensor.name)+2];
|
||||
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours");
|
||||
char path[strlen(PriceSensor.path)+1];
|
||||
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i);
|
||||
HomeAssistantSensor sensor = {
|
||||
i == 0 ? "Price current hour" : name,
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
i == 0 ? "total" : PriceSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
prInit[i] = true;
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
|
||||
if(priceImportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t importPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(importPriceSensorNo < priceImportInit) {
|
||||
importPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.import[%d]"), importPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("prices%d"), importPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Import price in %02d hour%s"), importPriceSensorNo, importPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Import price in %03d minutes"), importPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
importPriceSensorNo == 0 ? "Current import price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
importPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceImportInit = importPriceSensorNo++;
|
||||
}
|
||||
}
|
||||
|
||||
float exportPrice = ps->getValueForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
char path[20];
|
||||
snprintf(path, 20, "exportprices['%d']", 0);
|
||||
HomeAssistantSensor sensor = {
|
||||
"Export price current hour",
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
"total"
|
||||
};
|
||||
publishSensor(sensor);
|
||||
if(priceExportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t exportPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(exportPriceSensorNo < priceExportInit) {
|
||||
exportPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.export[%d]"), exportPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("exportprices%d"), exportPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Export price in %02d hour%s"), exportPriceSensorNo, exportPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Export price in %03d minutes"), exportPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
exportPriceSensorNo == 0 ? "Current export price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
exportPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceExportInit = exportPriceSensorNo++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void HomeAssistantMqttHandler::publishSystemSensors() {
|
||||
if(sInit) return;
|
||||
for(uint8_t i = 0; i < SystemSensorCount; i++) {
|
||||
@@ -751,7 +789,8 @@ void HomeAssistantMqttHandler::publishThresholdSensors() {
|
||||
RealtimeThresholdSensor.ttl,
|
||||
RealtimeThresholdSensor.uom,
|
||||
RealtimeThresholdSensor.devcl,
|
||||
RealtimeThresholdSensor.stacl
|
||||
RealtimeThresholdSensor.stacl,
|
||||
RealtimeThresholdSensor.uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@@ -762,15 +801,34 @@ uint8_t HomeAssistantMqttHandler::getFormat() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
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::publishFirmware() {
|
||||
if(!fInit) {
|
||||
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
|
||||
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"uniq_id\":\"%s_fwupgrade\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
|
||||
sensorNamePrefix.c_str(),
|
||||
pubTopic.c_str(),
|
||||
deviceUid.c_str(),
|
||||
subTopic.c_str()
|
||||
);
|
||||
fInit = mqtt.publish(updateTopic + "/" + deviceUid + "/config", json, true, 0);
|
||||
@@ -795,9 +853,10 @@ 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 = false;
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
|
||||
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
|
||||
for(uint8_t i = 0; i < 38; i++) prInit[i] = false;
|
||||
priceImportInit = 0;
|
||||
priceExportInit = 0;
|
||||
}
|
||||
} else if(topic.equals(subTopic)) {
|
||||
if(payload.equals("fwupgrade")) {
|
||||
@@ -807,3 +866,14 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ 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();
|
||||
@@ -67,7 +68,9 @@ private:
|
||||
bool ledInvert, rgbInvert;
|
||||
uint8_t vccPin, vccGnd_r, vccVcc_r;
|
||||
float vccOffset, vccMultiplier;
|
||||
float maxVcc = 3.2; // 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
|
||||
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;
|
||||
|
||||
uint16_t analogRange = 1024;
|
||||
AdcConfig voltAdc, tempAdc;
|
||||
|
||||
@@ -22,6 +22,9 @@ bool HwTools::applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterC
|
||||
gpioConfig.vccResistorGnd = 22;
|
||||
gpioConfig.vccResistorVcc = 33;
|
||||
gpioConfig.ledDisablePin = 6;
|
||||
gpioConfig.vccBootLimit = 0;
|
||||
gpioConfig.vccOffset = 0;
|
||||
gpioConfig.vccMultiplier = 0;
|
||||
return true;
|
||||
case 51: // Wemos S2 mini
|
||||
gpioConfig.ledPin = 15;
|
||||
@@ -654,8 +657,12 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
|
||||
}
|
||||
|
||||
bool HwTools::isVoltageOptimal(float range) {
|
||||
if(boardType >= 5 && boardType <= 7 && maxVcc > 2.8) { // Pow-*
|
||||
float vcc = getVcc();
|
||||
if(boardType >= 1 && boardType <= 8 && maxVcc > 2.8) { // BUS-Power boards
|
||||
unsigned long now = millis();
|
||||
if(now - lastVccRead > 250) {
|
||||
vcc = getVcc();
|
||||
lastVccRead = now;
|
||||
}
|
||||
if(vcc > 3.4 || vcc < 2.8) {
|
||||
maxVcc = 0; // Voltage is outside the operating range, we have to assume voltage is OK
|
||||
} else if(vcc > maxVcc) {
|
||||
@@ -670,4 +677,8 @@ bool HwTools::isVoltageOptimal(float range) {
|
||||
|
||||
uint8_t HwTools::getBoardType() {
|
||||
return boardType;
|
||||
}
|
||||
|
||||
void HwTools::setMaxVcc(float vcc) {
|
||||
this->maxVcc = min(3.3f, vcc);
|
||||
}
|
||||
@@ -12,19 +12,19 @@
|
||||
class JsonMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
this->hw = hw;
|
||||
};
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#else
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
this->hw = hw;
|
||||
};
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#endif
|
||||
this->hw = hw;
|
||||
this->ds = ds;
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(String data);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishFirmware();
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
|
||||
@@ -32,6 +32,9 @@ 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);
|
||||
@@ -39,5 +42,6 @@ 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
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
#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(!mqtt.connected()) {
|
||||
if(!connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -44,6 +45,15 @@ bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcc
|
||||
ret = publishList4(&data, ea);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
loop();
|
||||
return ret;
|
||||
}
|
||||
@@ -67,14 +77,24 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
|
||||
} else {
|
||||
memset(pf, 0, 4);
|
||||
}
|
||||
|
||||
String peaks = "";
|
||||
uint8_t peakCount = ea->getConfig()->hours;
|
||||
if(peakCount > 5) peakCount = 5;
|
||||
for(uint8_t i = 1; i <= peakCount; i++) {
|
||||
if(!peaks.isEmpty()) peaks += ",";
|
||||
peaks += String(ea->getPeak(i).value / 100.0, 2);
|
||||
}
|
||||
|
||||
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"),
|
||||
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
|
||||
strlen(pf) == 0 ? "},\"realtime\":{" : ",",
|
||||
pf,
|
||||
ea->getUseThisHour(),
|
||||
pf,
|
||||
ea->getUseToday(),
|
||||
pf,
|
||||
ea->getUseThisMonth(),
|
||||
pf,
|
||||
ea->getCurrentThreshold(),
|
||||
pf,
|
||||
ea->getMonthMax(),
|
||||
@@ -82,6 +102,9 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
|
||||
ea->getProducedThisHour(),
|
||||
pf,
|
||||
ea->getProducedToday(),
|
||||
pf,
|
||||
ea->getProducedThisMonth(),
|
||||
peaks.c_str(),
|
||||
strlen(pf) == 0 ? "}" : ""
|
||||
);
|
||||
}
|
||||
@@ -272,9 +295,9 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@@ -285,7 +308,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@@ -333,59 +356,89 @@ 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);
|
||||
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
|
||||
}
|
||||
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);
|
||||
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
|
||||
}
|
||||
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);
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts6hr, sizeof(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 {
|
||||
strcpy_P(pf, PSTR("pr_"));
|
||||
}
|
||||
if(mqttConfig.payloadFormat == 6) {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
|
||||
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":null,"), pf, i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":%.4f,"), pf, i, values[i]);
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":null,"), i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":%.4f,"), i, values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%smin\":%.4f,\"%smax\":%.4f,\"%scheapest1hr\":\"%s\",\"%scheapest3hr\":\"%s\",\"%scheapest6hr\":\"%s\"}"),
|
||||
pf,
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
pf,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
pf,
|
||||
ts1hr,
|
||||
pf,
|
||||
ts3hr,
|
||||
pf,
|
||||
ts6hr
|
||||
);
|
||||
if(mqttConfig.payloadFormat != 6) {
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":%s,\"pr_cheapest3hr\":%s,\"pr_cheapest6hr\":%s}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
} else {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
}
|
||||
bool ret = false;
|
||||
if(mqttConfig.payloadFormat == 5) {
|
||||
@@ -400,7 +453,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
|
||||
return false;
|
||||
|
||||
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
|
||||
@@ -428,9 +481,77 @@ uint8_t JsonMqttHandler::getFormat() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
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::publishFirmware() {
|
||||
snprintf_P(json, BufferSize, PSTR("{\"installed_version\":\"%s\",\"latest_version\":\"%s\",\"title\":\"amsreader firmware\",\"release_url\":\"https://github.com/UtilitechAS/amsreader-firmware/releases\",\"release_summary\":\"New version %s is available\",\"update_percentage\":%s}"),
|
||||
FirmwareVersion::VersionString,
|
||||
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
|
||||
strlen(updater->getNextVersion()) == 0 ? FirmwareVersion::VersionString : updater->getNextVersion(),
|
||||
updater->getProgress() < 0 ? "null" : String(updater->getProgress(), 0)
|
||||
);
|
||||
char topic[192];
|
||||
snprintf_P(topic, 192, PSTR("%s/firmware"), mqttConfig.publishTopic);
|
||||
bool ret = mqtt.publish(topic, json);
|
||||
loop();
|
||||
return ret;
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "AmsConfiguration.h"
|
||||
#include "DataParser.h"
|
||||
#include "Cosem.h"
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
#include "RemoteDebug.h"
|
||||
#endif
|
||||
|
||||
#define NOVALUE 0xFFFFFFFF
|
||||
|
||||
@@ -21,7 +24,11 @@ struct AmsOctetTimestamp {
|
||||
|
||||
class IEC6205675 : public AmsData {
|
||||
public:
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675(const char* payload, Timezone* tz, 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);
|
||||
#endif
|
||||
|
||||
private:
|
||||
CosemData* getCosemDataAt(uint8_t index, const char* ptr);
|
||||
@@ -30,8 +37,9 @@ private:
|
||||
float getNumber(uint8_t* obis, int matchlength, const char* ptr);
|
||||
float getNumber(CosemData*);
|
||||
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
|
||||
time_t adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType);
|
||||
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
|
||||
uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 };
|
||||
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };
|
||||
|
||||
@@ -12,7 +12,22 @@
|
||||
#include "DataParser.h"
|
||||
#include "Cosem.h"
|
||||
|
||||
struct Lng2Data_3p {
|
||||
struct Lng2Data_3p_0b {
|
||||
CosemBasic header;
|
||||
CosemLongUnsigned u1;
|
||||
CosemLongUnsigned u2;
|
||||
CosemLongUnsigned u3;
|
||||
CosemLongUnsigned i1;
|
||||
CosemLongUnsigned i2;
|
||||
CosemLongUnsigned i3;
|
||||
CosemDLongUnsigned activeImport;
|
||||
CosemDLongUnsigned activeExport;
|
||||
CosemDLongUnsigned acumulatedImport;
|
||||
CosemDLongUnsigned accumulatedExport;
|
||||
CosemString meterId;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Lng2Data_3p_0e {
|
||||
CosemBasic header;
|
||||
CosemLongUnsigned u1;
|
||||
CosemLongUnsigned u2;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#endif
|
||||
#include "AmsData.h"
|
||||
#include "AmsConfiguration.h"
|
||||
#include "AmsMqttHandler.h"
|
||||
|
||||
class MeterCommunicator {
|
||||
public:
|
||||
@@ -24,6 +25,13 @@ 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
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "AmsConfiguration.h"
|
||||
#include "DataParsers.h"
|
||||
#include "Timezone.h"
|
||||
#include "PassthroughMqttHandler.h"
|
||||
#include "AmsMqttHandler.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include "SoftwareSerial.h"
|
||||
@@ -36,7 +36,9 @@ public:
|
||||
bool isConfigChanged();
|
||||
void ackConfigChanged();
|
||||
void getCurrentConfig(MeterConfig& meterConfig);
|
||||
void setPassthroughMqttHandler(PassthroughMqttHandler*);
|
||||
void setTimezone(Timezone* tz) {
|
||||
this->tz = tz;
|
||||
};
|
||||
|
||||
HardwareSerial* getHwSerial();
|
||||
void rxerr(int err);
|
||||
@@ -51,8 +53,6 @@ protected:
|
||||
bool configChanged = false;
|
||||
Timezone* tz;
|
||||
|
||||
PassthroughMqttHandler* pt = NULL;
|
||||
|
||||
uint8_t *hanBuffer = NULL;
|
||||
uint16_t hanBufferSize = 0;
|
||||
Stream *hanSerial;
|
||||
@@ -62,11 +62,12 @@ protected:
|
||||
HardwareSerial *hwSerial = NULL;
|
||||
uint8_t rxBufferErrors = 0;
|
||||
|
||||
bool autodetect = false, validDataReceived = false;
|
||||
bool autodetect = false;
|
||||
uint8_t validDataReceived = 0;
|
||||
unsigned long meterAutodetectLastChange = 0;
|
||||
long rate = 10000;
|
||||
uint32_t autodetectBaud = 0;
|
||||
uint8_t autodetectParity = 11;
|
||||
uint8_t autodetectParity = 11; // 8E1
|
||||
bool autodetectInvert = false;
|
||||
uint8_t autodetectCount = 0;
|
||||
|
||||
@@ -91,6 +92,7 @@ protected:
|
||||
int16_t unwrapData(uint8_t *buf, DataParserContext &context);
|
||||
void printHanReadError(int pos);
|
||||
void handleAutodetect(unsigned long now);
|
||||
uint8_t getNextParity(uint8_t parityOrdinal);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,8 @@ IEC6205621::IEC6205621(const char* p, Timezone* tz, MeterConfig* meterConfig) {
|
||||
if(strlen(p) < 16)
|
||||
return;
|
||||
|
||||
this->packageTimestamp = time(nullptr);
|
||||
|
||||
String payload(p+1);
|
||||
|
||||
lastUpdateMillis = millis64();
|
||||
@@ -59,15 +61,44 @@ IEC6205621::IEC6205621(const char* p, Timezone* tz, MeterConfig* meterConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
tmElements_t tm { 0, 0, 0, 0, 0, 0, 0 };
|
||||
String timestamp = extract(payload, F("1.0.0"));
|
||||
if(timestamp.length() > 10) {
|
||||
tmElements_t tm;
|
||||
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
|
||||
tm.Month = timestamp.substring(4,6).toInt();
|
||||
tm.Day = timestamp.substring(2,4).toInt();
|
||||
tm.Hour = timestamp.substring(6,8).toInt();
|
||||
tm.Minute = timestamp.substring(8,10).toInt();
|
||||
tm.Second = timestamp.substring(10,12).toInt();
|
||||
if(timestamp.length() == 13) { // yyMMddHHmmssX
|
||||
char x = timestamp.charAt(12);
|
||||
if(x == 'S' || x == 'W') {
|
||||
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
|
||||
tm.Month = timestamp.substring(2,4).toInt();
|
||||
tm.Day = timestamp.substring(4,6).toInt();
|
||||
tm.Hour = timestamp.substring(6,8).toInt();
|
||||
tm.Minute = timestamp.substring(8,10).toInt();
|
||||
tm.Second = timestamp.substring(10,12).toInt();
|
||||
}
|
||||
} else if(timestamp.length() == 17) { // yyyyMMdd HH:mm:ss
|
||||
char x = timestamp.charAt(11);
|
||||
char y = timestamp.charAt(14);
|
||||
if(x == ':' && y == ':') {
|
||||
tm.Year = (timestamp.substring(0,4).toInt()) - 1970;
|
||||
tm.Month = timestamp.substring(4,6).toInt();
|
||||
tm.Day = timestamp.substring(6,8).toInt();
|
||||
tm.Hour = timestamp.substring(9,11).toInt();
|
||||
tm.Minute = timestamp.substring(12,14).toInt();
|
||||
tm.Second = timestamp.substring(15,17).toInt();
|
||||
}
|
||||
} else if(timestamp.length() == 19) { // yyyy-MM-dd HH:mm:ss
|
||||
char x = timestamp.charAt(4);
|
||||
char y = timestamp.charAt(13);
|
||||
if(x == '-' && y == ':') {
|
||||
tm.Year = (timestamp.substring(0,4).toInt()) - 1970;
|
||||
tm.Month = timestamp.substring(5,7).toInt();
|
||||
tm.Day = timestamp.substring(8,10).toInt();
|
||||
tm.Hour = timestamp.substring(11,13).toInt();
|
||||
tm.Minute = timestamp.substring(14,16).toInt();
|
||||
tm.Second = timestamp.substring(17,19).toInt();
|
||||
}
|
||||
} else {
|
||||
meterTimestamp = 0;
|
||||
}
|
||||
if(tm.Year > 0) {
|
||||
meterTimestamp = makeTime(tm);
|
||||
if(tz != NULL) meterTimestamp = tz->toUTC(meterTimestamp);
|
||||
}
|
||||
|
||||
@@ -11,30 +11,220 @@
|
||||
#include "Uptime.h"
|
||||
#include "hexutils.h"
|
||||
|
||||
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675::IEC6205675(const char* d, Timezone* tz, 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) {
|
||||
#endif
|
||||
float val;
|
||||
char str[64];
|
||||
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
Timezone tz(CEST, CET);
|
||||
|
||||
this->packageTimestamp = ctx.timestamp == 0 ? time(nullptr) : ctx.timestamp;
|
||||
this->packageTimestamp = time(nullptr); // ctx.timestamp is mostly garbage, so we use current time as package timestamp
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
|
||||
if(val == NOVALUE) {
|
||||
CosemData* data = getCosemDataAt(1, ((char *) (d)));
|
||||
|
||||
// Kaifa special case...
|
||||
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
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
|
||||
listType = 1;
|
||||
meterType = AmsTypeKaifa;
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(data->base.type == CosemTypeOctetString) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
|
||||
} else if(data->base.type == CosemTypeOctetString) { // Assuming first string is a list identifier
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
String listId = String(str);
|
||||
@@ -42,7 +232,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
this->listId = listId;
|
||||
meterType = AmsTypeKaifa;
|
||||
|
||||
int idx = 0;
|
||||
uint8_t idx = 0;
|
||||
data = getCosemDataAt(idx, ((char *) (d)));
|
||||
idx+=2;
|
||||
if(data->base.length == 0x0D || data->base.length == 0x12) {
|
||||
@@ -123,7 +313,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
if(data->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
meterTimestamp = tz != NULL ? tz->toUTC(ts) : ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,11 +334,11 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
this->listId = listId;
|
||||
meterType = AmsTypeIskra;
|
||||
|
||||
int idx = 0;
|
||||
uint8_t idx = 0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data->base.length == 0x12) {
|
||||
apply(state);
|
||||
listType = state.getListType() > 2 ? state.getListType() : 2;
|
||||
listType = state.getListType() > 4 ? state.getListType() : 4;
|
||||
|
||||
// 42.0.0 COSEM logical device name
|
||||
idx++;
|
||||
@@ -327,7 +517,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
CosemData* no7 = getCosemDataAt(7, ((char *) (d)));
|
||||
if(no7->base.type == CosemTypeLongUnsigned) {
|
||||
apply(state);
|
||||
listType = state.getListType() > 2 ? state.getListType() : 2;
|
||||
listType = state.getListType() > 4 ? state.getListType() : 4;
|
||||
|
||||
// 42.0.0 COSEM logical device name
|
||||
idx++;
|
||||
@@ -559,49 +749,31 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
|
||||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
if(d1->base.type == CosemTypeDLongUnsigned &&
|
||||
d2->base.type == CosemTypeDLongUnsigned &&
|
||||
d3->base.type == CosemTypeDLongUnsigned &&
|
||||
d7->base.type == CosemTypeOctetString &&
|
||||
d8->base.type == CosemTypeOctetString
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
listType = 3;
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(useMeterType == AmsTypeUnknown) {
|
||||
listType = 3;
|
||||
} else if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
|
||||
meterType = AmsTypeIskra;
|
||||
lastUpdateMillis = millis64();
|
||||
listType = 3;
|
||||
} else {
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
@@ -612,7 +784,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else { // OBIS code parsing
|
||||
listType = 1;
|
||||
activeImportPower = val;
|
||||
|
||||
@@ -635,12 +807,6 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
}
|
||||
}
|
||||
|
||||
if(this->packageTimestamp > 0) {
|
||||
if(meterType == AmsTypeKamstrup) {
|
||||
this->packageTimestamp = this->packageTimestamp - 3600;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
@@ -740,14 +906,7 @@ IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterCo
|
||||
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
if(amst->dt.deviation == 0x8000) { // Deviation not specified, adjust from localtime to UTC
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
} else if(meterType == AmsTypeAidon) {
|
||||
meterTimestamp = ts - 3600; // 21.09.24, the clock is now correct
|
||||
} else {
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
this->meterTimestamp = adjustForKnownIssues(amst->dt, tz, meterType == AmsTypeUnknown ? useMeterType : meterType);
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
|
||||
@@ -1123,3 +1282,24 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
time_t IEC6205675::adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType) {
|
||||
time_t ts = decodeCosemDateTime(dt);
|
||||
int16_t deviation = ntohs(dt.deviation);
|
||||
if(deviation < -720 || deviation > 720) {
|
||||
// Time zone not specified
|
||||
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
|
||||
// Special known case
|
||||
// 21.09.24, the clock is now correct for Aidon
|
||||
// 23.10.25, the clock is now correct for Kamstrup
|
||||
ts -= 3600;
|
||||
} else 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;
|
||||
}
|
||||
@@ -42,33 +42,75 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
|
||||
switch(descriptor->obis[2]) {
|
||||
case 1:
|
||||
o170 = getNumber(item);
|
||||
if(meterConfig->wattageMultiplier > 0) {
|
||||
o170 = o170 > 0 ? o170 * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
o270 = getNumber(item);
|
||||
if(meterConfig->wattageMultiplier > 0) {
|
||||
o270 = o270 > 0 ? o270 * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
reactiveImportPower = getNumber(item);
|
||||
if(meterConfig->wattageMultiplier > 0) {
|
||||
reactiveImportPower = reactiveImportPower > 0 ? reactiveImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
reactiveExportPower = getNumber(item);
|
||||
if(meterConfig->wattageMultiplier > 0) {
|
||||
reactiveExportPower = reactiveExportPower > 0 ? reactiveExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 21:
|
||||
l1activeImportPower = getNumber(item);
|
||||
listType = listType >= 4 ? listType : 4;
|
||||
break;
|
||||
case 41:
|
||||
l2activeImportPower = getNumber(item);
|
||||
listType = listType >= 4 ? listType : 4;
|
||||
break;
|
||||
case 61:
|
||||
l3activeImportPower = getNumber(item);
|
||||
listType = listType >= 4 ? listType : 4;
|
||||
break;
|
||||
case 31:
|
||||
l1current = getNumber(item) / 100.0;
|
||||
if(meterConfig->amperageMultiplier > 0) {
|
||||
l1current = l1current > 0 ? l1current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 51:
|
||||
l2current = getNumber(item) / 100.0;
|
||||
if(meterConfig->amperageMultiplier > 0) {
|
||||
l2current = l2current > 0 ? l2current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 71:
|
||||
l3current = getNumber(item) / 100.0;
|
||||
if(meterConfig->amperageMultiplier > 0) {
|
||||
l3current = l3current > 0 ? l3current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 32:
|
||||
l1voltage = getNumber(item) / 10.0;
|
||||
if(meterConfig->voltageMultiplier > 0) {
|
||||
l1voltage = l1voltage > 0 ? l1voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 52:
|
||||
l2voltage = getNumber(item) / 10.0;
|
||||
if(meterConfig->voltageMultiplier > 0) {
|
||||
l2voltage = l2voltage > 0 ? l2voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 72:
|
||||
l3voltage = getNumber(item) / 10.0;
|
||||
if(meterConfig->voltageMultiplier > 0) {
|
||||
l3voltage = l3voltage > 0 ? l3voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -79,30 +121,54 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
|
||||
case 1:
|
||||
o180 = getNumber(item);
|
||||
activeImportCounter = o180 / 1000.0;
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
activeImportCounter = activeImportCounter > 0 ? activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
o280 = getNumber(item);
|
||||
activeExportCounter = o280 / 1000.0;
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
activeExportCounter = activeExportCounter > 0 ? activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
o380 = getNumber(item);
|
||||
reactiveImportCounter = o380 / 1000.0;
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
reactiveImportCounter = reactiveImportCounter > 0 ? reactiveImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
o480 = getNumber(item);
|
||||
reactiveExportCounter = o480 / 1000.0;
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
reactiveExportCounter = reactiveExportCounter > 0 ? reactiveExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
o580 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o580 = o580 > 0 ? o580 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
o680 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o680 = o680 > 0 ? o680 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
o780 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o780 = o780 > 0 ? o780 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
o880 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o880 = o880 > 0 ? o880 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if(descriptor->obis[4] == 1) {
|
||||
@@ -110,9 +176,15 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
|
||||
switch(descriptor->obis[2]) {
|
||||
case 1:
|
||||
o181 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o181 = o181 > 0 ? o181 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
o281 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o281 = o281 > 0 ? o281 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if(descriptor->obis[4] == 2) {
|
||||
@@ -120,9 +192,15 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
|
||||
switch(descriptor->obis[2]) {
|
||||
case 1:
|
||||
o182 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o182 = o182 > 0 ? o182 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
o282 = getNumber(item);
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
o282 = o282 > 0 ? o282 * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -183,28 +261,6 @@ LNG::LNG(AmsData& meterState, const char* payload, uint8_t useMeterType, MeterCo
|
||||
lastUpdateMillis = millis64();
|
||||
}
|
||||
lastUpdateMillis = millis64();
|
||||
if(meterConfig->wattageMultiplier > 0) {
|
||||
activeImportPower = activeImportPower > 0 ? activeImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
activeExportPower = activeExportPower > 0 ? activeExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
reactiveImportPower = reactiveImportPower > 0 ? reactiveImportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
reactiveExportPower = reactiveExportPower > 0 ? reactiveExportPower * (meterConfig->wattageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
if(meterConfig->voltageMultiplier > 0) {
|
||||
l1voltage = l1voltage > 0 ? l1voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
l2voltage = l2voltage > 0 ? l2voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
l3voltage = l3voltage > 0 ? l3voltage * (meterConfig->voltageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
if(meterConfig->amperageMultiplier > 0) {
|
||||
l1current = l1current > 0 ? l1current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
l2current = l2current > 0 ? l2current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
l3current = l3current > 0 ? l3current * (meterConfig->amperageMultiplier / 1000.0) : 0;
|
||||
}
|
||||
if(meterConfig->accumulatedMultiplier > 0) {
|
||||
activeImportCounter = activeImportCounter > 0 ? activeImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
activeExportCounter = activeExportCounter > 0 ? activeExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
reactiveImportCounter = reactiveImportCounter > 0 ? reactiveImportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
reactiveExportCounter = reactiveExportCounter > 0 ? reactiveExportCounter * (meterConfig->accumulatedMultiplier / 1000.0) : 0;
|
||||
}
|
||||
|
||||
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
|
||||
if(!threePhase)
|
||||
|
||||
@@ -14,7 +14,33 @@ LNG2::LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, Meter
|
||||
meterType = AmsTypeLandisGyr;
|
||||
this->packageTimestamp = ctx.timestamp;
|
||||
|
||||
Lng2Data_3p* d = (Lng2Data_3p*) payload;
|
||||
Lng2Data_3p_0e* d = (Lng2Data_3p_0e*) payload;
|
||||
this->l1voltage = ntohs(d->u1.data);
|
||||
this->l2voltage = ntohs(d->u2.data);
|
||||
this->l3voltage = ntohs(d->u3.data);
|
||||
|
||||
this->l1current = ntohs(d->i1.data) / 100.0;
|
||||
this->l2current = ntohs(d->i2.data) / 100.0;
|
||||
this->l3current = ntohs(d->i3.data) / 100.0;
|
||||
|
||||
this->activeImportPower = ntohl(d->activeImport.data);
|
||||
this->activeExportPower = ntohl(d->activeExport.data);
|
||||
this->activeImportCounter = ntohl(d->acumulatedImport.data) / 1000.0;
|
||||
this->activeExportCounter = ntohl(d->accumulatedExport.data) / 1000.0;
|
||||
|
||||
char str[64];
|
||||
uint8_t str_len = getString((CosemData*) &d->meterId, str);
|
||||
if(str_len > 0) {
|
||||
this->meterId = String(str);
|
||||
}
|
||||
listType = 3;
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(h->length == 0x0b) {
|
||||
apply(meterState);
|
||||
meterType = AmsTypeLandisGyr;
|
||||
this->packageTimestamp = ctx.timestamp;
|
||||
|
||||
Lng2Data_3p_0b* d = (Lng2Data_3p_0b*) payload;
|
||||
this->l1voltage = ntohs(d->u1.data);
|
||||
this->l2voltage = ntohs(d->u2.data);
|
||||
this->l3voltage = ntohs(d->u3.data);
|
||||
|
||||
@@ -174,8 +174,8 @@ bool PassiveMeterCommunicator::loop() {
|
||||
lastError = pos;
|
||||
printHanReadError(pos);
|
||||
len += hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes(hanBuffer, len);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(hanBuffer, len);
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
@@ -229,25 +229,25 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
|
||||
char* payload = ((char *) (hanBuffer)) + pos;
|
||||
if(maxDetectedPayloadSize < pos) maxDetectedPayloadSize = pos;
|
||||
if(ctx.type == DATA_TAG_DLMS) {
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes((uint8_t*) payload, ctx.length);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw((uint8_t*) payload, ctx.length);
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Using application data:\n"));
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Using application data:\n"));
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugPrint((byte*) payload, 0, ctx.length, debugger);
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugPrint((byte*) payload, 0, ctx.length, debugger);
|
||||
|
||||
// Rudimentary detector for L&G proprietary format, this is terrible code... Fix later
|
||||
if(payload[0] == CosemTypeStructure && payload[2] == CosemTypeArray && payload[1] == payload[3]) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("LNG\n"));
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("LNG\n"));
|
||||
LNG lngData = LNG(meterState, payload, meterState.getMeterType(), &meterConfig, ctx);
|
||||
if(lngData.getListType() >= 1) {
|
||||
data = new AmsData();
|
||||
@@ -263,9 +263,9 @@ debugger->printf_P(PSTR("LNG\n"));
|
||||
payload[17] == CosemTypeLongUnsigned
|
||||
) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("LNG2\n"));
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("LNG2\n"));
|
||||
LNG2 lngData = LNG2(meterState, payload, meterState.getMeterType(), &meterConfig, ctx);
|
||||
if(lngData.getListType() >= 1) {
|
||||
data = new AmsData();
|
||||
@@ -274,11 +274,11 @@ debugger->printf_P(PSTR("LNG2\n"));
|
||||
}
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("DLMS\n"));
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("DLMS\n"));
|
||||
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
|
||||
data = new IEC6205675(payload, meterState.getMeterType(), &meterConfig, ctx, meterState);
|
||||
data = new IEC6205675(payload, tz, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
|
||||
}
|
||||
} else if(ctx.type == DATA_TAG_DSMR) {
|
||||
data = new IEC6205621(payload, tz, &meterConfig);
|
||||
@@ -286,7 +286,7 @@ debugger->printf_P(PSTR("DLMS\n"));
|
||||
len = 0;
|
||||
if(data != NULL) {
|
||||
if(data->getListType() > 0) {
|
||||
validDataReceived = true;
|
||||
validDataReceived++;
|
||||
if(rxBufferErrors > 0) rxBufferErrors--;
|
||||
}
|
||||
}
|
||||
@@ -299,9 +299,9 @@ int PassiveMeterCommunicator::getLastError() {
|
||||
if(hwSerial != NULL) {
|
||||
if(hwSerial->hasRxError()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial RX error\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial RX error\n"));
|
||||
lastError = 96;
|
||||
}
|
||||
if(hwSerial->hasOverrun()) {
|
||||
@@ -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(pt != NULL) {
|
||||
pt->publishBytes(buf, curLen);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(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(pt != NULL) {
|
||||
pt->publishBytes(buf, curLen);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(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(pt != NULL) {
|
||||
pt->publishString((char*) buf);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(buf, curLen);
|
||||
}
|
||||
break;
|
||||
case DATA_TAG_SNRM:
|
||||
@@ -505,75 +505,75 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
|
||||
|
||||
void PassiveMeterCommunicator::printHanReadError(int pos) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
{
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
{
|
||||
switch(pos) {
|
||||
case DATA_PARSE_BOUNDRY_FLAG_MISSING:
|
||||
case DATA_PARSE_BOUNDARY_FLAG_MISSING:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Boundry flag missing\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Boundary flag missing\n"));
|
||||
break;
|
||||
case DATA_PARSE_HEADER_CHECKSUM_ERROR:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Header checksum error\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Header checksum error\n"));
|
||||
break;
|
||||
case DATA_PARSE_FOOTER_CHECKSUM_ERROR:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Frame checksum error\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Frame checksum error\n"));
|
||||
break;
|
||||
case DATA_PARSE_INCOMPLETE:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Received frame is incomplete\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Received frame is incomplete\n"));
|
||||
break;
|
||||
case GCM_AUTH_FAILED:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Decrypt authentication failed\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Decrypt authentication failed\n"));
|
||||
break;
|
||||
case GCM_ENCRYPTION_KEY_FAILED:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Setting decryption key failed\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Setting decryption key failed\n"));
|
||||
break;
|
||||
case GCM_DECRYPT_FAILED:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Decryption failed\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Decryption failed\n"));
|
||||
break;
|
||||
case MBUS_FRAME_LENGTH_NOT_EQUAL:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Frame length mismatch\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Frame length mismatch\n"));
|
||||
break;
|
||||
case DATA_PARSE_INTERMEDIATE_SEGMENT:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Intermediate segment received\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Intermediate segment received\n"));
|
||||
break;
|
||||
case DATA_PARSE_UNKNOWN_DATA:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Unknown data format %02X\n"), hanBuffer[0]);
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Unknown data format %02X\n"), hanBuffer[0]);
|
||||
break;
|
||||
default:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Unspecified error while reading data: %d\n"), pos);
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Unspecified error while reading data: %d\n"), pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,15 +586,15 @@ void PassiveMeterCommunicator::setupHanPort(uint32_t baud, uint8_t parityOrdinal
|
||||
autodetectBaud = baud = 2400;
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(setupHanPort) Setting up HAN on pin %d/%d with baud %d and parity %d\n"), rxpin, txpin, baud, parityOrdinal);
|
||||
|
||||
if(parityOrdinal == 0) {
|
||||
parityOrdinal = 3; // 8N1
|
||||
parityOrdinal = 11; // 8E1
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(setupHanPort) Setting up HAN on pin %d/%d with baud %d and parity %d\n"), rxpin, txpin, baud, parityOrdinal);
|
||||
|
||||
if(rxpin == 3 || rxpin == 113) {
|
||||
#if ARDUINO_USB_CDC_ON_BOOT
|
||||
hwSerial = &Serial0;
|
||||
@@ -617,9 +617,9 @@ debugger->printf_P(PSTR("(setupHanPort) Setting up HAN on pin %d/%d with baud %d
|
||||
|
||||
if(rxpin == 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Invalid GPIO configured for HAN\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Invalid GPIO configured for HAN\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -628,9 +628,9 @@ debugger->printf_P(PSTR("Invalid GPIO configured for HAN\n"));
|
||||
|
||||
if(hwSerial != NULL) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Hardware serial\n"));
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Hardware serial\n"));
|
||||
Serial.flush();
|
||||
#if defined(ESP8266)
|
||||
SerialConfig serialConfig;
|
||||
@@ -667,15 +667,15 @@ debugger->printf_P(PSTR("Hardware serial\n"));
|
||||
#if defined(ESP8266)
|
||||
if(rxpin == 3) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Switching UART0 to pin 1 & 3\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Switching UART0 to pin 1 & 3\n"));
|
||||
Serial.pins(1,3);
|
||||
} else if(rxpin == 113) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Switching UART0 to pin 15 & 13\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Switching UART0 to pin 15 & 13\n"));
|
||||
Serial.pins(15,13);
|
||||
}
|
||||
#endif
|
||||
@@ -708,9 +708,9 @@ debugger->printf_P(PSTR("Switching UART0 to pin 15 & 13\n"));
|
||||
} else {
|
||||
#if defined(ESP8266)
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Software serial\n"));
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Software serial\n"));
|
||||
Serial.flush();
|
||||
|
||||
if(swSerial == NULL) {
|
||||
@@ -743,17 +743,17 @@ debugger->printf_P(PSTR("Software serial\n"));
|
||||
if(bufferSize > 2) bufferSize = 2;
|
||||
#endif
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Using serial buffer size %d\n"), 64 * bufferSize);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Using serial buffer size %d\n"), 64 * bufferSize);
|
||||
swSerial->begin(baud, serialConfig, rxpin, txpin, invert, meterConfig.bufferSize * 64, meterConfig.bufferSize * 64);
|
||||
hanSerial = swSerial;
|
||||
hwSerial = NULL;
|
||||
#else
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Software serial not available\n"));
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Software serial not available\n"));
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
@@ -767,9 +767,9 @@ debugger->printf_P(PSTR("Software serial not available\n"));
|
||||
// The library automatically sets the pullup in Serial.begin()
|
||||
if(!meterConfig.rxPinPullup) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("HAN pin pullup disabled\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("HAN pin pullup disabled\n"));
|
||||
pinMode(meterConfig.rxPin, INPUT);
|
||||
}
|
||||
|
||||
@@ -799,78 +799,49 @@ HardwareSerial* PassiveMeterCommunicator::getHwSerial() {
|
||||
|
||||
void PassiveMeterCommunicator::rxerr(int err) {
|
||||
if(err == 0) return;
|
||||
if(lastError == 90+err) return; // Do not flood with same error
|
||||
switch(err) {
|
||||
case 2:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial buffer overflow\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#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)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Increasing RX buffer to %d bytes\n"), meterConfig.bufferSize * 64);
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Increasing RX buffer to %d bytes\n"), meterConfig.bufferSize * 64);
|
||||
configChanged = true;
|
||||
rxBufferErrors = 0;
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case 3:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial FIFO overflow\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial FIFO overflow\n"));
|
||||
break;
|
||||
case 4:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial frame error\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial frame error\n"));
|
||||
break;
|
||||
case 5:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial parity error\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial parity error\n"));
|
||||
unsigned long now = millis();
|
||||
if(now - meterAutodetectLastChange < 120000) {
|
||||
switch(autodetectParity) {
|
||||
case 2: // 7N1
|
||||
autodetectParity = 10;
|
||||
break;
|
||||
case 10: // 7E1
|
||||
autodetectParity = 6;
|
||||
break;
|
||||
case 6: // 7N2
|
||||
autodetectParity = 14;
|
||||
break;
|
||||
case 14: // 7E2
|
||||
autodetectParity = 2;
|
||||
break;
|
||||
|
||||
case 3: // 8N1
|
||||
autodetectParity = 11;
|
||||
break;
|
||||
case 11: // 8E1
|
||||
autodetectParity = 7;
|
||||
break;
|
||||
case 7: // 8N2
|
||||
autodetectParity = 15;
|
||||
break;
|
||||
case 15: // 8E2
|
||||
autodetectParity = 3;
|
||||
break;
|
||||
|
||||
default:
|
||||
autodetectParity = 3;
|
||||
break;
|
||||
}
|
||||
if(validDataReceived) {
|
||||
meterConfig.parity = autodetectParity;
|
||||
configChanged = true;
|
||||
setupHanPort(meterConfig.baud, meterConfig.parity, meterConfig.invert);
|
||||
}
|
||||
if(autodetect) {
|
||||
meterAutodetectLastChange = 0;
|
||||
} else if(validDataReceived > 2) {
|
||||
meterConfig.parity = getNextParity(meterConfig.parity);
|
||||
configChanged = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -880,28 +851,35 @@ debugger->printf_P(PSTR("Serial parity error\n"));
|
||||
|
||||
void PassiveMeterCommunicator::handleAutodetect(unsigned long now) {
|
||||
if(!autodetect) return;
|
||||
if(now - meterAutodetectLastChange < 12000) return;
|
||||
|
||||
if(!validDataReceived) {
|
||||
if(now - meterAutodetectLastChange > 20000 && (meterConfig.baud == 0 || meterConfig.parity == 0)) {
|
||||
if(validDataReceived < 2) {
|
||||
if(meterConfig.baud == 0 || meterConfig.parity == 0) {
|
||||
autodetect = true;
|
||||
if(autodetectCount == 2) {
|
||||
if(lastError == 95) { // If parity error, switch parity
|
||||
autodetectParity = getNextParity(autodetectParity);
|
||||
lastError = 0;
|
||||
} else {
|
||||
autodetectCount++;
|
||||
}
|
||||
if(autodetectCount == sizeof(AUTO_BAUD_RATES)/sizeof(AUTO_BAUD_RATES[0])) {
|
||||
autodetectInvert = !autodetectInvert;
|
||||
autodetectCount = 0;
|
||||
}
|
||||
autodetectBaud = AUTO_BAUD_RATES[autodetectCount++];
|
||||
autodetectBaud = AUTO_BAUD_RATES[autodetectCount];
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Meter serial autodetect, swapping to: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Meter serial autodetect, swapping to: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
|
||||
meterConfig.bufferSize = max((uint32_t) 1, autodetectBaud / 14400);
|
||||
setupHanPort(autodetectBaud, autodetectParity, autodetectInvert);
|
||||
meterAutodetectLastChange = now;
|
||||
}
|
||||
} else if(autodetect) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Meter serial autodetected, saving: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Meter serial autodetected, saving: %d, %d, %s\n"), autodetectBaud, autodetectParity, autodetectInvert ? "true" : "false");
|
||||
autodetect = false;
|
||||
meterConfig.baud = autodetectBaud;
|
||||
meterConfig.parity = autodetectParity;
|
||||
@@ -910,3 +888,18 @@ debugger->printf_P(PSTR("Meter serial autodetected, saving: %d, %d, %s\n"), auto
|
||||
setupHanPort(meterConfig.baud, meterConfig.parity, meterConfig.invert);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t PassiveMeterCommunicator::getNextParity(uint8_t parityOrdinal) {
|
||||
switch(parityOrdinal) {
|
||||
case 10: // 7E1
|
||||
return 2; // 7N1
|
||||
case 14: // 7E2
|
||||
return 6; // 7N2
|
||||
|
||||
case 11: // 8E1
|
||||
return 3; // 8N1
|
||||
case 15: // 8E2
|
||||
return 7; // 8N2
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
this->topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#else
|
||||
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
this->topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -15,28 +15,23 @@
|
||||
#define DOCPOS_MEASUREMENTUNIT 2
|
||||
#define DOCPOS_POSITION 3
|
||||
#define DOCPOS_AMOUNT 4
|
||||
#define DOCPOS_RESOLUTION 5
|
||||
|
||||
class EntsoeA44Parser: public Stream {
|
||||
public:
|
||||
EntsoeA44Parser();
|
||||
EntsoeA44Parser(PricesContainer *container);
|
||||
virtual ~EntsoeA44Parser();
|
||||
|
||||
char* getCurrency();
|
||||
char* getMeasurementUnit();
|
||||
float getPoint(uint8_t position);
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
void flush();
|
||||
size_t write(const uint8_t *buffer, size_t size);
|
||||
size_t write(uint8_t);
|
||||
void get(PricesContainer*);
|
||||
|
||||
private:
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
float points[25];
|
||||
PricesContainer *container;
|
||||
float multiplier = 1.0;
|
||||
|
||||
char buf[64];
|
||||
uint8_t pos = 0;
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
|
||||
#define SSL_BUF_SIZE 512
|
||||
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
#define PRICE_DAY_MO 0x01
|
||||
#define PRICE_DAY_TU 0x02
|
||||
#define PRICE_DAY_WE 0x04
|
||||
@@ -57,10 +53,13 @@ struct PriceConfig {
|
||||
uint8_t end_dayofmonth;
|
||||
};
|
||||
|
||||
struct PricePart {
|
||||
char name[32];
|
||||
char description[32];
|
||||
uint32_t value;
|
||||
struct AmsPriceV2Header {
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
char source[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
};
|
||||
|
||||
class PriceService {
|
||||
@@ -71,23 +70,32 @@ public:
|
||||
PriceService(Stream*);
|
||||
#endif
|
||||
void setup(PriceServiceConfig&);
|
||||
void setTimezone(Timezone* tz);
|
||||
bool loop();
|
||||
|
||||
char* getToken();
|
||||
char* getCurrency();
|
||||
char* getArea();
|
||||
char* getSource();
|
||||
float getValueForHour(uint8_t direction, int8_t hour);
|
||||
float getValueForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
|
||||
float getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPointsAvailable();
|
||||
uint8_t getCurrentPricePointIndex();
|
||||
|
||||
bool isExportPricesDifferentFromImport();
|
||||
|
||||
bool hasPrice() { return hasPrice(PRICE_DIRECTION_IMPORT); }
|
||||
bool hasPrice(uint8_t direction) { return getCurrentPrice(direction) != PRICE_NO_VALUE; }
|
||||
bool hasPricePoint(uint8_t direction, int8_t point) { return getPricePoint(direction, point) != PRICE_NO_VALUE; }
|
||||
|
||||
float getCurrentPrice(uint8_t direction);
|
||||
float getPricePoint(uint8_t direction, uint8_t point);
|
||||
float getPriceForRelativeHour(uint8_t direction, int8_t hour); // If not 60min interval, average
|
||||
|
||||
std::vector<PriceConfig>& getPriceConfig();
|
||||
void setPriceConfig(uint8_t index, PriceConfig &priceConfig);
|
||||
void cropPriceConfig(uint8_t size);
|
||||
|
||||
PricePart getPricePart(uint8_t index);
|
||||
|
||||
int16_t getLastError();
|
||||
|
||||
bool load();
|
||||
@@ -102,7 +110,7 @@ private:
|
||||
PriceServiceConfig* config = NULL;
|
||||
HTTPClient* http = NULL;
|
||||
|
||||
uint8_t currentDay = 0, currentHour = 0;
|
||||
uint8_t currentDay = 0, currentPricePoint = 0;
|
||||
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
|
||||
uint8_t nextFetchDelayMinutes = 15;
|
||||
uint64_t lastTodayFetch = 0;
|
||||
@@ -114,9 +122,7 @@ private:
|
||||
std::vector<PriceConfig> priceConfig;
|
||||
|
||||
Timezone* tz = NULL;
|
||||
|
||||
static const uint16_t BufferSize = 256;
|
||||
char* buf;
|
||||
Timezone* entsoeTz = NULL;
|
||||
|
||||
bool hub = false;
|
||||
uint8_t* key = NULL;
|
||||
@@ -129,5 +135,8 @@ private:
|
||||
PricesContainer* fetchPrices(time_t);
|
||||
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
|
||||
|
||||
@@ -4,15 +4,43 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef _PRICESCONTAINER_H
|
||||
#define _PRICESCONTAINER_H
|
||||
|
||||
#define PRICE_NO_VALUE -127
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
struct PricesContainer {
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
int32_t points[25];
|
||||
class PricesContainer {
|
||||
public:
|
||||
PricesContainer(char* source);
|
||||
|
||||
void setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices);
|
||||
|
||||
char* getSource();
|
||||
void setCurrency(char* currency);
|
||||
char* getCurrency();
|
||||
|
||||
bool isExportPricesDifferentFromImport() {
|
||||
return differentExportPrices;
|
||||
}
|
||||
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPoints();
|
||||
|
||||
void setPrice(uint8_t point, float value, uint8_t direction);
|
||||
bool hasPrice(uint8_t point, uint8_t direction);
|
||||
float getPrice(uint8_t point, uint8_t direction); // int32_t / 10_000
|
||||
|
||||
private:
|
||||
char source[4];
|
||||
char currency[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
int32_t *points;
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -7,27 +7,14 @@
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
EntsoeA44Parser::EntsoeA44Parser() {
|
||||
for(int i = 0; i < 25; i++) points[i] = PRICE_NO_VALUE;
|
||||
EntsoeA44Parser::EntsoeA44Parser(PricesContainer *container) {
|
||||
this->container = container;
|
||||
}
|
||||
|
||||
EntsoeA44Parser::~EntsoeA44Parser() {
|
||||
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getMeasurementUnit() {
|
||||
return measurementUnit;
|
||||
}
|
||||
|
||||
float EntsoeA44Parser::getPoint(uint8_t position) {
|
||||
if(position >= 25) return PRICE_NO_VALUE;
|
||||
return points[position];
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::available() {
|
||||
return 0;
|
||||
}
|
||||
@@ -57,7 +44,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(currency, buf, pos);
|
||||
container->setCurrency(buf);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@@ -65,7 +52,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(measurementUnit, buf, pos);
|
||||
if(strcmp_P(buf, PSTR("MWH"))) multiplier = 0.001;
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@@ -73,7 +60,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
long pn = String(buf).toInt() - 1;
|
||||
if(pn < 25) {
|
||||
if(pn < container->getNumberOfPoints()) {
|
||||
pointNum = pn;
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
@@ -85,8 +72,25 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
float val = String(buf).toFloat();
|
||||
for(uint8_t i = pointNum; i < 25; i++) {
|
||||
points[i] = val;
|
||||
for(uint8_t i = pointNum; i < container->getNumberOfPoints(); i++) {
|
||||
container->setPrice(i, val * multiplier, PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(docPos == DOCPOS_RESOLUTION) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
|
||||
// This happens if there are two time series in the XML. We are only interrested in the first one, so we ignore the rest of the document
|
||||
if(container->hasPrice(0, PRICE_DIRECTION_IMPORT)) return 1;
|
||||
|
||||
if(strcmp_P(buf, PSTR("PT15M"))) {
|
||||
container->setup(15, 100, false);
|
||||
} else if(strcmp_P(buf, PSTR("PT60M"))) {
|
||||
container->setup(60, 25, false);
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
@@ -101,15 +105,17 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
buf[pos] = '\0';
|
||||
if(strcmp(buf, "<currency_Unit.name>") == 0) {
|
||||
if(strcmp_P(buf, PSTR("<currency_Unit.name>")) == 0) {
|
||||
docPos = DOCPOS_CURRENCY;
|
||||
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<price_Measure_Unit.name>")) == 0) {
|
||||
docPos = DOCPOS_MEASUREMENTUNIT;
|
||||
} else if(strcmp(buf, "<position>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<position>")) == 0) {
|
||||
docPos = DOCPOS_POSITION;
|
||||
pointNum = 0xFF;
|
||||
} else if(strcmp(buf, "<price.amount>") == 0) {
|
||||
} else if(strcmp(buf, PSTR("<price.amount>")) == 0) {
|
||||
docPos = DOCPOS_AMOUNT;
|
||||
} else if(strcmp(buf, PSTR("<resolution>")) == 0) {
|
||||
docPos = DOCPOS_RESOLUTION;
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
@@ -118,15 +124,3 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void EntsoeA44Parser::get(PricesContainer* container) {
|
||||
memset(container, 0, sizeof(*container));
|
||||
|
||||
strcpy(container->currency, currency);
|
||||
strcpy(container->measurementUnit, measurementUnit);
|
||||
strcpy(container->source, "EOE");
|
||||
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
container->points[i] = points[i] == PRICE_NO_VALUE ? PRICE_NO_VALUE : points[i] * 10000;
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,13 @@ 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
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
tz = new Timezone(CEST, CET);
|
||||
entsoeTz = new Timezone(CEST, CET);
|
||||
tz = entsoeTz;
|
||||
|
||||
tomorrowFetchMinute = 15 + random(45); // Random between 13:15 and 14:00
|
||||
}
|
||||
@@ -42,6 +41,10 @@ void PriceService::setup(PriceServiceConfig& config) {
|
||||
this->config = new PriceServiceConfig();
|
||||
}
|
||||
memcpy(this->config, &config, sizeof(config));
|
||||
if(this->config->resolutionInMinutes != 15 && this->config->resolutionInMinutes != 60) {
|
||||
this->config->resolutionInMinutes = 60;
|
||||
}
|
||||
|
||||
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) delete tomorrow;
|
||||
@@ -72,8 +75,12 @@ void PriceService::setup(PriceServiceConfig& config) {
|
||||
load();
|
||||
}
|
||||
|
||||
void PriceService::setTimezone(Timezone* tz) {
|
||||
this->tz = tz;
|
||||
}
|
||||
|
||||
char* PriceService::getToken() {
|
||||
return this->config->entsoeToken;
|
||||
return ""; // Currently the implementation is not working, so lets disable it for al. Old code: this->config->entsoeToken;
|
||||
}
|
||||
|
||||
char* PriceService::getCurrency() {
|
||||
@@ -86,188 +93,232 @@ char* PriceService::getArea() {
|
||||
|
||||
char* PriceService::getSource() {
|
||||
if(this->today != NULL && this->tomorrow != NULL) {
|
||||
if(strcmp(this->today->source, this->tomorrow->source) == 0) {
|
||||
return this->today->source;
|
||||
if(strcmp(this->today->getSource(), this->tomorrow->getSource()) == 0) {
|
||||
return this->today->getSource();
|
||||
} else {
|
||||
return "MIX";
|
||||
}
|
||||
} else if(today != NULL) {
|
||||
return this->today->source;
|
||||
return this->today->getSource();
|
||||
} else if(tomorrow != NULL) {
|
||||
return this->tomorrow->source;
|
||||
return this->tomorrow->getSource();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
float PriceService::getValueForHour(uint8_t direction, int8_t hour) {
|
||||
time_t cur = time(nullptr);
|
||||
return getValueForHour(direction, cur, hour);
|
||||
uint8_t PriceService::getResolutionInMinutes() {
|
||||
return today != NULL ? today->getResolutionInMinutes() : 60;
|
||||
}
|
||||
|
||||
float PriceService::getValueForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
float ret = getEnergyPriceForHour(direction, ts, hour);
|
||||
if(ret == PRICE_NO_VALUE)
|
||||
return ret;
|
||||
uint8_t PriceService::getNumberOfPointsAvailable() {
|
||||
if(today == NULL) return getResolutionInMinutes() == 15 ? 192 : 48;
|
||||
if(tomorrow != NULL) return today->getNumberOfPoints() + tomorrow->getNumberOfPoints();
|
||||
return today->getNumberOfPoints();
|
||||
}
|
||||
|
||||
bool PriceService::isExportPricesDifferentFromImport() {
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.direction != PRICE_DIRECTION_BOTH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return today != NULL && today->isExportPricesDifferentFromImport();
|
||||
}
|
||||
|
||||
float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
|
||||
float value = getFixedPrice(direction, point);
|
||||
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
|
||||
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
uint8_t day = 0x01 << ((tm.Wday+5)%7);
|
||||
uint32_t hrs = 0x01 << tm.Hour;
|
||||
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);
|
||||
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.type == PRICE_TYPE_FIXED) continue;
|
||||
uint8_t start_month = pc.start_month == 0 || pc.start_month > 12 ? 1 : pc.start_month;
|
||||
uint8_t start_dayofmonth = pc.start_dayofmonth == 0 || pc.start_dayofmonth > 31 ? 1 : pc.start_dayofmonth;
|
||||
uint8_t end_month = pc.end_month == 0 || pc.end_month > 12 ? 12 : pc.end_month;
|
||||
uint8_t end_dayofmonth = pc.end_dayofmonth == 0 || pc.end_dayofmonth > 31 ? 31 : pc.end_dayofmonth;
|
||||
if((pc.direction & direction) != direction) continue;
|
||||
if(!timeIsInPeriod(tm, pc)) continue;
|
||||
float pcVal = pc.value / 10000.0;
|
||||
|
||||
if((pc.direction & direction) == direction && (pc.days & day) == day && (pc.hours & hrs) == hrs && tm.Month >= start_month && tm.Day >= start_dayofmonth && tm.Month <= end_month && tm.Day <= end_dayofmonth) {
|
||||
switch(pc.type) {
|
||||
case PRICE_TYPE_ADD:
|
||||
ret += pc.value / 10000.0;
|
||||
break;
|
||||
case PRICE_TYPE_SUBTRACT:
|
||||
ret -= pc.value / 10000.0;
|
||||
break;
|
||||
case PRICE_TYPE_PCT:
|
||||
ret += ((pc.value / 10000.0) * ret) / 100.0;
|
||||
break;
|
||||
}
|
||||
switch(pc.type) {
|
||||
case PRICE_TYPE_ADD:
|
||||
value += pcVal;
|
||||
break;
|
||||
case PRICE_TYPE_SUBTRACT:
|
||||
value -= pcVal;
|
||||
break;
|
||||
case PRICE_TYPE_PCT:
|
||||
value += (pcVal * value) / 100.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
uint8_t day = 0x01 << ((tm.Wday+5)%7);
|
||||
uint32_t hrs = 0x01 << tm.Hour;
|
||||
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;
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.type != PRICE_TYPE_FIXED) continue;
|
||||
uint8_t start_month = pc.start_month == 0 || pc.start_month > 12 ? 1 : pc.start_month;
|
||||
uint8_t start_dayofmonth = pc.start_dayofmonth == 0 || pc.start_dayofmonth > 31 ? 1 : pc.start_dayofmonth;
|
||||
uint8_t end_month = pc.end_month == 0 || pc.end_month > 12 ? 12 : pc.end_month;
|
||||
uint8_t end_dayofmonth = pc.end_dayofmonth == 0 || pc.end_dayofmonth > 31 ? 31 : pc.end_dayofmonth;
|
||||
|
||||
if((pc.direction & direction) == direction && (pc.days & day) == day && (pc.hours & hrs) == hrs && tm.Month >= start_month && tm.Day >= start_dayofmonth && tm.Month <= end_month && tm.Day <= end_dayofmonth) {
|
||||
if(value == PRICE_NO_VALUE) {
|
||||
value = pc.value / 10000.0;
|
||||
} else {
|
||||
value += pc.value / 10000.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(value != PRICE_NO_VALUE) return value;
|
||||
|
||||
int8_t pos = hour;
|
||||
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
while(tm.Hour > 0) {
|
||||
ts -= 3600;
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
pos++;
|
||||
}
|
||||
uint8_t hoursToday = 0;
|
||||
uint8_t todayDate = tm.Day;
|
||||
while(tm.Day == todayDate) {
|
||||
ts += 3600;
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
hoursToday++;
|
||||
}
|
||||
uint8_t hoursTomorrow = 0;
|
||||
uint8_t tomorrowDate = tm.Day;
|
||||
while(tm.Day == tomorrowDate) {
|
||||
ts += 3600;
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
hoursTomorrow++;
|
||||
}
|
||||
|
||||
float multiplier = 1.0;
|
||||
if(pos >= hoursToday) {
|
||||
pos = pos - hoursToday;
|
||||
if(pos >= hoursTomorrow) return PRICE_NO_VALUE;
|
||||
if(pos >= numberOfPointsToday) {
|
||||
pos = pos - numberOfPointsToday;
|
||||
if(tomorrow == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(tomorrow->points[pos] == PRICE_NO_VALUE)
|
||||
if(pos >= tomorrow->getNumberOfPoints()) return PRICE_NO_VALUE;
|
||||
if(!tomorrow->hasPrice(pos, direction))
|
||||
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));
|
||||
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->points[pos] == PRICE_NO_VALUE)
|
||||
if(!today->hasPrice(pos, direction))
|
||||
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));
|
||||
value = today->getPrice(pos, direction);
|
||||
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
}
|
||||
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
|
||||
}
|
||||
|
||||
float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
|
||||
time_t ts = time(nullptr);
|
||||
tmElements_t tm;
|
||||
|
||||
breakTime(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;
|
||||
|
||||
float value = PRICE_NO_VALUE;
|
||||
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;
|
||||
|
||||
if(value == PRICE_NO_VALUE) {
|
||||
value = pc.value / 10000.0;
|
||||
} else {
|
||||
value += pc.value / 10000.0;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool PriceService::loop() {
|
||||
uint64_t now = millis64();
|
||||
if(now < 10000) return false; // Grace period
|
||||
|
||||
time_t t = time(nullptr);
|
||||
if(t < FirmwareVersion::BuildEpoch) return false;
|
||||
|
||||
#ifndef AMS2MQTT_PRICE_KEY
|
||||
if(strlen(getToken()) == 0) {
|
||||
if(t < FirmwareVersion::BuildEpoch) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if(strlen(config->area) == 0)
|
||||
return false;
|
||||
if(strlen(config->currency) == 0)
|
||||
return false;
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(t), tm);
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
if(currentDay == 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day init\n"));
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
}
|
||||
|
||||
if(currentDay != tm.Day) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day reset\n"));
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) {
|
||||
today = tomorrow;
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
} else if(currentHour != tm.Hour) {
|
||||
currentHour = tm.Hour;
|
||||
} else if(currentPricePoint != getCurrentPricePointIndex()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Price point reset\n"));
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
}
|
||||
|
||||
if(!config->enabled)
|
||||
return false;
|
||||
|
||||
#ifndef AMS2MQTT_PRICE_KEY
|
||||
if(strlen(getToken()) == 0) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if(strlen(config->area) == 0){
|
||||
return false;
|
||||
}
|
||||
if(strlen(config->currency) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
|
||||
|
||||
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > (nextFetchDelayMinutes*60000))) {
|
||||
@@ -281,6 +332,7 @@ bool PriceService::loop() {
|
||||
}
|
||||
today = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL && !readyToFetchForTomorrow; // Only trigger MQTT publish if we have todays prices and we are not immediately ready to fetch price for tomorrow.
|
||||
}
|
||||
|
||||
@@ -297,6 +349,7 @@ bool PriceService::loop() {
|
||||
}
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return tomorrow != NULL;
|
||||
}
|
||||
|
||||
@@ -336,17 +389,17 @@ bool PriceService::retrieve(const char* url, Stream* doc) {
|
||||
nextFetchDelayMinutes = 2;
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Communication error, returned status: %d\n"), status);
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Communication error, returned status: %d\n"), status);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf(http->errorToString(status).c_str());
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf(http->errorToString(status).c_str());
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf(http->getString().c_str());
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf(http->getString().c_str());
|
||||
|
||||
http->end();
|
||||
return false;
|
||||
@@ -375,11 +428,12 @@ float PriceService::getCurrencyMultiplier(const char* from, const char* to, time
|
||||
#endif
|
||||
|
||||
float currencyMultiplier = 0;
|
||||
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
|
||||
char buf[80];
|
||||
snprintf_P(buf, 80, 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, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
|
||||
snprintf_P(buf, 80, 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();
|
||||
@@ -393,18 +447,18 @@ float PriceService::getCurrencyMultiplier(const char* from, const char* to, time
|
||||
}
|
||||
if(currencyMultiplier != 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Resulting currency multiplier: %.4f\n"), currencyMultiplier);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Resulting currency multiplier: %.4f\n"), currencyMultiplier);
|
||||
tmElements_t tm;
|
||||
breakTime(t, tm);
|
||||
lastCurrencyFetch = now + (SECS_PER_DAY * 1000) - (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000) + (3600000 * 6) + (tomorrowFetchMinute * 60);
|
||||
this->currencyMultiplier = currencyMultiplier;
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Multiplier ended in success, but without value\n"));
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Multiplier ended in success, but without value\n"));
|
||||
lastCurrencyFetch = now + (SECS_PER_HOUR * 1000);
|
||||
if(this->currencyMultiplier == 1) return 0;
|
||||
}
|
||||
@@ -415,14 +469,15 @@ debugger->printf_P(PSTR("(PriceService) Multiplier ended in success, but without
|
||||
PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
if(strlen(getToken()) > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(t), tm);
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
time_t e1 = t - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second; // Local midnight
|
||||
time_t e2 = e1 + SECS_PER_DAY;
|
||||
tmElements_t d1, d2;
|
||||
breakTime(e1, d1);
|
||||
breakTime(e2, d2);
|
||||
|
||||
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"),
|
||||
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"),
|
||||
getToken(),
|
||||
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
|
||||
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
|
||||
@@ -435,41 +490,47 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
#endif
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Fetching prices for %02d.%02d.%04d\n"), tm.Day, tm.Month, tm.Year+1970);
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Fetching prices for %02d.%02d.%04d\n"), tm.Day, tm.Month, tm.Year+1970);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
EntsoeA44Parser a44;
|
||||
if(retrieve(buf, &a44) && a44.getPoint(0) != PRICE_NO_VALUE) {
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
a44.get(ret);
|
||||
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)) {
|
||||
return ret;
|
||||
} else {
|
||||
delete ret;
|
||||
return NULL;
|
||||
}
|
||||
} else if(hub) {
|
||||
tmElements_t tm;
|
||||
breakTime(tz->toLocal(t), tm);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Going to fetch prices from hub\n"));
|
||||
|
||||
String data;
|
||||
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
|
||||
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"),
|
||||
config->area,
|
||||
tm.Year+1970,
|
||||
tm.Month,
|
||||
tm.Day,
|
||||
config->resolutionInMinutes,
|
||||
config->currency
|
||||
);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Fetching prices for %02d.%02d.%04d\n"), tm.Day, tm.Month, tm.Year+1970);
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Fetching prices for %02d.%02d.%04d\n"), tm.Day, tm.Month, tm.Year+1970);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -486,23 +547,53 @@ debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
#endif
|
||||
|
||||
if(status == HTTP_CODE_OK) {
|
||||
data = http->getString();
|
||||
http->end();
|
||||
|
||||
uint8_t* content = (uint8_t*) (data.c_str());
|
||||
uint8_t content[1024];
|
||||
|
||||
WiFiClient* stream = http->getStreamPtr();
|
||||
|
||||
DataParserContext ctx = {0,0,0,0};
|
||||
ctx.length = data.length();
|
||||
ctx.length = stream->readBytes(content, http->getSize());
|
||||
http->end();
|
||||
|
||||
GCMParser gcm(key, auth);
|
||||
int8_t gcmRet = gcm.parse(content, ctx);
|
||||
if(gcmRet > 0) {
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = PRICE_NO_VALUE;
|
||||
AmsPriceV2Header* header = (AmsPriceV2Header*) (content+gcmRet);
|
||||
|
||||
PricesContainer* ret = new PricesContainer(header->source);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Setting up price container with pt%dm, %dpts, edi: %d\n"), header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
|
||||
ret->setup(header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
ret->setCurrency(header->currency);
|
||||
int32_t* points = (int32_t*) &header[1];
|
||||
|
||||
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);
|
||||
}
|
||||
memcpy(ret, content+gcmRet, sizeof(*ret));
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = ntohl(ret->points[i]);
|
||||
if(header->differentExportPrices) {
|
||||
for(uint8_t i = 0; i < 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);
|
||||
}
|
||||
}
|
||||
lastError = 0;
|
||||
nextFetchDelayMinutes = 1;
|
||||
@@ -511,9 +602,9 @@ debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
lastError = gcmRet;
|
||||
nextFetchDelayMinutes = 60;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Error code while decrypting prices: %d\n"), gcmRet);
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Error code while decrypting prices: %d\n"), gcmRet);
|
||||
}
|
||||
} else {
|
||||
lastError = status;
|
||||
@@ -525,20 +616,20 @@ debugger->printf_P(PSTR("(PriceService) Error code while decrypting prices: %d\n
|
||||
nextFetchDelayMinutes = 5;
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Communication error, returned status: %d\n"), status);
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Communication error, returned status: %d\n"), status);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
{
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
{
|
||||
debugger->printf(http->errorToString(status).c_str());
|
||||
debugger->println();
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf(http->getString().c_str());
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf(http->getString().c_str());
|
||||
|
||||
http->end();
|
||||
}
|
||||
@@ -575,16 +666,16 @@ void PriceService::cropPriceConfig(uint8_t size) {
|
||||
bool PriceService::save() {
|
||||
if(!LittleFS.begin()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Unable to load LittleFS\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Unable to load LittleFS\n"));
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Saving price config\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Saving price config\n"));
|
||||
|
||||
PriceConfig pc;
|
||||
File file = LittleFS.open(FILE_PRICE_CONF, "w");
|
||||
@@ -607,18 +698,18 @@ debugger->printf_P(PSTR("(PriceService) Saving price config\n"));
|
||||
bool PriceService::load() {
|
||||
if(!LittleFS.begin()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Unable to load LittleFS\n"));
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Unable to load LittleFS\n"));
|
||||
return false;
|
||||
}
|
||||
if(!LittleFS.exists(FILE_PRICE_CONF)) {
|
||||
return false;
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Loading price config\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Loading price config\n"));
|
||||
|
||||
this->priceConfig.clear();
|
||||
|
||||
@@ -632,4 +723,45 @@ debugger->printf_P(PSTR("(PriceService) Loading price config\n"));
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool PriceService::timeIsInPeriod(tmElements_t tm, PriceConfig pc) {
|
||||
uint8_t day = 0x01 << ((tm.Wday+5)%7);
|
||||
uint32_t hrs = 0x01 << tm.Hour;
|
||||
|
||||
if((pc.days & day) != day) return false;
|
||||
if((pc.hours & hrs) != hrs) return false;
|
||||
|
||||
tmElements_t tms;
|
||||
tms.Year = tm.Year;
|
||||
tms.Month = pc.start_month == 0 || pc.start_month > 12 ? 1 : pc.start_month;
|
||||
tms.Day = pc.start_dayofmonth == 0 || pc.start_dayofmonth > 31 ? 1 : pc.start_dayofmonth;
|
||||
tms.Hour = 0;
|
||||
tms.Minute = 0;
|
||||
tms.Second = 0;
|
||||
|
||||
tmElements_t tme;
|
||||
tme.Year = tm.Year;
|
||||
tme.Month = pc.end_month == 0 || pc.end_month > 12 ? 12 : pc.end_month;
|
||||
tme.Day = pc.end_dayofmonth == 0 || pc.end_dayofmonth > 31 ? 31 : pc.end_dayofmonth;
|
||||
tme.Hour = 23;
|
||||
tme.Minute = 59;
|
||||
tme.Second = 59;
|
||||
if(makeTime(tms) > makeTime(tme)) {
|
||||
if(makeTime(tm) <= makeTime(tme)) {
|
||||
tms.Year--;
|
||||
} else {
|
||||
tme.Year++;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
67
lib/PriceService/src/PricesContainer.cpp
Normal file
67
lib/PriceService/src/PricesContainer.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "PricesContainer.h"
|
||||
#include <cstring>
|
||||
|
||||
PricesContainer::PricesContainer(char* source) {
|
||||
strncpy(this->source, source, 4);
|
||||
}
|
||||
|
||||
void PricesContainer::setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices) {
|
||||
this->resolutionInMinutes = resolutionInMinutes;
|
||||
this->differentExportPrices = differentExportPrices;
|
||||
this->numberOfPoints = numberOfPoints;
|
||||
this->points = new int32_t[numberOfPoints * (differentExportPrices ? 2 : 1)];
|
||||
memset(this->points, PRICE_NO_VALUE * 10000, numberOfPoints * (differentExportPrices ? 2 : 1) * sizeof(int32_t));
|
||||
}
|
||||
|
||||
char* PricesContainer::getSource() {
|
||||
return this->source;
|
||||
}
|
||||
|
||||
void PricesContainer::setCurrency(char* currency) {
|
||||
strncpy(this->currency, currency, 4);
|
||||
}
|
||||
|
||||
char* PricesContainer::getCurrency() {
|
||||
return this->currency;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getResolutionInMinutes() {
|
||||
return this->resolutionInMinutes;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getNumberOfPoints() {
|
||||
return this->numberOfPoints;
|
||||
}
|
||||
|
||||
void PricesContainer::setPrice(uint8_t point, float value, uint8_t direction) {
|
||||
if(direction == PRICE_DIRECTION_EXPORT && !differentExportPrices) {
|
||||
return; // Export prices not supported
|
||||
}
|
||||
if(direction != PRICE_DIRECTION_EXPORT) {
|
||||
points[point] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
if(differentExportPrices && direction != PRICE_DIRECTION_IMPORT) {
|
||||
points[point + numberOfPoints] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
bool PricesContainer::hasPrice(uint8_t point, uint8_t direction) {
|
||||
float val = getPrice(point, direction);
|
||||
return val != PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
float PricesContainer::getPrice(uint8_t point, uint8_t direction) {
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_EXPORT) {
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point + numberOfPoints]) / 10000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_BOTH) return PRICE_NO_VALUE; // Can't get a price for both directions if the export prices are different
|
||||
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point]) / 10000.0f;
|
||||
}
|
||||
|
||||
return PRICE_NO_VALUE; // Invalid point
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#else
|
||||
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
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(String data);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
|
||||
@@ -36,6 +36,7 @@ private:
|
||||
bool full;
|
||||
String topic;
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
bool hasExport = false;
|
||||
|
||||
bool publishList1(AmsData* data, AmsData* meterState);
|
||||
bool publishList2(AmsData* data, AmsData* meterState);
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
#include "RawMqttHandler.h"
|
||||
#include "hexutils.h"
|
||||
#include "Uptime.h"
|
||||
#include "FirmwareVersion.h"
|
||||
|
||||
bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
if(topic.isEmpty() || !connected())
|
||||
return false;
|
||||
|
||||
AmsData data;
|
||||
@@ -40,6 +41,15 @@ bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcco
|
||||
publishList1(&data, previousState);
|
||||
loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(ea->isInitialized()) {
|
||||
publishRealtime(ea);
|
||||
loop();
|
||||
@@ -227,9 +237,9 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
if(topic.isEmpty() || !connected())
|
||||
return false;
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
if(!ps->hasPrice())
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@@ -240,7 +250,7 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[34];
|
||||
for(int i = 0;i < 34; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 34; i++) {
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
values[i] = val;
|
||||
|
||||
if(i > 23) continue;
|
||||
@@ -307,15 +317,33 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 34; i++) {
|
||||
float val = values[i];
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/" + String(i), "", true, 0);
|
||||
mqtt.publish(topic + "/price/resolution", String(ps->getResolutionInMinutes()), true, 0);
|
||||
mqtt.loop();
|
||||
|
||||
uint8_t relativeIndex = 0;
|
||||
uint8_t startIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = startIndex; i < numberOfPoints; i++) {
|
||||
float importVal = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(importVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/" + String(i), String(val, 4), true, 0);
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), String(importVal, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
float exportVal = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(exportVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), String(exportVal, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
}
|
||||
relativeIndex++;
|
||||
}
|
||||
if(min != INT16_MAX) {
|
||||
mqtt.publish(topic + "/price/min", String(min, 4), true, 0);
|
||||
@@ -337,20 +365,11 @@ 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() || !mqtt.connected())
|
||||
if(topic.isEmpty() || !connected())
|
||||
return false;
|
||||
|
||||
mqtt.publish(topic + "/id", WiFi.macAddress(), true, 0);
|
||||
@@ -377,9 +396,24 @@ uint8_t RawMqttHandler::getFormat() {
|
||||
return full ? 3 : 2;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
void RawMqttHandler::onMessage(String &topic, String &payload) {
|
||||
if(topic.equals(subTopic)) {
|
||||
if(payload.equals("fwupgrade")) {
|
||||
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
|
||||
updater->setTargetVersion(updater->getNextVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
lib/SvelteUi/app/README.md
Normal file
59
lib/SvelteUi/app/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 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
|
||||
2
lib/SvelteUi/app/dist/index.css
vendored
2
lib/SvelteUi/app/dist/index.css
vendored
File diff suppressed because one or more lines are too long
3
lib/SvelteUi/app/dist/index.html
vendored
3
lib/SvelteUi/app/dist/index.html
vendored
@@ -8,10 +8,9 @@
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
<title>AMS reader</title>
|
||||
<script type="module" crossorigin src="/index.js"></script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
<link rel="stylesheet" crossorigin href="/index.css">
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-900">
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
10
lib/SvelteUi/app/dist/index.js
vendored
10
lib/SvelteUi/app/dist/index.js
vendored
File diff suppressed because one or more lines are too long
2952
lib/SvelteUi/app/package-lock.json
generated
2952
lib/SvelteUi/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,24 +9,19 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"overrides": {
|
||||
"svelte-navigator": {
|
||||
"svelte": ">=4.x"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte": "^5.17.0",
|
||||
"svelte-spa-router": "^4.0.1",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-qrcode": "^1.0.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vite": "^4.5.9"
|
||||
"vite": "^6.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssnano": "^5.1.15",
|
||||
|
||||
19
lib/SvelteUi/app/public/favicon.svg
Normal file
19
lib/SvelteUi/app/public/favicon.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1,44 +1,27 @@
|
||||
<script>
|
||||
import { Router, Route, navigate } from "svelte-navigator";
|
||||
import { getTariff, tariffStore, sysinfoStore, dataStore, pricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js';
|
||||
import Router from "svelte-spa-router";
|
||||
import { push } from "svelte-spa-router";
|
||||
import { getTariff, sysinfoStore, dataStore, 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 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 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 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;
|
||||
@@ -51,11 +34,11 @@
|
||||
sysinfoStore.subscribe(update => {
|
||||
sysinfo = update;
|
||||
if(sysinfo.vndcfg === false) {
|
||||
navigate(basepath + "vendor");
|
||||
push("/vendor");
|
||||
} else if(sysinfo.usrcfg === false) {
|
||||
navigate(basepath + "setup");
|
||||
push("/setup");
|
||||
} else if(sysinfo.fwconsent === 0) {
|
||||
navigate(basepath + "consent");
|
||||
push("/consent");
|
||||
}
|
||||
|
||||
if(sysinfo.ui.k === 1) {
|
||||
@@ -89,53 +72,26 @@
|
||||
updateRealtime(update);
|
||||
});
|
||||
|
||||
let tariffData = {};
|
||||
tariffStore.subscribe(update => {
|
||||
tariffData = update;
|
||||
});
|
||||
getTariff();
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto m-3">
|
||||
<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>
|
||||
<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,
|
||||
}} />
|
||||
|
||||
{#if sysinfo.booting}
|
||||
{#if sysinfo.trying}
|
||||
|
||||
@@ -188,3 +188,22 @@ svg {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -9px;
|
||||
border-width: 9px;
|
||||
border-style: solid;
|
||||
border-color: #ddd transparent transparent transparent;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { Link } from "svelte-navigator";
|
||||
import { tooltip } from './tooltip';
|
||||
|
||||
export let config;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{#if config.link}
|
||||
<div class="text-xs text-right">
|
||||
{#if config.link.route}
|
||||
<Link to={config.link.url}>{config.link.text}</Link>
|
||||
<a href={"#" + config.link.url}>{config.link.text}</a>
|
||||
{: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' ? '85%' : ''}>{tick.label}</text>
|
||||
<text y="-4" x={tick.align == 'right' ? '90%' : ''}>{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}
|
||||
{#if barWidth > 20 || i%2 == 0 || !config.x.ticks[i-1].label}
|
||||
<text x="{barWidth/2}" y="-4">{point.label}</text>
|
||||
{/if}
|
||||
</g>
|
||||
@@ -83,28 +83,25 @@
|
||||
<g class='bars'>
|
||||
{#each config.points as point, i}
|
||||
{#if !isNaN(xScale(i)) && !isNaN(yScale(point.value))}
|
||||
<g>
|
||||
<g data-title="{point.title}" use:tooltip>
|
||||
{#if point.value !== undefined}
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(point.value)}"
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
|
||||
fill="{point.color}"
|
||||
/>
|
||||
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{barWidth < vertSwitch || point.labelAngle ? 'left' : 'middle'}"
|
||||
fill="{yScale(point.value) > yScale(0)-labelOffset && !config.dark ? point.color : 'white'}"
|
||||
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>
|
||||
@@ -113,13 +110,13 @@
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(0)}"
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
|
||||
fill="{point.color2 ? point.color2 : point.color}"
|
||||
/>
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth - 4}"
|
||||
width="{barWidth * 0.95}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{'middle'}"
|
||||
fill="{yScale(-point.value2) < yScale(0) + 15 && !config.dark ? point.color2 ? point.color2 : point.color : 'white'}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { translationsStore } from './TranslationService';
|
||||
import { navigate } from 'svelte-navigator';
|
||||
import { push } from 'svelte-spa-router';
|
||||
import Mask from './Mask.svelte'
|
||||
|
||||
export let prefix;
|
||||
@@ -59,7 +59,7 @@
|
||||
let res = (await response.json())
|
||||
|
||||
saving = false;
|
||||
navigate(basepath);
|
||||
push(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.01" class="in-txt w-full text-right"/>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" 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.01" class="in-txt w-full text-right"/>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
|
||||
<span class="in-post">kWh</span>
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const dataStore = readable(data, (set) => {
|
||||
lastTemp = data.t;
|
||||
setTimeout(getTemperatures, 2000);
|
||||
}
|
||||
if(lastPrice == null && data.pe && data.p != null) {
|
||||
if(data.pe && data.p != lastPrice) {
|
||||
lastPrice = data.p;
|
||||
getPrices();
|
||||
}
|
||||
@@ -109,41 +109,31 @@ export const dataStore = readable(data, (set) => {
|
||||
}
|
||||
});
|
||||
|
||||
let prices = {};
|
||||
let priceShiftTimeout;
|
||||
export const pricesStore = writable(prices);
|
||||
|
||||
export async function shiftPrices() {
|
||||
let fetchUpdate = false;
|
||||
pricesStore.update(p => {
|
||||
for(var i = 0; i < 36; i++) {
|
||||
if(p[zeropad(i)] == null) {
|
||||
fetchUpdate = i < 12;
|
||||
break;
|
||||
}
|
||||
p[zeropad(i)] = p[zeropad(i+1)];
|
||||
}
|
||||
return p;
|
||||
});
|
||||
if(fetchUpdate) {
|
||||
getPrices();
|
||||
} else {
|
||||
let date = new Date();
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
}
|
||||
}
|
||||
let priceFetchTimeout;
|
||||
let importPrices = {};
|
||||
export const importPricesStore = writable(importPrices);
|
||||
let exportprices = {};
|
||||
export const exportPricesStore = writable(exportprices);
|
||||
|
||||
export async function getPrices() {
|
||||
if(priceShiftTimeout) {
|
||||
clearTimeout(priceShiftTimeout);
|
||||
priceShiftTimeout = 0;
|
||||
if(priceFetchTimeout) {
|
||||
clearTimeout(priceFetchTimeout);
|
||||
priceFetchTimeout = 0;
|
||||
}
|
||||
{
|
||||
const response = await fetchWithTimeout("importprice.json");
|
||||
importPrices = (await response.json())
|
||||
importPricesStore.set(importPrices);
|
||||
}
|
||||
|
||||
if(importPrices?.importExportPriceDifferent) {
|
||||
const response = await fetchWithTimeout("exportprice.json");
|
||||
exportprices = (await response.json())
|
||||
exportPricesStore.set(exportprices);
|
||||
}
|
||||
const response = await fetchWithTimeout("energyprice.json");
|
||||
prices = (await response.json())
|
||||
pricesStore.set(prices);
|
||||
|
||||
let date = new Date();
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
priceFetchTimeout = setTimeout(getPrices, ((24-date.getHours())*3600000)+10)
|
||||
}
|
||||
|
||||
let dayPlot = {};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import { Link } from "svelte-navigator";
|
||||
import { sysinfoStore } from './DataStores.js';
|
||||
import { upgrade, upgradeWarningText } from './UpgradeHelper';
|
||||
import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js';
|
||||
@@ -46,7 +45,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">
|
||||
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
|
||||
<a href={basepath}>AMS reader <span>{sysinfo.version}</span></a>
|
||||
</div>
|
||||
<div class="flex-none my-auto p-2 flex space-x-4">
|
||||
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
|
||||
@@ -79,14 +78,14 @@
|
||||
</div>
|
||||
{#if sysinfo.vndcfg && sysinfo.usrcfg}
|
||||
<div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}>
|
||||
<Link to="/configuration"><GearIcon/></Link>
|
||||
<a href="#/configuration"><GearIcon/></a>
|
||||
</div>
|
||||
<div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}>
|
||||
<Link to="/status"><InfoIcon/></Link>
|
||||
<a href="#/status"><InfoIcon/></a>
|
||||
</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>
|
||||
|
||||
@@ -130,7 +130,11 @@ export function uiVisibility(choice, state) {
|
||||
}
|
||||
|
||||
export function wiki(page) {
|
||||
return "https://github.com/UtilitechAS/amsreader-firmware/wiki/" + page;
|
||||
let ret = "https://wiki.amsleser.no";
|
||||
if(page) {
|
||||
ret += "/en/firmware#" + page;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function fmtnum(v,d) {
|
||||
@@ -145,6 +149,11 @@ export function addHours(date, hours) {
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMinutes(date, minutes) {
|
||||
date.setTime(date.getTime() + minutes * 60000);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getPriceSourceName(code) {
|
||||
if(code == "EOE") return "ENTSO-E";
|
||||
if(code == "HKS") return "hvakosterstrommen.no";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { zeropad, addHours, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import { zeropad, addHours, addMinutes, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import BarChart from './BarChart.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let title;
|
||||
export let json;
|
||||
@@ -12,113 +13,124 @@
|
||||
|
||||
let dark = document.documentElement.classList.contains('dark');
|
||||
|
||||
let cur = new Date();
|
||||
|
||||
onMount(() => {
|
||||
let timeout;
|
||||
function scheduleUpdate() {
|
||||
cur = new Date();
|
||||
timeout = setTimeout(() => {
|
||||
scheduleUpdate();
|
||||
}, (15 - (cur.getMinutes() % 15)) * 60000);
|
||||
}
|
||||
|
||||
scheduleUpdate();
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
});
|
||||
|
||||
$: {
|
||||
let currency = json.currency;
|
||||
let hour = new Date().getUTCHours();
|
||||
let i = 0;
|
||||
let val = 0;
|
||||
let h = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
let cur = new Date();
|
||||
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
|
||||
for(i = hour; i<24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
for(i = 0; i < 24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
if(json?.prices?.length > 0) {
|
||||
cur = new Date();
|
||||
let currency = json?.currency;
|
||||
let val = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
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 points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
}
|
||||
}
|
||||
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
let points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<BarChart config={config} />
|
||||
{#if config.points && config.points.length > 0}
|
||||
<BarChart config={config} />
|
||||
{/if}
|
||||
@@ -41,12 +41,6 @@
|
||||
min = 0;
|
||||
max = 0;
|
||||
|
||||
/*
|
||||
console.log("\n--Realtime plot debug--")
|
||||
console.log("Data length: %d\nSize: %d", realtime?.data?.length, realtime?.size);
|
||||
console.log("Height: %d\nWidth: %d\nBar width: %s", heightAvailable, widthAvailable, barWidth);
|
||||
*/
|
||||
|
||||
if(realtime.data && heightAvailable > 10 && widthAvailable > 100 && barWidth > 0.1) {
|
||||
visible = true;
|
||||
for(let p in realtime.data) {
|
||||
@@ -90,9 +84,6 @@
|
||||
} else {
|
||||
visible = false;
|
||||
}
|
||||
/*
|
||||
console.log("Min: %d\nMax: %d\nShow: %s", min, max, visible);
|
||||
*/
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { zeropad } from './Helpers.js';
|
||||
import { ampcol, zeropad } from './Helpers.js';
|
||||
import BarChart from './BarChart.svelte';
|
||||
|
||||
export let title;
|
||||
@@ -12,6 +12,7 @@
|
||||
let min = 0;
|
||||
|
||||
export let tariffData;
|
||||
export let realtime;
|
||||
|
||||
$: {
|
||||
let i = 0;
|
||||
@@ -24,16 +25,42 @@
|
||||
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: peak.d > 0 ? zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1)) : "-"
|
||||
label: daylabel
|
||||
})
|
||||
max = Math.max(max, peak.v);
|
||||
}
|
||||
@@ -71,7 +98,7 @@
|
||||
config = {
|
||||
title: title,
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 35, bottom: 20, left: 35 },
|
||||
padding: { top: 20, right: 20, bottom: 20, left: 20 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
|
||||
14
lib/SvelteUi/app/src/lib/Tooltip.svelte
Normal file
14
lib/SvelteUi/app/src/lib/Tooltip.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
export let title;
|
||||
export let x;
|
||||
export let y;
|
||||
|
||||
let width;
|
||||
let height;
|
||||
</script>
|
||||
<div
|
||||
class="tooltip"
|
||||
style="top: {y - height - 10}px; left: {x - (width / 2)}px;"
|
||||
bind:clientHeight={height}
|
||||
bind:clientWidth={width}
|
||||
>{title}</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
case 'esp8266': gpioMax = 16; break;
|
||||
case 'esp32s2': gpioMax = 44; break;
|
||||
case 'esp32s3': gpioMax = 46; break;
|
||||
case 'esp32c3': gpioMax = 19; break;
|
||||
case 'esp32c3': gpioMax = 21; break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -23,4 +23,3 @@
|
||||
<option value={i}>GPIO{i}</option>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
|
||||
41
lib/SvelteUi/app/src/lib/tooltip.js
Normal file
41
lib/SvelteUi/app/src/lib/tooltip.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
|
||||
export function tooltip(element) {
|
||||
let title;
|
||||
let tooltipComponent;
|
||||
|
||||
function click(event) {
|
||||
if(tooltipComponent) tooltipComponent.$destroy();
|
||||
|
||||
title = element.dataset.title || element.getAttribute('title');
|
||||
var rect = element.getBoundingClientRect();
|
||||
|
||||
tooltipComponent = new Tooltip({
|
||||
props: {
|
||||
title: title,
|
||||
x: rect.left + window.scrollX + (rect.width / 2),
|
||||
y: rect.top + window.scrollY,
|
||||
},
|
||||
target: document.body,
|
||||
});
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
if(tooltipComponent) {
|
||||
setTimeout(() => {
|
||||
tooltipComponent.$destroy();
|
||||
tooltipComponent = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener('click', click);
|
||||
element.addEventListener('mouseleave', mouseLeave);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
element.removeEventListener('click', click);
|
||||
element.removeEventListener('mouseleave', mouseLeave);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import "./app.postcss";
|
||||
import { mount } from "svelte";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = new App({
|
||||
const app = mount(App, {
|
||||
target: document.getElementById("app"),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script>
|
||||
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 { 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 QrCode from 'svelte-qrcode';
|
||||
|
||||
export let basepath = "/";
|
||||
export let sysinfo = {};
|
||||
export let data;
|
||||
let basepath = "/";
|
||||
let sysinfo = {};
|
||||
let data;
|
||||
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
dataStore.subscribe(v => data = v);
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
@@ -150,7 +153,7 @@
|
||||
});
|
||||
|
||||
saving = false;
|
||||
navigate(basepath);
|
||||
push(basepath);
|
||||
}
|
||||
|
||||
async function reboot() {
|
||||
@@ -225,6 +228,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function enablePriceFetch() {
|
||||
configuration.p.e = true;
|
||||
}
|
||||
|
||||
let gpioMax = 44;
|
||||
$: {
|
||||
gpioMax = sysinfo.chip == 'esp8266' ? 16 : sysinfo.chip == 'esp32s2' ? 44 : 39;
|
||||
@@ -251,7 +258,7 @@
|
||||
{#if configuration?.g}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.general?.title ?? "General"}</strong>
|
||||
<a href="{wiki('General-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('general')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="g" value="true"/>
|
||||
<div class="my-1">
|
||||
<div class="flex">
|
||||
@@ -272,14 +279,14 @@
|
||||
<div class="flex">
|
||||
<div class="w-full">
|
||||
{translations.conf?.price?.region ?? "Price region"}<br/>
|
||||
<select name="pr" bind:value={configuration.p.r} class="in-f w-full">
|
||||
<select name="pr" bind:value={configuration.p.r} on:change={enablePriceFetch} class="in-f w-full">
|
||||
<optgroup label="Norway">
|
||||
{#if !configuration.p.t}
|
||||
<option value="NO1S">NO1 with support</option>
|
||||
<option value="NO2S">NO2 with support</option>
|
||||
<option value="NO3S">NO3 with support</option>
|
||||
<option value="NO4S">NO4 with support</option>
|
||||
<option value="NO5S">NO5 with support</option>
|
||||
<option value="NO1S">NO1 w/support</option>
|
||||
<option value="NO2S">NO2 w/support</option>
|
||||
<option value="NO3S">NO3 w/support</option>
|
||||
<option value="NO4S">NO4 w/support</option>
|
||||
<option value="NO5S">NO5 w/support</option>
|
||||
{/if}
|
||||
<option value="10YNO-1--------2">NO1</option>
|
||||
<option value="10YNO-2--------T">NO2</option>
|
||||
@@ -313,6 +320,14 @@
|
||||
<option value="10YCH-SWISSGRIDZ">Switzerland</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.resolution ?? "Resolution"}<br/>
|
||||
<select name="pm" bind:value={configuration.p.m} class="in-m">
|
||||
{#each [15,60] as m}
|
||||
<option value={m}>{m}M</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.currency ?? "Currency"}<br/>
|
||||
<select name="pc" bind:value={configuration.p.c} class="in-l">
|
||||
@@ -324,12 +339,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
<Link to="/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</Link>
|
||||
<a href="#/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</a>
|
||||
</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>
|
||||
{#if configuration.p.e && sysinfo.chip != 'esp8266'}
|
||||
<br/><input name="pt" bind:value={configuration.p.t} type="text" class="in-s" placeholder={translations.conf?.price?.api_key_placeholder ?? ""} pattern={charAndNumPattern}/>
|
||||
{#if configuration.p.e && sysinfo.chip != 'esp8266' && configuration.p.t}
|
||||
<input name="pt" type="hidden" bind:value={configuration.p.t}/>
|
||||
<br/><input type="text" class="in-s" placeholder="ENTSO-E API key disabled, ref issue #1030" disabled/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="my-1">
|
||||
@@ -359,7 +375,7 @@
|
||||
{#if configuration?.m}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.meter?.title ?? "Meter"}</strong>
|
||||
<a href="{wiki('Meter-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('meter')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="m" value="true"/>
|
||||
<input type="hidden" name="mo" value="1"/>
|
||||
<div class="my-1">
|
||||
@@ -466,7 +482,7 @@
|
||||
{#if configuration?.w}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.connection?.title ?? "Connection"}</strong>
|
||||
<a href="{wiki('Network-connection')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('connection')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="w" value="true"/>
|
||||
<div class="my-1">
|
||||
<select name="nc" class="in-s" bind:value={configuration.n.c}>
|
||||
@@ -528,7 +544,7 @@
|
||||
{#if configuration?.n}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.network?.title ?? "Network"}</strong>
|
||||
<a href="{wiki('Network-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('network')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<div class="my-1">
|
||||
{translations.conf?.network?.ip ?? "IP"}<br/>
|
||||
<div class="flex">
|
||||
@@ -573,7 +589,7 @@
|
||||
{#if configuration?.q}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.mqtt?.title ?? "MQTT"}</strong>
|
||||
<a href="{wiki('MQTT-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('mqtt')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="q" value="true"/>
|
||||
<div class="my-1">
|
||||
{translations.conf?.mqtt?.server ?? "Server"}
|
||||
@@ -590,28 +606,28 @@
|
||||
<div class="my-1 flex">
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.c}
|
||||
<span class="bd-on"><Link to="/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</Link></span>
|
||||
<span class="bd-on"><a href="#/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</a></span>
|
||||
<span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>🗑</span>
|
||||
{:else}
|
||||
<Link to="/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></Link>
|
||||
<a href="#/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></a>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.r}
|
||||
<span class="bd-on"><Link to="/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</Link></span>
|
||||
<span class="bd-on"><a href="#/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</a></span>
|
||||
<span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>🗑</span>
|
||||
{:else}
|
||||
<Link to="/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></Link>
|
||||
<a href="#/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></a>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.k}
|
||||
<span class="bd-on"><Link to="/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</Link></span>
|
||||
<span class="bd-on"><a href="#/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</a></span>
|
||||
<span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>🗑</span>
|
||||
{:else}
|
||||
<Link to="/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></Link>
|
||||
<a href="#/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></a>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -663,11 +679,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
{translations.conf?.mqtt?.timeout ?? "Timeout"}
|
||||
<span class="float-right">{translations.conf?.mqtt?.keepalive ?? "Keep-alive"}</span>
|
||||
<div class="grid grid-cols-3">
|
||||
<p>{translations.conf?.mqtt?.timeout ?? "Timeout"}</p>
|
||||
<p>{translations.conf?.mqtt?.keepalive ?? "Keep-alive"}</p>
|
||||
<p>{translations.conf?.mqtt?.autoreboot ?? "Auto-reboot"}</p>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<input name="qi" bind:value={configuration.q.i} type="number" min="500" max="10000" class="in-f tr w-1/2"/>
|
||||
<input name="qk" bind:value={configuration.q.k} type="number" min="5" max="180" class="in-l tr w-1/2"/>
|
||||
<input name="qk" bind:value={configuration.q.k} type="number" min="5" max="180" class="in-m tr w-1/2"/>
|
||||
<input name="qe" bind:value={configuration.q.e} type="number" min="0" max="240" class="in-l tr w-1/2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -719,7 +739,7 @@
|
||||
{#if configuration?.c}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.cloud?.title ?? "Cloud connections"}</strong>
|
||||
<a href="{wiki('Cloud')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('cloud-connections')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="c" value="true"/>
|
||||
{#if sysinfo?.features?.includes('cloud')}
|
||||
<div class="my-1">
|
||||
@@ -753,12 +773,22 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{#if sysinfo?.features?.includes('zc')}
|
||||
<div class="my-1">
|
||||
<label><input type="checkbox" name="cze" value="true" bind:checked={configuration.c.ze} class="rounded mb-1"/> ZmartCharge</label>
|
||||
</div>
|
||||
{#if configuration.c.ze}
|
||||
<div class="my-1">
|
||||
<input name="czt" bind:value={configuration.c.zt} type="text" class="in-s" placeholder="ZmartCharge token"/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#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('Threshold-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('tariff-thresholds')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="t" value="true"/>
|
||||
<div class="flex flex-wrap my-1">
|
||||
{#each {length: 9} as _, i}
|
||||
@@ -779,7 +809,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">ⓘ</a>
|
||||
<a href="{wiki('user-interface')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="u" value="true"/>
|
||||
<div class="flex flex-wrap">
|
||||
{#each uiElements as el}
|
||||
@@ -806,7 +836,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('GPIO-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('hardware')}" target="_blank" class="float-right">ⓘ</a>
|
||||
{#if sysinfo.board > 20}
|
||||
<input type="hidden" name="i" value="true"/>
|
||||
<div class="flex flex-wrap">
|
||||
@@ -888,6 +918,13 @@
|
||||
</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">
|
||||
@@ -1,18 +1,19 @@
|
||||
<script>
|
||||
import { sysinfoStore } from './DataStores.js';
|
||||
import { translationsStore } from './TranslationService.js';
|
||||
import Mask from './Mask.svelte'
|
||||
import { navigate } from 'svelte-navigator';
|
||||
import { wiki } from './Helpers';
|
||||
import { sysinfoStore } from '../lib/DataStores.js';
|
||||
import { translationsStore } from '../lib/TranslationService.js';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import { push } from 'svelte-spa-router';
|
||||
|
||||
export let basepath = "/";
|
||||
export let sysinfo = {};
|
||||
let basepath = "/";
|
||||
let sysinfo = {};
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
});
|
||||
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
|
||||
let loadingOrSaving = false;
|
||||
|
||||
async function handleSubmit(e) {
|
||||
@@ -36,7 +37,7 @@
|
||||
s.booting = res.reboot;
|
||||
return s;
|
||||
});
|
||||
navigate(basepath);
|
||||
push(basepath);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -49,7 +50,7 @@
|
||||
<hr/>
|
||||
<div class="my-3">
|
||||
{translations.consent?.one_click ?? "One-click"}<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/>
|
||||
<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/>
|
||||
<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>
|
||||
@@ -1,25 +1,38 @@
|
||||
<script>
|
||||
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';
|
||||
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';
|
||||
|
||||
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 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);
|
||||
|
||||
let it,et,threePhase, l1e, l2e, l3e;
|
||||
$: {
|
||||
@@ -128,7 +141,7 @@
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("NO") || data.pr.startsWith("10YNO") || data.pr.startsWith('10Y1001A1001A4')))}
|
||||
<div class="cnt h-64">
|
||||
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} translations={translations}/>
|
||||
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} realtime={data.ea} translations={translations}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.l, data.hm == 1)}
|
||||
@@ -136,9 +149,20 @@
|
||||
<RealtimePlot title={translations.dashboard?.realtime ?? "Real time"}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.p && !Number.isNaN(data.p))}
|
||||
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0)}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title="{translations.dashboard?.price_import ?? "Price import"}" json={importPrices} 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))}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title={translations.dashboard?.price ?? "Price"} json={prices} sysinfo={sysinfo}/>
|
||||
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices} sysinfo={sysinfo}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
|
||||
11
lib/SvelteUi/app/src/routes/EditDayRoute.svelte
Normal file
11
lib/SvelteUi/app/src/routes/EditDayRoute.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<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} />
|
||||
11
lib/SvelteUi/app/src/routes/EditMonthRoute.svelte
Normal file
11
lib/SvelteUi/app/src/routes/EditMonthRoute.svelte
Normal file
@@ -0,0 +1,11 @@
|
||||
<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} />
|
||||
@@ -1,9 +1,6 @@
|
||||
<script>
|
||||
import Mask from "./Mask.svelte";
|
||||
import { translationsStore } from "./TranslationService";
|
||||
|
||||
export let action;
|
||||
export let title;
|
||||
import Mask from "../lib/Mask.svelte";
|
||||
import { translationsStore } from "../lib/TranslationService";
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
@@ -15,12 +12,12 @@
|
||||
|
||||
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
|
||||
<div class="cnt">
|
||||
<strong>{translations.upload?.title ?? "Upload"} {title}</strong>
|
||||
<strong>{translations.upload?.title ?? "Upload"} CA</strong>
|
||||
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
|
||||
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
|
||||
<form action="/mqtt-ca" 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"><p class="mb-4">{translations.btn?.upload ?? "Upload"}</button>
|
||||
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
25
lib/SvelteUi/app/src/routes/MqttCertRoute.svelte
Normal file
25
lib/SvelteUi/app/src/routes/MqttCertRoute.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<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"}/>
|
||||
25
lib/SvelteUi/app/src/routes/MqttKeyRoute.svelte
Normal file
25
lib/SvelteUi/app/src/routes/MqttKeyRoute.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<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"}/>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user