mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-03-12 05:25:24 +00:00
Compare commits
3 Commits
upgrade/sv
...
v2.4.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ed701fc57 | ||
|
|
473932f1bc | ||
|
|
d7e68ce692 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -27,7 +27,6 @@ jobs:
|
||||
run: |
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
|
||||
- name: Cache Python dependencies
|
||||
@@ -51,7 +50,7 @@ jobs:
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
node-version: '19.x'
|
||||
- name: Build with node
|
||||
run: |
|
||||
cd lib/SvelteUi/app
|
||||
|
||||
2
.github/workflows/localazy.yml
vendored
2
.github/workflows/localazy.yml
vendored
@@ -38,4 +38,4 @@ jobs:
|
||||
run: localazy download -k localazy-keys.json
|
||||
|
||||
- name: Upload translations to S3
|
||||
run: aws s3 sync ./localazy/language/ s3://${{ secrets.AWS_S3_BUCKET }}/language/
|
||||
run: aws s3 sync ./localazy/language/ s3://amscloud-private/language/
|
||||
|
||||
85
.github/workflows/prerelease.yml
vendored
85
.github/workflows/prerelease.yml
vendored
@@ -1,85 +0,0 @@
|
||||
name: Release candidate build and upload
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+'
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
|
||||
- name: Create release with release notes
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release candidate v${{ steps.release_tag.outputs.tag }}
|
||||
prerelease: true
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.release_tag.outputs.tag }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
|
||||
esp32s2:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s2
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
esp32s3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
esp32c3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32c3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
esp32:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
esp32solo:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32solo
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
esp8266:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp8266
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
subfolder: /rc
|
||||
is_esp32: false
|
||||
136
.github/workflows/release-deploy-env.yml
vendored
136
.github/workflows/release-deploy-env.yml
vendored
@@ -1,136 +0,0 @@
|
||||
name: Build with env and deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
env:
|
||||
description: 'The environment to build for'
|
||||
required: true
|
||||
type: string
|
||||
upload_url:
|
||||
description: 'The upload URL for the release assets'
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: 'The version tag for the release assets'
|
||||
required: true
|
||||
type: string
|
||||
subfolder:
|
||||
description: 'The subfolder in S3 to upload the binary to'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
is_esp32:
|
||||
description: 'Whether the build is for ESP32 based firmware'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: eu-north-1
|
||||
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache PlatformIO dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pio/libdeps
|
||||
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Inject secrets into ini file
|
||||
run: |
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio css_html_js_minify
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
- name: Build with node
|
||||
run: |
|
||||
cd lib/SvelteUi/app
|
||||
npm ci
|
||||
npm run build
|
||||
cd -
|
||||
env:
|
||||
CI: false
|
||||
|
||||
- name: PlatformIO lib install
|
||||
run: pio lib install
|
||||
|
||||
- name: Build firmware
|
||||
env:
|
||||
GITHUB_TAG: v${{ inputs.version }}
|
||||
run: pio run -e ${{ inputs.env }}
|
||||
|
||||
- name: Create zip file
|
||||
run: /bin/sh scripts/${{ inputs.env }}/mkzip.sh
|
||||
|
||||
- name: Upload binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ inputs.upload_url }}
|
||||
asset_path: .pio/build/${{ inputs.env }}/firmware.bin
|
||||
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ inputs.upload_url }}
|
||||
asset_path: ${{ inputs.env }}.zip
|
||||
asset_name: ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Create MD5 checksum file
|
||||
run: md5sum .pio/build/${{ inputs.env }}/firmware.bin | cut -z -d ' ' -f 1 > firmware.md5
|
||||
|
||||
- name: Upload binary to S3
|
||||
run: aws s3 cp .pio/build/${{ inputs.env }}/firmware.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.bin
|
||||
|
||||
- name: Upload MD5 checksum to S3
|
||||
run: aws s3 cp firmware.md5 s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}.md5
|
||||
|
||||
- name: Upload bootloader to S3
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
run: aws s3 cp .pio/build/${{ inputs.env }}/bootloader.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-bootloader.bin
|
||||
|
||||
- name: Upload partition table to S3
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
run: aws s3 cp .pio/build/${{ inputs.env }}/partitions.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-partitions.bin
|
||||
|
||||
- name: Upload app0 to S3
|
||||
if: ${{ inputs.is_esp32 }}
|
||||
run: aws s3 cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin s3://${{ secrets.AWS_S3_BUCKET }}/firmware${{ inputs.subfolder }}/ams2mqtt-${{ inputs.env }}-${{ inputs.version }}-app0.bin
|
||||
270
.github/workflows/release.yml
vendored
270
.github/workflows/release.yml
vendored
@@ -1,79 +1,213 @@
|
||||
name: Release build and upload
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v*.*.*'
|
||||
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
- name: Get release version for code
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
|
||||
|
||||
- name: Create release with release notes
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release v${{ steps.release_tag.outputs.tag }}
|
||||
generateReleaseNotes: true
|
||||
- name: Inject secrets into ini file
|
||||
run: |
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_KEY/AMS2MQTT_PRICE_KEY="${{secrets.AMS2MQTT_PRICE_KEY}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_PRICE_AUTHENTICATION/AMS2MQTT_PRICE_AUTHENTICATION="${{secrets.AMS2MQTT_PRICE_AUTHENTICATION}}"/g' platformio.ini
|
||||
sed -i 's/NO_AMS2MQTT_SC_KEY/AMS2MQTT_SC_KEY=\\"${{secrets.AMS2MQTT_SC_KEY}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_USER/ENERGY_SPEEDOMETER_USER=\\"${{secrets.ENERGY_SPEEDOMETER_USER}}\\"/g' platformio.ini
|
||||
sed -i 's/NO_ENERGY_SPEEDOMETER_PASS/ENERGY_SPEEDOMETER_PASS=\\"${{secrets.ENERGY_SPEEDOMETER_PASS}}\\"/g' platformio.ini
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.release_tag.outputs.tag }}
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('platformio.ini') }}
|
||||
- name: Cache PlatformIO dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pio/libdeps
|
||||
key: ${{ runner.os }}-pio-${{ hashFiles('platformio.ini') }}
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio css_html_js_minify
|
||||
|
||||
esp32s2:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s2
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32s3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32s3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32c3:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32c3
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp32solo:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp32solo
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
esp8266:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/release-deploy-env.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
env: esp8266
|
||||
version: ${{ needs.prepare.outputs.version }}
|
||||
upload_url: ${{ needs.prepare.outputs.upload_url }}
|
||||
is_esp32: false
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '19.x'
|
||||
- name: Build with node
|
||||
run: |
|
||||
cd lib/SvelteUi/app
|
||||
npm ci
|
||||
npm run build
|
||||
cd -
|
||||
env:
|
||||
CI: false
|
||||
|
||||
- name: PlatformIO lib install
|
||||
run: pio lib install
|
||||
|
||||
- name: Create release with release notes
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: Release v${{ steps.release_tag.outputs.tag }}
|
||||
generateReleaseNotes: true
|
||||
|
||||
- name: Build esp8266 firmware
|
||||
run: pio run -e esp8266
|
||||
- name: Create esp8266 zip file
|
||||
run: /bin/sh scripts/esp8266/mkzip.sh
|
||||
- name: Upload esp8266 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp8266/firmware.bin
|
||||
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp8266 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp8266.zip
|
||||
asset_name: ams2mqtt-esp8266-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32 firmware
|
||||
run: pio run -e esp32
|
||||
- name: Create esp32 zip file
|
||||
run: /bin/sh scripts/esp32/mkzip.sh
|
||||
- name: Upload esp32 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32/firmware.bin
|
||||
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32.zip
|
||||
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32s2 firmware
|
||||
run: pio run -e esp32s2
|
||||
- name: Create esp32s2 zip file
|
||||
run: /bin/sh scripts/esp32s2/mkzip.sh
|
||||
- name: Upload esp32s2 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32s2/firmware.bin
|
||||
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32s2 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32s2.zip
|
||||
asset_name: ams2mqtt-esp32s2-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32s3 firmware
|
||||
run: pio run -e esp32s3
|
||||
- name: Create esp32s3 zip file
|
||||
run: /bin/sh scripts/esp32s3/mkzip.sh
|
||||
- name: Upload esp32s3 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32s3/firmware.bin
|
||||
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32s3 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32s3.zip
|
||||
asset_name: ams2mqtt-esp32s3-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32solo firmware
|
||||
run: pio run -e esp32solo
|
||||
- name: Create esp32solo zip file
|
||||
run: /bin/sh scripts/esp32solo/mkzip.sh
|
||||
- name: Upload esp32solo binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32solo/firmware.bin
|
||||
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32solo zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32solo.zip
|
||||
asset_name: ams2mqtt-esp32solo-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
- name: Build esp32c3 firmware
|
||||
run: pio run -e esp32c3
|
||||
- name: Create esp32c3 zip file
|
||||
run: /bin/sh scripts/esp32c3/mkzip.sh
|
||||
- name: Upload esp32c3 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: .pio/build/esp32c3/firmware.bin
|
||||
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp32c3 zip to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: esp32c3.zip
|
||||
asset_name: ams2mqtt-esp32c3-${{ steps.release_tag.outputs.tag }}.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
# AMS Reader
|
||||
This code is designed to decode data from electric smart meters installed in many countries in Europe these days. The data is presented in a graphical web interface and can also send the data to a MQTT broker which makes it suitable for home automation project. Originally it was only designed to work with Norwegian meters, but has since been adapter to read any IEC-62056-7-5 or IEC-62056-21 compliant meters.
|
||||
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://www.amsleser.no/).
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price. The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki). If you don't have the knowledge to set up a ESP device yourself, or you would like to support our work, please have a look at our shop at [amsleser.no](https://amsleser.no/).
|
||||
|
||||
|
||||
<img src="images/dashboard.png">
|
||||
|
||||
## Installing pre-built firmware
|
||||
If you have a device already running this firmware and you for some reason need to upgrade via USB port, you can use a [this web-based tool](https://www.amsleser.cloud/flasher)
|
||||
|
||||
If you are using a development board and want to flash a pre-built firmware manually, get the necessary files from the [release](https://github.com/UtilitechAS/amsreader-firmware/releases) section and visit the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) and have a look at the [Flashing](https://github.com/UtilitechAS/amsreader-firmware/wiki/flashinghttps://github.com/UtilitechAS/amsreader-firmware/wiki/flashing) section
|
||||
Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases).
|
||||
|
||||
## Building this project with PlatformIO
|
||||
To build this project, you need [PlatformIO](https://platformio.org/) installed.
|
||||
|
||||
Binary file not shown.
@@ -1,48 +0,0 @@
|
||||
They actually use multiple frames, so this is a "fake" frame combining the two into one, but without checksum fields.
|
||||
|
||||
7E
|
||||
A0 BD
|
||||
CF 02 23 03 00 00
|
||||
E6 E7 00
|
||||
0F 00 03 46 3B
|
||||
0C 07 E9 0C 13 05 17 37 28 00 FF C4 00
|
||||
|
||||
02 21
|
||||
09 08 39 32 30 32 39 36 39 31
|
||||
09 04 17 37 28 00
|
||||
09 05 07 E9 0C 13 05
|
||||
06 00 6C 28 5A
|
||||
06 00 4B 76 1A
|
||||
06 00 20 B2 40
|
||||
06 00 58 68 AA
|
||||
06 00 57 A1 62
|
||||
06 00 00 C7 48
|
||||
06 00 17 EE D7
|
||||
06 00 12 F5 5C
|
||||
06 00 00 D9 6A
|
||||
06 00 15 36 84
|
||||
06 00 00 01 7E
|
||||
06 00 00 00 00
|
||||
12 03 79
|
||||
06 00 00 00 7F
|
||||
06 00 00 00 BD
|
||||
06 00 00 00 41
|
||||
06 00 00 00 00
|
||||
06 00 00 00 00
|
||||
06 00 00 00 00
|
||||
12 09 54
|
||||
12 09 35
|
||||
12 09 49
|
||||
12 00 37
|
||||
12 00 59
|
||||
12 00 4D
|
||||
06 00 00 43 62
|
||||
01 01
|
||||
12 24 B8
|
||||
01 01
|
||||
12 24 B8
|
||||
01 01
|
||||
12 24 B8
|
||||
03 01
|
||||
|
||||
00 00 7E
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 173 KiB |
@@ -50,22 +50,6 @@
|
||||
#define LED_BEHAVIOUR_ERROR_ONLY 3
|
||||
#define LED_BEHAVIOUR_OFF 9
|
||||
|
||||
#define FIRMWARE_CHANNEL_STABLE 0
|
||||
#define FIRMWARE_CHANNEL_EARLY 1
|
||||
#define FIRMWARE_CHANNEL_RC 2
|
||||
#define FIRMWARE_CHANNEL_SNAPSHOT 3
|
||||
|
||||
#define REBOOT_CAUSE_WEB_SYSINFO_JSON 1
|
||||
#define REBOOT_CAUSE_WEB_SAVE 2
|
||||
#define REBOOT_CAUSE_WEB_REBOOT 3
|
||||
#define REBOOT_CAUSE_WEB_FACTORY_RESET 4
|
||||
#define REBOOT_CAUSE_BTN_FACTORY_RESET 5
|
||||
#define REBOOT_CAUSE_REPARTITION 6
|
||||
#define REBOOT_CAUSE_CONFIG_FILE_UPDATE 7
|
||||
#define REBOOT_CAUSE_FIRMWARE_UPDATE 8
|
||||
#define REBOOT_CAUSE_MQTT_DISCONNECTED 9
|
||||
#define REBOOT_CAUSE_SMART_CONFIG 10
|
||||
|
||||
struct ResetDataContainer {
|
||||
uint8_t cause;
|
||||
uint8_t last_cause;
|
||||
@@ -79,8 +63,7 @@ struct SystemConfig {
|
||||
uint8_t dataCollectionConsent; // 0 = unknown, 1 = accepted, 2 = declined
|
||||
char country[3];
|
||||
uint8_t energyspeedometer;
|
||||
uint8_t firmwareChannel;
|
||||
}; // 9
|
||||
}; // 8
|
||||
|
||||
struct NetworkConfig {
|
||||
char ssid[32];
|
||||
@@ -175,8 +158,7 @@ struct GpioConfig {
|
||||
uint16_t vccResistorVcc;
|
||||
uint8_t ledDisablePin;
|
||||
uint8_t ledBehaviour;
|
||||
uint8_t powersaving;
|
||||
}; // 22
|
||||
}; // 21
|
||||
|
||||
struct GpioConfig103 {
|
||||
uint8_t hanPin;
|
||||
@@ -225,12 +207,10 @@ struct PriceServiceConfig {
|
||||
char entsoeToken[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
uint16_t unused2;
|
||||
uint16_t unused3;
|
||||
uint32_t unused1;
|
||||
bool enabled;
|
||||
uint16_t unused6;
|
||||
};
|
||||
uint16_t unused2;
|
||||
}; // 64
|
||||
|
||||
struct EnergyAccountingConfig {
|
||||
uint16_t thresholds[10];
|
||||
@@ -257,14 +237,14 @@ struct UiConfig {
|
||||
}; // 15
|
||||
|
||||
struct UpgradeInformation {
|
||||
char fromVersion[16];
|
||||
char toVersion[16];
|
||||
char fromVersion[8];
|
||||
char toVersion[8];
|
||||
uint32_t size;
|
||||
uint16_t block_position;
|
||||
uint8_t retry_count;
|
||||
uint8_t reboot_count;
|
||||
int8_t errorCode;
|
||||
}; // 41+3
|
||||
}; // 25
|
||||
|
||||
struct CloudConfig {
|
||||
bool enabled;
|
||||
|
||||
@@ -15,11 +15,6 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
|
||||
uint8_t configVersion = EEPROM.read(EEPROM_CONFIG_ADDRESS);
|
||||
EEPROM.get(CONFIG_SYSTEM_START, config);
|
||||
EEPROM.end();
|
||||
|
||||
if(config.firmwareChannel > 3) {
|
||||
config.firmwareChannel = 0;
|
||||
}
|
||||
|
||||
if(configVersion == EEPROM_CHECK_SUM) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -32,7 +27,6 @@ bool AmsConfiguration::getSystemConfig(SystemConfig& config) {
|
||||
}
|
||||
config.userConfigured = false;
|
||||
config.dataCollectionConsent = 0;
|
||||
config.firmwareChannel = 0;
|
||||
config.energyspeedometer = 0;
|
||||
memset(config.country, 0, 3);
|
||||
return false;
|
||||
@@ -48,9 +42,6 @@ bool AmsConfiguration::setSystemConfig(SystemConfig& config) {
|
||||
sysChanged |= config.dataCollectionConsent != existing.dataCollectionConsent;
|
||||
sysChanged |= strcmp(config.country, existing.country) != 0;
|
||||
sysChanged |= config.energyspeedometer != existing.energyspeedometer;
|
||||
sysChanged |= config.firmwareChannel != existing.firmwareChannel;
|
||||
} else {
|
||||
sysChanged = true;
|
||||
}
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
stripNonAscii((uint8_t*) config.country, 2);
|
||||
@@ -517,7 +508,6 @@ bool AmsConfiguration::getGpioConfig(GpioConfig& config) {
|
||||
if(configVersion == EEPROM_CHECK_SUM || configVersion == EEPROM_CLEARED_INDICATOR) {
|
||||
EEPROM.get(CONFIG_GPIO_START, config);
|
||||
EEPROM.end();
|
||||
if(config.powersaving > 4) config.powersaving = 0;
|
||||
return true;
|
||||
} else {
|
||||
clearGpio(config);
|
||||
@@ -599,7 +589,6 @@ void AmsConfiguration::clearGpio(GpioConfig& config, bool all) {
|
||||
config.vccResistorGnd = 0;
|
||||
config.vccResistorVcc = 0;
|
||||
config.ledBehaviour = LED_BEHAVIOUR_DEFAULT;
|
||||
config.powersaving = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,9 +655,6 @@ bool AmsConfiguration::getPriceServiceConfig(PriceServiceConfig& config) {
|
||||
clearPriceServiceConfig(config);
|
||||
return false;
|
||||
}
|
||||
if(config.resolutionInMinutes != 15 && config.resolutionInMinutes != 60) {
|
||||
config.resolutionInMinutes = 60;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
clearPriceServiceConfig(config);
|
||||
@@ -683,7 +669,6 @@ bool AmsConfiguration::setPriceServiceConfig(PriceServiceConfig& config) {
|
||||
priceChanged |= strcmp(config.area, existing.area) != 0;
|
||||
priceChanged |= strcmp(config.currency, existing.currency) != 0;
|
||||
priceChanged |= config.enabled != existing.enabled;
|
||||
priceChanged |= config.resolutionInMinutes != existing.resolutionInMinutes;
|
||||
} else {
|
||||
priceChanged = true;
|
||||
}
|
||||
@@ -703,8 +688,9 @@ void AmsConfiguration::clearPriceServiceConfig(PriceServiceConfig& config) {
|
||||
memset(config.entsoeToken, 0, 37);
|
||||
memset(config.area, 0, 17);
|
||||
memset(config.currency, 0, 4);
|
||||
config.unused1 = 1000;
|
||||
config.enabled = false;
|
||||
config.resolutionInMinutes = 60;
|
||||
config.unused2 = 0;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isPriceServiceChanged() {
|
||||
@@ -832,8 +818,8 @@ void AmsConfiguration::ackUiLanguageChange() {
|
||||
}
|
||||
|
||||
bool AmsConfiguration::setUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
stripNonAscii((uint8_t*) upinfo.fromVersion, 16);
|
||||
stripNonAscii((uint8_t*) upinfo.toVersion, 16);
|
||||
stripNonAscii((uint8_t*) upinfo.fromVersion, 8);
|
||||
stripNonAscii((uint8_t*) upinfo.toVersion, 8);
|
||||
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.put(CONFIG_UPGRADE_INFO_START, upinfo);
|
||||
@@ -847,7 +833,7 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_UPGRADE_INFO_START, upinfo);
|
||||
EEPROM.end();
|
||||
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 16) || stripNonAscii((uint8_t*) upinfo.toVersion, 16)) {
|
||||
if(stripNonAscii((uint8_t*) upinfo.fromVersion, 8) || stripNonAscii((uint8_t*) upinfo.toVersion, 8)) {
|
||||
clearUpgradeInformation(upinfo);
|
||||
return false;
|
||||
}
|
||||
@@ -859,8 +845,8 @@ bool AmsConfiguration::getUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
}
|
||||
|
||||
void AmsConfiguration::clearUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
memset(upinfo.fromVersion, 0, 16);
|
||||
memset(upinfo.toVersion, 0, 16);
|
||||
memset(upinfo.fromVersion, 0, 8);
|
||||
memset(upinfo.toVersion, 0, 8);
|
||||
upinfo.errorCode = 0;
|
||||
upinfo.size = 0;
|
||||
upinfo.block_position = 0;
|
||||
@@ -990,7 +976,6 @@ void AmsConfiguration::clear() {
|
||||
EEPROM.get(CONFIG_SYSTEM_START, sys);
|
||||
sys.userConfigured = false;
|
||||
sys.dataCollectionConsent = 0;
|
||||
sys.firmwareChannel = 0;
|
||||
sys.energyspeedometer = 0;
|
||||
memset(sys.country, 0, 3);
|
||||
EEPROM.put(CONFIG_SYSTEM_START, sys);
|
||||
@@ -1153,8 +1138,7 @@ bool AmsConfiguration::relocateConfig103() {
|
||||
gpio103.vccResistorGnd,
|
||||
gpio103.vccResistorVcc,
|
||||
gpio103.ledDisablePin,
|
||||
gpio103.ledBehaviour,
|
||||
0
|
||||
gpio103.ledBehaviour
|
||||
};
|
||||
|
||||
WebConfig web = {web103.security};
|
||||
@@ -1326,7 +1310,6 @@ void AmsConfiguration::print(Print* debugger)
|
||||
debugger->printf_P(PSTR("Vcc pin: %i\r\n"), gpio.vccPin);
|
||||
debugger->printf_P(PSTR("LED disable pin: %i\r\n"), gpio.ledDisablePin);
|
||||
debugger->printf_P(PSTR("LED behaviour: %i\r\n"), gpio.ledBehaviour);
|
||||
debugger->printf_P(PSTR("Power saving: %i\r\n"), gpio.powersaving);
|
||||
if(gpio.vccMultiplier > 0) {
|
||||
debugger->printf_P(PSTR("Vcc multiplier: %f\r\n"), gpio.vccMultiplier / 1000.0);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ int8_t DSMRParser::parse(uint8_t *buf, DataParserContext &ctx, bool verified, Pr
|
||||
fromHex((uint8_t*) &crc, String((char*) buf+crcPos), 2);
|
||||
crc = ntohs(crc);
|
||||
|
||||
if(crc > 0 && crc != crc_calc) {
|
||||
if(crc != crc_calc) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf_P(PSTR("CRC incorrrect, %04X != %04X at position %lu\n"), crc, crc_calc, crcPos);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
return DATA_PARSE_BOUNDARY_FLAG_MISSING;
|
||||
|
||||
// Verify FCS
|
||||
if(f->fcs > 0 && ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
|
||||
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
|
||||
return DATA_PARSE_FOOTER_CHECKSUM_ERROR;
|
||||
|
||||
// Skip destination address, LSB marks last byte
|
||||
@@ -50,7 +50,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
|
||||
|
||||
// Verify HCS
|
||||
if(t3->hcs > 0 && ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
|
||||
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
|
||||
return DATA_PARSE_HEADER_CHECKSUM_ERROR;
|
||||
ptr += 3;
|
||||
|
||||
@@ -69,12 +69,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
|
||||
if(buf == NULL) return DATA_PARSE_FAIL;
|
||||
|
||||
if((*ptr) == DATA_TAG_LLC) {
|
||||
ptr += 3; // Skip LLC
|
||||
ctx.length -= 3;
|
||||
}
|
||||
|
||||
memcpy(buf + pos, ptr, ctx.length);
|
||||
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
|
||||
pos += ctx.length;
|
||||
|
||||
lastSequenceNumber++;
|
||||
@@ -83,12 +78,7 @@ int8_t HDLCParser::parse(uint8_t *d, DataParserContext &ctx) {
|
||||
lastSequenceNumber = 0;
|
||||
if(buf == NULL) return DATA_PARSE_FAIL;
|
||||
|
||||
if((*ptr) == DATA_TAG_LLC) {
|
||||
ptr += 3; // Skip LLC
|
||||
ctx.length -= 3;
|
||||
}
|
||||
|
||||
memcpy(buf + pos, ptr, ctx.length);
|
||||
memcpy(buf + pos, ptr+3, ctx.length); // +3 to skip LLC
|
||||
pos += ctx.length;
|
||||
|
||||
memcpy((uint8_t *) d, buf, pos);
|
||||
|
||||
@@ -39,8 +39,6 @@
|
||||
#define AMS_UPDATE_ERR_SUCCESS_CONFIRMED 123
|
||||
|
||||
#define UPDATE_BUF_SIZE 4096
|
||||
#define UPDATE_MAX_BLOCK_RETRY 25
|
||||
#define UPDATE_MAX_REBOOT_RETRY 12
|
||||
|
||||
class AmsFirmwareUpdater {
|
||||
public:
|
||||
@@ -62,13 +60,6 @@ public:
|
||||
bool isUpgradeInformationChanged();
|
||||
void ackUpgradeInformationChanged();
|
||||
|
||||
void setFirmwareChannel(uint8_t channel) {
|
||||
if(firmwareChannel != channel) {
|
||||
firmwareChannel = channel;
|
||||
lastVersionCheck = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool startFirmwareUpload(uint32_t size, const char* version);
|
||||
bool addFirmwareUploadChunk(uint8_t* buf, size_t length);
|
||||
bool completeFirmwareUpload(uint32_t size);
|
||||
@@ -104,11 +95,10 @@ private:
|
||||
String md5;
|
||||
|
||||
uint32_t lastVersionCheck = 0;
|
||||
uint8_t firmwareChannel;
|
||||
uint8_t firmwareVariant;
|
||||
bool autoUpgrade;
|
||||
char nextVersion[17];
|
||||
char nextVersion[10];
|
||||
|
||||
void getChannelName(char * buffer);
|
||||
|
||||
bool fetchNextVersion();
|
||||
bool fetchVersionDetails();
|
||||
|
||||
@@ -22,7 +22,7 @@ this->debugger = debugger;
|
||||
this->hw = hw;
|
||||
this->meterState = meterState;
|
||||
memset(nextVersion, 0, sizeof(nextVersion));
|
||||
firmwareChannel = 0;
|
||||
firmwareVariant = 0;
|
||||
autoUpgrade = false;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ void AmsFirmwareUpdater::setUpgradeInformation(UpgradeInformation& upinfo) {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Resuming uprade to %s\n"), updateStatus.toVersion);
|
||||
|
||||
if(updateStatus.reboot_count++ < UPDATE_MAX_REBOOT_RETRY) {
|
||||
if(updateStatus.reboot_count++ < 8) {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_OK;
|
||||
} else {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_REBOOT;
|
||||
@@ -129,7 +129,7 @@ void AmsFirmwareUpdater::loop() {
|
||||
HTTPClient http;
|
||||
start = millis();
|
||||
if(!fetchFirmwareChunk(http)) {
|
||||
if(updateStatus.retry_count++ > UPDATE_MAX_BLOCK_RETRY) {
|
||||
if(updateStatus.retry_count++ == 3) {
|
||||
updateStatus.errorCode = AMS_UPDATE_ERR_FETCH;
|
||||
updateStatusChanged = true;
|
||||
}
|
||||
@@ -208,33 +208,15 @@ void AmsFirmwareUpdater::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void AmsFirmwareUpdater::getChannelName(char * buffer) {
|
||||
switch(firmwareChannel) {
|
||||
case FIRMWARE_CHANNEL_EARLY:
|
||||
strcpy(buffer, PSTR("early"));
|
||||
break;
|
||||
case FIRMWARE_CHANNEL_RC:
|
||||
strcpy(buffer, PSTR("rc"));
|
||||
break;
|
||||
case FIRMWARE_CHANNEL_SNAPSHOT:
|
||||
strcpy(buffer, PSTR("snapshot"));
|
||||
break;
|
||||
default:
|
||||
strcpy(buffer, PSTR("stable"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AmsFirmwareUpdater::fetchNextVersion() {
|
||||
HTTPClient http;
|
||||
const char * headerkeys[] = { "x-version" };
|
||||
http.collectHeaders(headerkeys, 1);
|
||||
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
char firmwareVariant[10] = "stable";
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, channel);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/next"), chipType, firmwareVariant);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -248,16 +230,6 @@ bool AmsFirmwareUpdater::fetchNextVersion() {
|
||||
http.setUserAgent("AMS-Firmware-Updater");
|
||||
http.addHeader(F("Cache-Control"), "no-cache");
|
||||
http.addHeader(F("x-AMS-version"), FirmwareVersion::VersionString);
|
||||
http.addHeader(F("x-AMS-STA-MAC"), WiFi.macAddress());
|
||||
http.addHeader(F("x-AMS-AP-MAC"), WiFi.softAPmacAddress());
|
||||
http.addHeader(F("x-AMS-chip-size"), String(ESP.getFlashChipSize()));
|
||||
http.addHeader(F("x-AMS-board-type"), String(hw->getBoardType(), 10));
|
||||
if(meterState->getMeterType() != AmsTypeAutodetect) {
|
||||
http.addHeader(F("x-AMS-meter-mfg"), String(meterState->getMeterType(), 10));
|
||||
}
|
||||
if(!meterState->getMeterModel().isEmpty()) {
|
||||
http.addHeader(F("x-AMS-meter-model"), meterState->getMeterModel());
|
||||
}
|
||||
int status = http.GET();
|
||||
if(status == 204) {
|
||||
String nextVersion = http.header("x-version");
|
||||
@@ -281,11 +253,10 @@ bool AmsFirmwareUpdater::fetchVersionDetails() {
|
||||
const char * headerkeys[] = { "x-size" };
|
||||
http.collectHeaders(headerkeys, 1);
|
||||
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
char firmwareVariant[10] = "stable";
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, channel, updateStatus.toVersion);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/details"), chipType, firmwareVariant, updateStatus.toVersion);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
@@ -338,11 +309,10 @@ bool AmsFirmwareUpdater::fetchFirmwareChunk(HTTPClient& http) {
|
||||
char range[24];
|
||||
snprintf_P(range, 24, PSTR("bytes=%lu-%lu"), start, end);
|
||||
|
||||
char channel[10] = "";
|
||||
getChannelName(channel);
|
||||
char firmwareVariant[10] = "stable";
|
||||
|
||||
char url[128];
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, channel, updateStatus.toVersion);
|
||||
snprintf_P(url, 128, PSTR("http://hub.amsleser.no/hub/firmware/%s/%s/%s/chunk"), chipType, firmwareVariant, updateStatus.toVersion);
|
||||
#if defined(ESP8266)
|
||||
WiFiClient client;
|
||||
client.setTimeout(5000);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "AmsDataStorage.h"
|
||||
|
||||
class AmsJsonGenerator {
|
||||
public:
|
||||
static void generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
|
||||
static void generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "AmsJsonGenerator.h"
|
||||
|
||||
void AmsJsonGenerator::generateDayPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
|
||||
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 0; i < 24; i++) {
|
||||
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
|
||||
}
|
||||
|
||||
void AmsJsonGenerator::generateMonthPlotJson(AmsDataStorage* ds, char* buf, size_t bufSize) {
|
||||
uint16_t pos = snprintf_P(buf, bufSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 1; i < 32; i++) {
|
||||
pos += snprintf_P(buf+pos, bufSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, bufSize-pos, PSTR("}"));
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
AmsMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, AmsFirmwareUpdater* updater) {
|
||||
#else
|
||||
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) {
|
||||
AmsMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) {
|
||||
#endif
|
||||
this->mqttConfig = mqttConfig;
|
||||
this->mqttConfigChanged = true;
|
||||
@@ -43,12 +43,10 @@ public:
|
||||
void setConfig(MqttConfig& mqttConfig);
|
||||
|
||||
bool connect();
|
||||
bool defaultSubscribe();
|
||||
void disconnect();
|
||||
lwmqtt_err_t lastError();
|
||||
bool connected();
|
||||
bool loop();
|
||||
bool isRebootSuggested();
|
||||
|
||||
virtual uint8_t getFormat() { return 0; };
|
||||
|
||||
@@ -57,16 +55,11 @@ public:
|
||||
virtual bool publishTemperatures(AmsConfiguration*, HwTools*) { return false; };
|
||||
virtual bool publishPrices(PriceService* ps) { return false; };
|
||||
virtual bool publishSystem(HwTools*, PriceService*, EnergyAccounting*) { return false; };
|
||||
virtual bool publishRaw(uint8_t* raw, size_t length) { return false; };
|
||||
virtual bool publishRaw(String data) { return false; };
|
||||
virtual bool publishFirmware() { return false; };
|
||||
virtual void onMessage(String &topic, String &payload) {};
|
||||
|
||||
virtual ~AmsMqttHandler() {
|
||||
if(mqttSecureClient != NULL) {
|
||||
mqttSecureClient->stop();
|
||||
delete mqttSecureClient;
|
||||
}
|
||||
|
||||
if(mqttClient != NULL) {
|
||||
mqttClient->stop();
|
||||
delete mqttClient;
|
||||
@@ -86,7 +79,6 @@ protected:
|
||||
bool caVerification = true;
|
||||
WiFiClient *mqttClient = NULL;
|
||||
WiFiClientSecure *mqttSecureClient = NULL;
|
||||
boolean _connected = false;
|
||||
char* json;
|
||||
uint16_t BufferSize = 2048;
|
||||
uint64_t lastStateUpdate = 0;
|
||||
@@ -96,7 +88,6 @@ protected:
|
||||
String subTopic;
|
||||
|
||||
AmsFirmwareUpdater* updater = NULL;
|
||||
bool rebootSuggested = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -103,17 +103,6 @@ bool AmsMqttHandler::connect() {
|
||||
actualClient = mqttClient;
|
||||
}
|
||||
|
||||
// This section helps with power saving on ESP32 devices by reducing timeouts
|
||||
// The timeout is multiplied by 10 because WiFiClient is retrying 10 times internally
|
||||
// Power drain for this timeout is too great when using the default 3s timeout
|
||||
// On ESP8266 the timeout is used differently and the following code causes MQTT instability
|
||||
#if defined(ESP32)
|
||||
int clientTimeout = mqttConfig.timeout / 1000;
|
||||
if(clientTimeout > 3) clientTimeout = 3; // 3000ms is default, see WiFiClient.cpp WIFI_CLIENT_DEF_CONN_TIMEOUT_MS
|
||||
actualClient->setTimeout(clientTimeout);
|
||||
// Why can't we set number of retries for write here? WiFiClient defaults to 10 (10*3s == 30s)
|
||||
#endif
|
||||
|
||||
mqttConfigChanged = false;
|
||||
mqtt.setTimeout(mqttConfig.timeout);
|
||||
mqtt.setKeepAlive(mqttConfig.keepalive);
|
||||
@@ -136,9 +125,20 @@ bool AmsMqttHandler::connect() {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Successfully connected to MQTT\n"));
|
||||
mqtt.onMessage(std::bind(&AmsMqttHandler::onMessage, this, std::placeholders::_1, std::placeholders::_2));
|
||||
_connected = mqtt.publish(statusTopic, "online", true, 0);
|
||||
if(strlen(mqttConfig.subscribeTopic) > 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Subscribing to [%s]\n"), mqttConfig.subscribeTopic);
|
||||
if(!mqtt.subscribe(mqttConfig.subscribeTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), mqttConfig.subscribeTopic);
|
||||
}
|
||||
}
|
||||
mqtt.publish(statusTopic, "online", true, 0);
|
||||
mqtt.loop();
|
||||
defaultSubscribe();
|
||||
postConnect();
|
||||
return true;
|
||||
} else {
|
||||
@@ -158,29 +158,13 @@ bool AmsMqttHandler::connect() {
|
||||
}
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::defaultSubscribe() {
|
||||
bool ret = true;
|
||||
if(!subTopic.isEmpty()) {
|
||||
if(mqtt.subscribe(subTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), subTopic.c_str());
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), subTopic.c_str());
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AmsMqttHandler::disconnect() {
|
||||
mqtt.disconnect();
|
||||
mqtt.loop();
|
||||
_connected = false;
|
||||
if(mqttSecureClient != NULL) {
|
||||
delete mqttSecureClient;
|
||||
mqttSecureClient = NULL;
|
||||
}
|
||||
delay(10);
|
||||
yield();
|
||||
}
|
||||
@@ -190,12 +174,12 @@ lwmqtt_err_t AmsMqttHandler::lastError() {
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::connected() {
|
||||
return _connected && mqtt.connected();
|
||||
return mqtt.connected();
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::loop() {
|
||||
uint64_t now = millis64();
|
||||
bool ret = connected() && mqtt.loop();
|
||||
bool ret = mqtt.connected() && mqtt.loop();
|
||||
if(ret) {
|
||||
lastSuccessfulLoop = now;
|
||||
} else if(mqttConfig.rebootMinutes > 0) {
|
||||
@@ -205,10 +189,10 @@ bool AmsMqttHandler::loop() {
|
||||
if (debugger->isActive(RemoteDebug::WARNING))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("MQTT connection lost for over %d minutes, rebooting device\n"), mqttConfig.rebootMinutes);
|
||||
rebootSuggested = true;
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
|
||||
delay(10);
|
||||
yield();
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
@@ -216,8 +200,4 @@ bool AmsMqttHandler::loop() {
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsMqttHandler::isRebootSuggested() {
|
||||
return rebootSuggested;
|
||||
}
|
||||
@@ -114,7 +114,6 @@ bool EthernetConnectionHandler::connect(NetworkConfig config, SystemConfig sys)
|
||||
debugger->printf_P(PSTR("Static IP configuration is invalid, not using\n"));
|
||||
}
|
||||
}
|
||||
this->config = config;
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
@@ -148,9 +147,6 @@ void EthernetConnectionHandler::eventHandler(WiFiEvent_t event, WiFiEventInfo_t
|
||||
{
|
||||
debugger->printf_P(PSTR("Successfully connected to Ethernet!\n"));
|
||||
}
|
||||
if(config.ipv6 && !ETH.enableIpV6()) {
|
||||
debugger->printf_P(PSTR("Unable to enable IPv6\n"));
|
||||
}
|
||||
break;
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
this->config = config;
|
||||
};
|
||||
#else
|
||||
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
DomoticzMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, DomoticzConfig config) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
this->config = config;
|
||||
};
|
||||
#endif
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishRaw(String data);
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ uint8_t DomoticzMqttHandler::getFormat() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
bool DomoticzMqttHandler::publishRaw(uint8_t* raw, size_t length) {
|
||||
bool DomoticzMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,6 @@
|
||||
#include "PriceService.h"
|
||||
|
||||
struct EnergyAccountingPeak {
|
||||
uint8_t day;
|
||||
uint8_t hour;
|
||||
uint16_t value;
|
||||
};
|
||||
|
||||
struct EnergyAccountingPeak6 {
|
||||
uint8_t day;
|
||||
uint16_t value;
|
||||
};
|
||||
@@ -26,11 +20,9 @@ struct EnergyAccountingPeak6 {
|
||||
struct EnergyAccountingData {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
int32_t costToday;
|
||||
int32_t costYesterday;
|
||||
int32_t costThisMonth;
|
||||
int32_t costLastMonth;
|
||||
int32_t incomeToday;
|
||||
int32_t incomeYesterday;
|
||||
int32_t incomeThisMonth;
|
||||
int32_t incomeLastMonth;
|
||||
@@ -38,22 +30,36 @@ struct EnergyAccountingData {
|
||||
uint32_t lastMonthExport;
|
||||
uint8_t lastMonthAccuracy;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
time_t lastUpdated;
|
||||
};
|
||||
|
||||
struct EnergyAccountingData6 {
|
||||
struct EnergyAccountingData5 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
int32_t costYesterday;
|
||||
int32_t costThisMonth;
|
||||
int32_t costLastMonth;
|
||||
int32_t incomeYesterday;
|
||||
int32_t incomeThisMonth;
|
||||
int32_t incomeLastMonth;
|
||||
uint32_t lastMonthImport;
|
||||
uint32_t lastMonthExport;
|
||||
uint8_t lastMonthAccuracy;
|
||||
EnergyAccountingPeak6 peaks[5];
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
uint16_t incomeYesterday;
|
||||
uint16_t incomeThisMonth;
|
||||
uint16_t incomeLastMonth;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData4 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData2 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t maxHour;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
};
|
||||
|
||||
struct EnergyAccountingRealtimeData {
|
||||
@@ -118,6 +124,7 @@ public:
|
||||
void setData(EnergyAccountingData&);
|
||||
|
||||
void setCurrency(String currency);
|
||||
float getPriceForHour(uint8_t d, uint8_t h);
|
||||
|
||||
private:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -130,12 +137,12 @@ private:
|
||||
PriceService *ps = NULL;
|
||||
EnergyAccountingConfig *config = NULL;
|
||||
Timezone *tz = NULL;
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
||||
EnergyAccountingRealtimeData* realtimeData = NULL;
|
||||
String currency = "";
|
||||
|
||||
void calcDayCost();
|
||||
bool updateMax(uint16_t val, uint8_t day, uint8_t hour);
|
||||
bool updateMax(uint16_t val, uint8_t day);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,7 @@ EnergyAccounting::EnergyAccounting(Stream* Stream, EnergyAccountingRealtimeData*
|
||||
rtd->lastImportUpdateMillis = 0;
|
||||
rtd->lastExportUpdateMillis = 0;
|
||||
}
|
||||
realtimeData = rtd;
|
||||
this->realtimeData = rtd;
|
||||
}
|
||||
|
||||
void EnergyAccounting::setup(AmsDataStorage *ds, EnergyAccountingConfig *config) {
|
||||
@@ -67,55 +67,59 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
breakTime(tz->toLocal(now), local);
|
||||
|
||||
if(!init) {
|
||||
realtimeData->lastImportUpdateMillis = 0;
|
||||
realtimeData->lastExportUpdateMillis = 0;
|
||||
realtimeData->currentHour = local.Hour;
|
||||
realtimeData->currentDay = local.Day;
|
||||
this->realtimeData->lastImportUpdateMillis = 0;
|
||||
this->realtimeData->lastExportUpdateMillis = 0;
|
||||
this->realtimeData->currentHour = local.Hour;
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
if(!load()) {
|
||||
data = { 7, local.Month,
|
||||
0, 0, 0, 0, // Cost
|
||||
0, 0, 0, 0, // Income
|
||||
data = { 6, local.Month,
|
||||
0, 0, 0, // Cost
|
||||
0, 0, 0, // Income
|
||||
0, 0, 0, // Last month import, export and accuracy
|
||||
0, 0, 0, // Peak 1
|
||||
0, 0, 0, // Peak 2
|
||||
0, 0, 0, // Peak 3
|
||||
0, 0, 0, // Peak 4
|
||||
0, 0, 0 // Peak 5
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
0, 0, // Peak 4
|
||||
0, 0 // Peak 5
|
||||
};
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
|
||||
if(!initPrice && ps != NULL && ps->hasPrice()) {
|
||||
float importPrice = getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
|
||||
if(!initPrice && importPrice != PRICE_NO_VALUE) {
|
||||
calcDayCost();
|
||||
}
|
||||
|
||||
if(local.Hour != realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
if(local.Hour != this->realtimeData->currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
tmElements_t oneHrAgo, oneHrAgoLocal;
|
||||
breakTime(now-3600, oneHrAgo);
|
||||
uint16_t val = round(ds->getHourImport(oneHrAgo.Hour) / 10.0);
|
||||
|
||||
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
|
||||
ret |= updateMax(val, oneHrAgoLocal.Day, oneHrAgoLocal.Hour);
|
||||
ret |= updateMax(val, oneHrAgoLocal.Day);
|
||||
|
||||
realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
this->realtimeData->currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
if(local.Hour > 0) {
|
||||
calcDayCost();
|
||||
}
|
||||
|
||||
realtimeData->use = 0;
|
||||
realtimeData->produce = 0;
|
||||
realtimeData->costHour = 0;
|
||||
realtimeData->incomeHour = 0;
|
||||
this->realtimeData->use = 0;
|
||||
this->realtimeData->produce = 0;
|
||||
this->realtimeData->costHour = 0;
|
||||
this->realtimeData->incomeHour = 0;
|
||||
|
||||
uint8_t prevDay = realtimeData->currentDay;
|
||||
if(local.Day != realtimeData->currentDay) {
|
||||
data.costYesterday = realtimeData->costDay * 100;
|
||||
data.costThisMonth += realtimeData->costDay * 100;
|
||||
realtimeData->costDay = 0;
|
||||
uint8_t prevDay = this->realtimeData->currentDay;
|
||||
if(local.Day != this->realtimeData->currentDay) {
|
||||
data.costYesterday = this->realtimeData->costDay * 100;
|
||||
data.costThisMonth += this->realtimeData->costDay * 100;
|
||||
this->realtimeData->costDay = 0;
|
||||
|
||||
data.incomeYesterday = realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += realtimeData->incomeDay * 100;
|
||||
realtimeData->incomeDay = 0;
|
||||
data.incomeYesterday = this->realtimeData->incomeDay * 100;
|
||||
data.incomeThisMonth += this->realtimeData->incomeDay * 100;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
|
||||
realtimeData->currentDay = local.Day;
|
||||
this->realtimeData->currentDay = local.Day;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
@@ -145,49 +149,42 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
data.lastMonthAccuracy = accuracy;
|
||||
|
||||
data.month = local.Month;
|
||||
realtimeData->currentThresholdIdx = 0;
|
||||
this->realtimeData->currentThresholdIdx = 0;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if(ret) {
|
||||
data.costToday = realtimeData->costDay * 100;
|
||||
data.incomeToday = realtimeData->incomeDay * 100;
|
||||
data.lastUpdated = now;
|
||||
}
|
||||
}
|
||||
|
||||
if(realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastImportUpdateMillis;
|
||||
if(this->realtimeData->lastImportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastImportUpdateMillis;
|
||||
float kwhi = (amsData->getActiveImportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhi > 0) {
|
||||
realtimeData->use += kwhi;
|
||||
float importPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
|
||||
this->realtimeData->use += kwhi;
|
||||
if(importPrice != PRICE_NO_VALUE) {
|
||||
float cost = importPrice * kwhi;
|
||||
realtimeData->costHour += cost;
|
||||
realtimeData->costDay += cost;
|
||||
this->realtimeData->costHour += cost;
|
||||
this->realtimeData->costDay += cost;
|
||||
}
|
||||
}
|
||||
realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
this->realtimeData->lastImportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(amsData->getListType() > 1 && realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - realtimeData->lastExportUpdateMillis;
|
||||
if(amsData->getListType() > 1 && this->realtimeData->lastExportUpdateMillis < amsData->getLastUpdateMillis()) {
|
||||
unsigned long ms = amsData->getLastUpdateMillis() - this->realtimeData->lastExportUpdateMillis;
|
||||
float kwhe = (amsData->getActiveExportPower() * (((float) ms) / 3600000.0)) / 1000.0;
|
||||
if(kwhe > 0) {
|
||||
realtimeData->produce += kwhe;
|
||||
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
|
||||
this->realtimeData->produce += kwhe;
|
||||
float exportPrice = getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
float income = exportPrice * kwhe;
|
||||
realtimeData->incomeHour += income;
|
||||
realtimeData->incomeDay += income;
|
||||
this->realtimeData->incomeHour += income;
|
||||
this->realtimeData->incomeDay += income;
|
||||
}
|
||||
}
|
||||
realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
this->realtimeData->lastExportUpdateMillis = amsData->getLastUpdateMillis();
|
||||
}
|
||||
|
||||
if(config != NULL) {
|
||||
while(getMonthMax() > config->thresholds[realtimeData->currentThresholdIdx] && realtimeData->currentThresholdIdx < 10) realtimeData->currentThresholdIdx++;
|
||||
while(getMonthMax() > config->thresholds[this->realtimeData->currentThresholdIdx] && this->realtimeData->currentThresholdIdx < 10) this->realtimeData->currentThresholdIdx++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
@@ -195,36 +192,28 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
|
||||
void EnergyAccounting::calcDayCost() {
|
||||
time_t now = time(nullptr);
|
||||
tmElements_t local, utc, lastUpdateUtc;
|
||||
tmElements_t local, utc;
|
||||
if(tz == NULL) return;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
if(ps == NULL) return;
|
||||
|
||||
if(ps->hasPrice()) {
|
||||
breakTime(data.lastUpdated, lastUpdateUtc);
|
||||
uint8_t calcFromHour = 0;
|
||||
if(lastUpdateUtc.Day != local.Day || lastUpdateUtc.Month != local.Month || lastUpdateUtc.Year != local.Year) {
|
||||
realtimeData->costDay = 0;
|
||||
realtimeData->incomeDay = 0;
|
||||
calcFromHour = 0;
|
||||
} else {
|
||||
realtimeData->costDay = data.costToday / 100.0;
|
||||
realtimeData->incomeDay = data.incomeToday / 100.0;
|
||||
calcFromHour = lastUpdateUtc.Hour;
|
||||
if(getPriceForHour(PRICE_DIRECTION_IMPORT, 0) != PRICE_NO_VALUE) {
|
||||
if(initPrice) {
|
||||
this->realtimeData->costDay = 0;
|
||||
this->realtimeData->incomeDay = 0;
|
||||
}
|
||||
for(uint8_t i = calcFromHour; i < realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
|
||||
float priceIn = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
float priceIn = getPriceForHour(PRICE_DIRECTION_IMPORT, i - local.Hour);
|
||||
if(priceIn != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourImport(utc.Hour);
|
||||
realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
this->realtimeData->costDay += priceIn * (wh / 1000.0);
|
||||
}
|
||||
|
||||
float priceOut = ps->getPriceForRelativeHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
float priceOut = getPriceForHour(PRICE_DIRECTION_EXPORT, i - local.Hour);
|
||||
if(priceOut != PRICE_NO_VALUE) {
|
||||
int16_t wh = ds->getHourExport(utc.Hour);
|
||||
realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
this->realtimeData->incomeDay += priceOut * (wh / 1000.0);
|
||||
}
|
||||
}
|
||||
initPrice = true;
|
||||
@@ -232,7 +221,7 @@ void EnergyAccounting::calcDayCost() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseThisHour() {
|
||||
return realtimeData->use;
|
||||
return this->realtimeData->use;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseToday() {
|
||||
@@ -242,7 +231,7 @@ float EnergyAccounting::getUseToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourImport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@@ -253,20 +242,18 @@ float EnergyAccounting::getUseThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayImport(i) / 1000.0;
|
||||
}
|
||||
return ret + getUseToday();
|
||||
}
|
||||
|
||||
float EnergyAccounting::getUseLastMonth() {
|
||||
float ret = (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
if(std::isnan(ret)) return 0.0;
|
||||
return ret;
|
||||
return (data.lastMonthImport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedThisHour() {
|
||||
return realtimeData->produce;
|
||||
return this->realtimeData->produce;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedToday() {
|
||||
@@ -276,7 +263,7 @@ float EnergyAccounting::getProducedToday() {
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(uint8_t i = 0; i < realtimeData->currentHour; i++) {
|
||||
for(uint8_t i = 0; i < this->realtimeData->currentHour; i++) {
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourExport(utc.Hour) / 1000.0;
|
||||
}
|
||||
@@ -287,24 +274,22 @@ float EnergyAccounting::getProducedThisMonth() {
|
||||
time_t now = time(nullptr);
|
||||
if(now < FirmwareVersion::BuildEpoch) return 0.0;
|
||||
float ret = 0;
|
||||
for(uint8_t i = 1; i < realtimeData->currentDay; i++) {
|
||||
for(uint8_t i = 1; i < this->realtimeData->currentDay; i++) {
|
||||
ret += ds->getDayExport(i) / 1000.0;
|
||||
}
|
||||
return ret + getProducedToday();
|
||||
}
|
||||
|
||||
float EnergyAccounting::getProducedLastMonth() {
|
||||
float ret = (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
if(std::isnan(ret)) return 0.0;
|
||||
return ret;
|
||||
return (data.lastMonthExport * pow(10, data.lastMonthAccuracy)) / 1000;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostThisHour() {
|
||||
return realtimeData->costHour;
|
||||
return this->realtimeData->costHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostToday() {
|
||||
return realtimeData->costDay;
|
||||
return this->realtimeData->costDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getCostYesterday() {
|
||||
@@ -320,11 +305,11 @@ float EnergyAccounting::getCostLastMonth() {
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeThisHour() {
|
||||
return realtimeData->incomeHour;
|
||||
return this->realtimeData->incomeHour;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeToday() {
|
||||
return realtimeData->incomeDay;
|
||||
return this->realtimeData->incomeDay;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getIncomeYesterday() {
|
||||
@@ -342,7 +327,7 @@ float EnergyAccounting::getIncomeLastMonth() {
|
||||
uint8_t EnergyAccounting::getCurrentThreshold() {
|
||||
if(config == NULL)
|
||||
return 0;
|
||||
return config->thresholds[realtimeData->currentThresholdIdx];
|
||||
return config->thresholds[this->realtimeData->currentThresholdIdx];
|
||||
}
|
||||
|
||||
float EnergyAccounting::getMonthMax() {
|
||||
@@ -422,31 +407,85 @@ bool EnergyAccounting::load() {
|
||||
char buf[file.size()];
|
||||
file.readBytes(buf, file.size());
|
||||
|
||||
if(buf[0] == 7) {
|
||||
if(buf[0] == 6) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
memcpy(&this->data, data, sizeof(this->data));
|
||||
ret = true;
|
||||
} else if(buf[0] == 6) {
|
||||
EnergyAccountingData6* data = (EnergyAccountingData6*) buf;
|
||||
this->data = { 7, data->month,
|
||||
0, // Cost today
|
||||
data->costYesterday,
|
||||
data->costThisMonth,
|
||||
data->costLastMonth,
|
||||
0, // Income today
|
||||
data->incomeYesterday,
|
||||
data->incomeThisMonth,
|
||||
data->incomeLastMonth,
|
||||
data->lastMonthImport,
|
||||
data->lastMonthExport,
|
||||
data->lastMonthAccuracy,
|
||||
data->peaks[0].day, 0, data->peaks[0].value,
|
||||
data->peaks[1].day, 0, data->peaks[1].value,
|
||||
data->peaks[2].day, 0, data->peaks[2].value,
|
||||
data->peaks[3].day, 0, data->peaks[3].value,
|
||||
data->peaks[4].day, 0, data->peaks[4].value
|
||||
} else if(buf[0] == 5) {
|
||||
EnergyAccountingData5* data = (EnergyAccountingData5*) buf;
|
||||
this->data = { 6, data->month,
|
||||
((uint32_t) data->costYesterday) * 10,
|
||||
((uint32_t) data->costThisMonth) * 100,
|
||||
((uint32_t) data->costLastMonth) * 100,
|
||||
((uint32_t) data->incomeYesterday) * 10,
|
||||
((uint32_t) data->incomeThisMonth) * 100,
|
||||
((uint32_t) data->incomeLastMonth) * 100,
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else if(buf[0] == 4) {
|
||||
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
|
||||
this->data = { 5, data->month,
|
||||
((uint32_t) data->costYesterday) * 10,
|
||||
((uint32_t) data->costThisMonth) * 100,
|
||||
((uint32_t) data->costLastMonth) * 100,
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else if(buf[0] == 3) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
this->data = { 5, data->month,
|
||||
data->costYesterday * 10,
|
||||
data->costThisMonth,
|
||||
data->costLastMonth,
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else {
|
||||
data = { 5, 0,
|
||||
0, 0, 0, // Cost
|
||||
0,0,0, // Income from production
|
||||
0,0,0, // Last month import, export and accuracy
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
0, 0, // Peak 4
|
||||
0, 0 // Peak 5
|
||||
};
|
||||
if(buf[0] == 2) {
|
||||
EnergyAccountingData2* data = (EnergyAccountingData2*) buf;
|
||||
this->data.month = data->month;
|
||||
this->data.costYesterday = data->costYesterday * 10;
|
||||
this->data.costThisMonth = data->costThisMonth;
|
||||
this->data.costLastMonth = data->costLastMonth;
|
||||
uint8_t b = 0;
|
||||
for(uint8_t i = sizeof(this->data); i < file.size(); i+=2) {
|
||||
this->data.peaks[b].day = b;
|
||||
memcpy(&this->data.peaks[b].value, buf+i, 2);
|
||||
b++;
|
||||
if(b >= config->hours || b >= 5) break;
|
||||
}
|
||||
ret = true;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -479,12 +518,11 @@ void EnergyAccounting::setData(EnergyAccountingData& data) {
|
||||
this->data = data;
|
||||
}
|
||||
|
||||
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
|
||||
bool EnergyAccounting::updateMax(uint16_t val, uint8_t day) {
|
||||
for(uint8_t i = 0; i < 5; i++) {
|
||||
if(data.peaks[i].day == day || data.peaks[i].day == 0) {
|
||||
if(val > data.peaks[i].value) {
|
||||
data.peaks[i].day = day;
|
||||
data.peaks[i].hour = hour;
|
||||
data.peaks[i].value = val;
|
||||
return true;
|
||||
}
|
||||
@@ -512,3 +550,8 @@ bool EnergyAccounting::updateMax(uint16_t val, uint8_t day, uint8_t hour) {
|
||||
void EnergyAccounting::setCurrency(String currency) {
|
||||
this->currency = currency;
|
||||
}
|
||||
|
||||
float EnergyAccounting::getPriceForHour(uint8_t d, uint8_t h) {
|
||||
if(ps == NULL) return PRICE_NO_VALUE;
|
||||
return ps->getValueForHour(d, h);
|
||||
}
|
||||
@@ -15,19 +15,19 @@
|
||||
class HomeAssistantMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#else
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw, AmsFirmwareUpdater* updater, char* hostname) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
HomeAssistantMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, uint8_t boardType, HomeAssistantConfig config, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
#endif
|
||||
this->boardType = boardType;
|
||||
this->hw = hw;
|
||||
setHomeAssistantConfig(config, hostname);
|
||||
setHomeAssistantConfig(config);
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishRaw(String data);
|
||||
bool publishFirmware();
|
||||
|
||||
bool postConnect();
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
|
||||
uint8_t getFormat();
|
||||
|
||||
void setHomeAssistantConfig(HomeAssistantConfig config, char* hostname);
|
||||
void setHomeAssistantConfig(HomeAssistantConfig config);
|
||||
private:
|
||||
uint8_t boardType;
|
||||
|
||||
@@ -51,9 +51,9 @@ private:
|
||||
String updateTopic;
|
||||
String sensorNamePrefix;
|
||||
|
||||
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit, dInit;
|
||||
bool l1Init, l2Init, l2eInit, l3Init, l3eInit, l4Init, l4eInit, rtInit, rteInit, pInit, sInit, rInit, fInit;
|
||||
bool tInit[32] = {false};
|
||||
uint8_t priceImportInit = 0, priceExportInit = 0;
|
||||
bool prInit[38] = {false};
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
|
||||
HwTools* hw;
|
||||
@@ -79,7 +79,6 @@ private:
|
||||
void publishPriceSensors(PriceService* ps);
|
||||
void publishSystemSensors();
|
||||
void publishThresholdSensors();
|
||||
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
|
||||
|
||||
String boardTypeToString(uint8_t b) {
|
||||
switch(b) {
|
||||
|
||||
@@ -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,7 +2,8 @@
|
||||
"name" : "%s%s",
|
||||
"stat_t" : "%s%s",
|
||||
"uniq_id" : "%s_%s",
|
||||
"default_entity_id" : "sensor.%s_%s",
|
||||
"obj_id" : "%s_%s",
|
||||
"unit_of_meas" : "%s",
|
||||
"val_tpl" : "{{ value_json.%s | is_defined }}",
|
||||
"expire_after" : %d,
|
||||
"dev" : {
|
||||
@@ -12,8 +13,5 @@
|
||||
"sw" : "%s",
|
||||
"mf" : "%s",
|
||||
"cu" : "%s"
|
||||
}
|
||||
%s%s%s
|
||||
%s%s%s
|
||||
%s%s%s
|
||||
}%s%s%s%s%s%s
|
||||
}
|
||||
@@ -19,8 +19,8 @@
|
||||
#include <esp_task_wdt.h>
|
||||
#endif
|
||||
|
||||
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config, char* hostname) {
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
|
||||
void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config) {
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = false;
|
||||
|
||||
if(strlen(config.discoveryNameTag) > 0) {
|
||||
snprintf_P(json, 128, PSTR("AMS reader (%s)"), config.discoveryNameTag);
|
||||
@@ -28,18 +28,21 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
|
||||
snprintf_P(json, 128, PSTR("[%s] "), config.discoveryNameTag);
|
||||
sensorNamePrefix = String(json);
|
||||
} else {
|
||||
snprintf_P(json, 128, PSTR("AMS reader"));
|
||||
deviceName = String(json);
|
||||
deviceName = F("AMS reader");
|
||||
sensorNamePrefix = "";
|
||||
}
|
||||
deviceModel = boardTypeToString(boardType);
|
||||
manufacturer = boardManufacturerToString(boardType);
|
||||
|
||||
deviceUid = String(hostname); // Maybe configurable in the future?
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
char hostname[32];
|
||||
#if defined(ESP8266)
|
||||
strcpy(hostname, WiFi.hostname().c_str());
|
||||
#elif defined(ESP32)
|
||||
strcpy(hostname, WiFi.getHostname());
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Hostname is [%s]\n"), hostname);
|
||||
|
||||
stripNonAscii((uint8_t*) hostname, 32, false);
|
||||
deviceUid = String(hostname); // Maybe configurable in the future?
|
||||
|
||||
if(strlen(config.discoveryHostname) > 0) {
|
||||
if(strncmp_P(config.discoveryHostname, PSTR("http"), 4) == 0) {
|
||||
@@ -53,42 +56,36 @@ void HomeAssistantMqttHandler::setHomeAssistantConfig(HomeAssistantConfig config
|
||||
deviceUrl = String(json);
|
||||
}
|
||||
|
||||
if(strlen(config.discoveryPrefix) == 0) {
|
||||
snprintf_P(config.discoveryPrefix, 64, PSTR("homeassistant"));
|
||||
if(strlen(config.discoveryPrefix) > 0) {
|
||||
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
|
||||
statusTopic = String(json);
|
||||
|
||||
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
|
||||
sensorTopic = String(json);
|
||||
|
||||
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
|
||||
updateTopic = String(json);
|
||||
} else {
|
||||
statusTopic = F("homeassistant/status");
|
||||
sensorTopic = F("homeassistant/sensor");
|
||||
updateTopic = F("homeassistant/update");
|
||||
}
|
||||
|
||||
snprintf_P(json, 128, PSTR("%s/status"), config.discoveryPrefix);
|
||||
statusTopic = String(json);
|
||||
|
||||
snprintf_P(json, 128, PSTR("%s/sensor"), config.discoveryPrefix);
|
||||
sensorTopic = String(json);
|
||||
|
||||
snprintf_P(json, 128, PSTR("%s/update"), config.discoveryPrefix);
|
||||
updateTopic = String(json);
|
||||
strcpy(this->mqttConfig.subscribeTopic, statusTopic.c_str());
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::postConnect() {
|
||||
bool ret = true;
|
||||
if(!statusTopic.isEmpty()) {
|
||||
if(mqtt.subscribe(statusTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Subscribed to [%s]\n"), statusTopic.c_str());
|
||||
} else {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Unable to subscribe to [%s]\n"), statusTopic.c_str());
|
||||
ret = false;
|
||||
}
|
||||
if(!subTopic.isEmpty() && !mqtt.subscribe(subTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::ERROR))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" Unable to subscribe to to [%s]\n"), subTopic.c_str());
|
||||
return false;
|
||||
}
|
||||
return ret;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
if(time(nullptr) < FirmwareVersion::BuildEpoch)
|
||||
@@ -133,7 +130,12 @@ bool HomeAssistantMqttHandler::publishList1(AmsData* data, EnergyAccounting* ea)
|
||||
publishList1Sensors();
|
||||
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
memset(pt, 0, 24);
|
||||
if(data->getPackageTimestamp() > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(data->getPackageTimestamp(), tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
snprintf_P(json, BufferSize, HA1_JSON, data->getActiveImportPower(), pt);
|
||||
return mqtt.publish(pubTopic + "/power", json);
|
||||
@@ -144,7 +146,12 @@ bool HomeAssistantMqttHandler::publishList2(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getActiveExportPower() > 0) publishList2ExportSensors();
|
||||
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
memset(pt, 0, 24);
|
||||
if(data->getPackageTimestamp() > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(data->getPackageTimestamp(), tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
snprintf_P(json, BufferSize, HA3_JSON,
|
||||
data->getListId().c_str(),
|
||||
@@ -170,11 +177,20 @@ bool HomeAssistantMqttHandler::publishList3(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getActiveExportCounter() > 0.0) publishList3ExportSensors();
|
||||
|
||||
char mt[24];
|
||||
toJsonIsoTimestamp(data->getMeterTimestamp(), mt, sizeof(mt));
|
||||
memset(mt, 0, 24);
|
||||
if(data->getMeterTimestamp() > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(data->getMeterTimestamp(), tm);
|
||||
sprintf_P(mt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
char pt[24];
|
||||
memset(pt, 0, 24);
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
if(data->getPackageTimestamp() > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(data->getPackageTimestamp(), tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
snprintf_P(json, BufferSize, HA2_JSON,
|
||||
data->getActiveImportCounter(),
|
||||
@@ -192,7 +208,12 @@ bool HomeAssistantMqttHandler::publishList4(AmsData* data, EnergyAccounting* ea)
|
||||
if(data->getL1ActiveExportPower() > 0 || data->getL2ActiveExportPower() > 0 || data->getL3ActiveExportPower() > 0) publishList4ExportSensors();
|
||||
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(data->getPackageTimestamp(), pt, sizeof(pt));
|
||||
memset(pt, 0, 24);
|
||||
if(data->getPackageTimestamp() > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(data->getPackageTimestamp(), tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
snprintf_P(json, BufferSize, HA4_JSON,
|
||||
data->getListId().c_str(),
|
||||
@@ -282,8 +303,13 @@ bool HomeAssistantMqttHandler::publishRealtime(AmsData* data, EnergyAccounting*
|
||||
|
||||
time_t now = time(nullptr);
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
|
||||
memset(pt, 0, 24);
|
||||
if(now > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(now, tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
|
||||
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
@@ -313,8 +339,13 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
|
||||
|
||||
time_t now = time(nullptr);
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
|
||||
memset(pt, 0, 24);
|
||||
if(now > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(now, tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("}"));
|
||||
|
||||
bool ret = mqtt.publish(pubTopic + "/temperatures", json);
|
||||
@@ -323,9 +354,9 @@ bool HomeAssistantMqttHandler::publishTemperatures(AmsConfiguration* config, HwT
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
if(!ps->hasPrice())
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
return false;
|
||||
|
||||
publishPriceSensors(ps);
|
||||
@@ -338,7 +369,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@@ -386,73 +417,59 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
memset(ts1hr, 0, 24);
|
||||
if(min1hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
|
||||
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts3hr[24];
|
||||
memset(ts3hr, 0, 24);
|
||||
if(min3hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
|
||||
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts6hr[24];
|
||||
memset(ts6hr, 0, 24);
|
||||
if(min6hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{"), WiFi.macAddress().c_str());
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":null,"), i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(rteInit && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%d\":%.4f,"), i, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}"),
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":\"%s\",\"cheapest3hr\":\"%s\",\"cheapest6hr\":\"%s\"}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":%s"), pt);
|
||||
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_EXPORT, now, 0);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":null}"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"exportprices\":{\"0\":%.4f}"), val);
|
||||
}
|
||||
|
||||
char pt[24];
|
||||
memset(pt, 0, 24);
|
||||
if(now > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(now, tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR(",\"t\":\"%s\""), pt);
|
||||
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
@@ -463,7 +480,7 @@ bool HomeAssistantMqttHandler::publishPrices(PriceService* ps) {
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
|
||||
if(pubTopic.isEmpty() || !connected())
|
||||
if(pubTopic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
publishSystemSensors();
|
||||
@@ -471,9 +488,14 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
|
||||
|
||||
time_t now = time(nullptr);
|
||||
char pt[24];
|
||||
toJsonIsoTimestamp(now, pt, sizeof(pt));
|
||||
memset(pt, 0, 24);
|
||||
if(now > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(now, tm);
|
||||
sprintf_P(pt, PSTR("%04d-%02d-%02dT%02d:%02d:%02dZ"), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
}
|
||||
|
||||
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":%s}"),
|
||||
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\",\"t\":\"%s\"}"),
|
||||
WiFi.macAddress().c_str(),
|
||||
mqttConfig.clientId,
|
||||
(uint32_t) (millis64()/1000),
|
||||
@@ -489,22 +511,18 @@ bool HomeAssistantMqttHandler::publishSystem(HwTools* hw, PriceService* ps, Ener
|
||||
}
|
||||
|
||||
void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
|
||||
String uid;
|
||||
if(strlen(sensor.uid) > 0) {
|
||||
uid = String(sensor.uid);
|
||||
} else {
|
||||
uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
}
|
||||
String uid = String(sensor.path);
|
||||
uid.replace(".", "");
|
||||
uid.replace("[", "");
|
||||
uid.replace("]", "");
|
||||
uid.replace("'", "");
|
||||
snprintf_P(json, BufferSize, HADISCOVER_JSON,
|
||||
sensorNamePrefix.c_str(),
|
||||
sensor.name,
|
||||
mqttConfig.publishTopic, sensor.topic,
|
||||
deviceUid.c_str(), uid.c_str(),
|
||||
deviceUid.c_str(), uid.c_str(),
|
||||
sensor.uom,
|
||||
sensor.path,
|
||||
sensor.ttl,
|
||||
deviceUid.c_str(),
|
||||
@@ -513,21 +531,14 @@ void HomeAssistantMqttHandler::publishSensor(const HomeAssistantSensor sensor) {
|
||||
FirmwareVersion::VersionString,
|
||||
manufacturer.c_str(),
|
||||
deviceUrl.c_str(),
|
||||
|
||||
strlen_P(sensor.devcl) > 0 ? ",\"dev_cla\":\"" : "",
|
||||
strlen_P(sensor.devcl) > 0 ? (char *) FPSTR(sensor.devcl) : "",
|
||||
strlen_P(sensor.devcl) > 0 ? "\"" : "",
|
||||
|
||||
strlen_P(sensor.stacl) > 0 ? ",\"stat_cla\":\"" : "",
|
||||
strlen_P(sensor.stacl) > 0 ? (char *) FPSTR(sensor.stacl) : "",
|
||||
strlen_P(sensor.stacl) > 0 ? "\"" : "",
|
||||
|
||||
strlen_P(sensor.uom) > 0 ? ",\"unit_of_meas\":\"" : "",
|
||||
strlen_P(sensor.uom) > 0 ? (char *) FPSTR(sensor.uom) : "",
|
||||
strlen_P(sensor.uom) > 0 ? "\"" : ""
|
||||
strlen_P(sensor.stacl) > 0 ? "\"" : ""
|
||||
);
|
||||
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid + "/config", json, true, 0);
|
||||
mqtt.publish(sensorTopic + "/" + deviceUid + "_" + uid.c_str() + "/config", json, true, 0);
|
||||
loop();
|
||||
}
|
||||
|
||||
@@ -616,8 +627,7 @@ void HomeAssistantMqttHandler::publishRealtimeSensors(EnergyAccounting* ea, Pric
|
||||
RealtimePeakSensor.ttl,
|
||||
RealtimePeakSensor.uom,
|
||||
RealtimePeakSensor.devcl,
|
||||
RealtimePeakSensor.stacl,
|
||||
RealtimePeakSensor.uid
|
||||
RealtimePeakSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@@ -656,8 +666,7 @@ void HomeAssistantMqttHandler::publishTemperatureSensor(uint8_t index, String id
|
||||
TemperatureSensor.ttl,
|
||||
TemperatureSensor.uom,
|
||||
TemperatureSensor.devcl,
|
||||
TemperatureSensor.stacl,
|
||||
TemperatureSensor.uid
|
||||
TemperatureSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
tInit[index] = true;
|
||||
@@ -677,96 +686,45 @@ void HomeAssistantMqttHandler::publishPriceSensors(PriceService* ps) {
|
||||
}
|
||||
pInit = true;
|
||||
}
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
|
||||
if(priceImportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t importPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(importPriceSensorNo < priceImportInit) {
|
||||
importPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.import[%d]"), importPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("prices%d"), importPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Import price in %02d hour%s"), importPriceSensorNo, importPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Import price in %03d minutes"), importPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
importPriceSensorNo == 0 ? "Current import price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
importPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceImportInit = importPriceSensorNo++;
|
||||
}
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
if(prInit[i]) continue;
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) continue;
|
||||
|
||||
char name[strlen(PriceSensor.name)+2];
|
||||
snprintf(name, strlen(PriceSensor.name)+2, PriceSensor.name, i, i == 1 ? "hour" : "hours");
|
||||
char path[strlen(PriceSensor.path)+1];
|
||||
snprintf(path, strlen(PriceSensor.path)+1, PriceSensor.path, i);
|
||||
HomeAssistantSensor sensor = {
|
||||
i == 0 ? "Price current hour" : name,
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
i == 0 ? "total" : PriceSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
prInit[i] = true;
|
||||
}
|
||||
|
||||
if(priceExportInit < numberOfPoints-currentPricePointIndex) {
|
||||
uint8_t exportPriceSensorNo = 0;
|
||||
for(int pricePointIndex = currentPricePointIndex; pricePointIndex < numberOfPoints; pricePointIndex++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, pricePointIndex);
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
if(exportPriceSensorNo < priceExportInit) {
|
||||
exportPriceSensorNo++;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t resolution = ps->getResolutionInMinutes();
|
||||
|
||||
char path[64];
|
||||
memset(path, 0, 64);
|
||||
snprintf_P(path, 64, PSTR("prices.export[%d]"), exportPriceSensorNo);
|
||||
|
||||
char uid[32];
|
||||
memset(uid, 0, 32);
|
||||
snprintf_P(uid, 32, PSTR("exportprices%d"), exportPriceSensorNo);
|
||||
|
||||
char name[64];
|
||||
if(resolution == 60)
|
||||
snprintf_P(name, 64, PSTR("Export price in %02d hour%s"), exportPriceSensorNo, exportPriceSensorNo == 1 ? "" : "s");
|
||||
else
|
||||
snprintf_P(name, 64, PSTR("Export price in %03d minutes"), exportPriceSensorNo * resolution);
|
||||
|
||||
HomeAssistantSensor sensor = {
|
||||
exportPriceSensorNo == 0 ? "Current export price" : name,
|
||||
"/prices",
|
||||
path,
|
||||
resolution * 60 + 300,
|
||||
uom.c_str(),
|
||||
"monetary",
|
||||
exportPriceSensorNo == 0 ? "total" : "",
|
||||
uid
|
||||
};
|
||||
publishSensor(sensor);
|
||||
|
||||
priceExportInit = exportPriceSensorNo++;
|
||||
}
|
||||
float exportPrice = ps->getValueForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
if(exportPrice != PRICE_NO_VALUE) {
|
||||
char path[20];
|
||||
snprintf(path, 20, "exportprices['%d']", 0);
|
||||
HomeAssistantSensor sensor = {
|
||||
"Export price current hour",
|
||||
PriceSensor.topic,
|
||||
path,
|
||||
PriceSensor.ttl,
|
||||
uom.c_str(),
|
||||
PriceSensor.devcl,
|
||||
"total"
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void HomeAssistantMqttHandler::publishSystemSensors() {
|
||||
if(sInit) return;
|
||||
for(uint8_t i = 0; i < SystemSensorCount; i++) {
|
||||
@@ -789,8 +747,7 @@ void HomeAssistantMqttHandler::publishThresholdSensors() {
|
||||
RealtimeThresholdSensor.ttl,
|
||||
RealtimeThresholdSensor.uom,
|
||||
RealtimeThresholdSensor.devcl,
|
||||
RealtimeThresholdSensor.stacl,
|
||||
RealtimeThresholdSensor.uid
|
||||
RealtimeThresholdSensor.stacl
|
||||
};
|
||||
publishSensor(sensor);
|
||||
}
|
||||
@@ -801,34 +758,15 @@ uint8_t HomeAssistantMqttHandler::getFormat() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishRaw(uint8_t* raw, size_t length) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
if(length <= 0 || length > BufferSize) return false;
|
||||
|
||||
if(!dInit) {
|
||||
// Not sure how this sensor should be defined in HA, so skipping for now
|
||||
//publishSensor(DataSensor);
|
||||
dInit = true;
|
||||
}
|
||||
|
||||
String str = toHex(raw, length);
|
||||
|
||||
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
|
||||
char topic[192];
|
||||
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
|
||||
bool ret = mqtt.publish(topic, json);
|
||||
loop();
|
||||
return ret;
|
||||
bool HomeAssistantMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HomeAssistantMqttHandler::publishFirmware() {
|
||||
if(!fInit) {
|
||||
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"uniq_id\":\"%s_fwupgrade\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
|
||||
snprintf_P(json, BufferSize, PSTR("{\"name\":\"%sFirmware\",\"stat_t\":\"%s/firmware\",\"dev_cla\":\"firmware\",\"cmd_t\":\"%s\",\"pl_inst\":\"fwupgrade\"}"),
|
||||
sensorNamePrefix.c_str(),
|
||||
pubTopic.c_str(),
|
||||
deviceUid.c_str(),
|
||||
subTopic.c_str()
|
||||
);
|
||||
fInit = mqtt.publish(updateTopic + "/" + deviceUid + "/config", json, true, 0);
|
||||
@@ -853,10 +791,9 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Received online status from HA, resetting sensor status\n"));
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = fInit = dInit = false;
|
||||
l1Init = l2Init = l2eInit = l3Init = l3eInit = l4Init = l4eInit = rtInit = rteInit = pInit = sInit = rInit = false;
|
||||
for(uint8_t i = 0; i < 32; i++) tInit[i] = false;
|
||||
priceImportInit = 0;
|
||||
priceExportInit = 0;
|
||||
for(uint8_t i = 0; i < 38; i++) prInit[i] = false;
|
||||
}
|
||||
} else if(topic.equals(subTopic)) {
|
||||
if(payload.equals("fwupgrade")) {
|
||||
@@ -866,14 +803,3 @@ void HomeAssistantMqttHandler::onMessage(String &topic, String &payload) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HomeAssistantMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
|
||||
memset(buf, 0, buflen);
|
||||
if(t > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(t, tm);
|
||||
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
} else {
|
||||
snprintf_P(buf, buflen, PSTR("null"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ public:
|
||||
bool applyBoardConfig(uint8_t boardType, GpioConfig& gpioConfig, MeterConfig& meterConfig, uint8_t hanPin);
|
||||
void setup(SystemConfig* sys, GpioConfig* gpio);
|
||||
float getVcc();
|
||||
void setMaxVcc(float maxVcc);
|
||||
uint8_t getTempSensorCount();
|
||||
TempSensorData* getTempSensorData(uint8_t);
|
||||
bool updateTemperatures();
|
||||
@@ -68,9 +67,7 @@ private:
|
||||
bool ledInvert, rgbInvert;
|
||||
uint8_t vccPin, vccGnd_r, vccVcc_r;
|
||||
float vccOffset, vccMultiplier;
|
||||
float vcc = 3.3; // Last known Vcc
|
||||
float maxVcc = 3.28; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
|
||||
unsigned long lastVccRead = 0;
|
||||
float maxVcc = 3.25; // Best to have this close to max as a start, in case Pow-U reboots and starts off with a low voltage, we dont want that to be perceived as max
|
||||
|
||||
uint16_t analogRange = 1024;
|
||||
AdcConfig voltAdc, tempAdc;
|
||||
|
||||
@@ -657,12 +657,8 @@ bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
|
||||
}
|
||||
|
||||
bool HwTools::isVoltageOptimal(float range) {
|
||||
if(boardType >= 1 && boardType <= 8 && maxVcc > 2.8) { // BUS-Power boards
|
||||
unsigned long now = millis();
|
||||
if(now - lastVccRead > 250) {
|
||||
vcc = getVcc();
|
||||
lastVccRead = now;
|
||||
}
|
||||
if(boardType >= 5 && boardType <= 7 && maxVcc > 2.8) { // Pow-*
|
||||
float vcc = getVcc();
|
||||
if(vcc > 3.4 || vcc < 2.8) {
|
||||
maxVcc = 0; // Voltage is outside the operating range, we have to assume voltage is OK
|
||||
} else if(vcc > maxVcc) {
|
||||
@@ -677,8 +673,4 @@ bool HwTools::isVoltageOptimal(float range) {
|
||||
|
||||
uint8_t HwTools::getBoardType() {
|
||||
return boardType;
|
||||
}
|
||||
|
||||
void HwTools::setMaxVcc(float vcc) {
|
||||
this->maxVcc = min(3.3f, vcc);
|
||||
}
|
||||
@@ -12,18 +12,19 @@
|
||||
class JsonMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#else
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw, AmsDataStorage* ds, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
#endif
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, RemoteDebug* debugger, char* buf, HwTools* hw, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
this->hw = hw;
|
||||
this->ds = ds;
|
||||
};
|
||||
#else
|
||||
JsonMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, HwTools* hw) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
this->hw = hw;
|
||||
};
|
||||
#endif
|
||||
bool publish(AmsData* data, AmsData* previousState, EnergyAccounting* ea, PriceService* ps);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishRaw(String data);
|
||||
bool publishFirmware();
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
@@ -32,9 +33,6 @@ public:
|
||||
|
||||
private:
|
||||
HwTools* hw;
|
||||
bool hasExport = false;
|
||||
AmsDataStorage* ds;
|
||||
|
||||
uint16_t appendJsonHeader(AmsData* data);
|
||||
uint16_t appendJsonFooter(EnergyAccounting* ea, uint16_t pos);
|
||||
bool publishList1(AmsData* data, EnergyAccounting* ea);
|
||||
@@ -42,6 +40,5 @@ private:
|
||||
bool publishList3(AmsData* data, EnergyAccounting* ea);
|
||||
bool publishList4(AmsData* data, EnergyAccounting* ea);
|
||||
String getMeterModel(AmsData* data);
|
||||
void toJsonIsoTimestamp(time_t t, char* buf, size_t buflen);
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -8,13 +8,12 @@
|
||||
#include "FirmwareVersion.h"
|
||||
#include "hexutils.h"
|
||||
#include "Uptime.h"
|
||||
#include "AmsJsonGenerator.h"
|
||||
|
||||
bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0) {
|
||||
return false;
|
||||
}
|
||||
if(!connected()) {
|
||||
if(!mqtt.connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -45,15 +44,6 @@ bool JsonMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcc
|
||||
ret = publishList4(&data, ea);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
loop();
|
||||
return ret;
|
||||
}
|
||||
@@ -77,24 +67,14 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
|
||||
} else {
|
||||
memset(pf, 0, 4);
|
||||
}
|
||||
|
||||
String peaks = "";
|
||||
uint8_t peakCount = ea->getConfig()->hours;
|
||||
if(peakCount > 5) peakCount = 5;
|
||||
for(uint8_t i = 1; i <= peakCount; i++) {
|
||||
if(!peaks.isEmpty()) peaks += ",";
|
||||
peaks += String(ea->getPeak(i).value / 100.0, 2);
|
||||
}
|
||||
|
||||
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.3f,\"%sd\":%.2f,\"%sm\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.3f,\"%sde\":%.2f,\"%sme\":%.1f,\"peaks\":[%s]%s"),
|
||||
return snprintf_P(json+pos, BufferSize-pos, PSTR("%s\"%sh\":%.2f,\"%sd\":%.1f,\"%st\":%d,\"%sx\":%.2f,\"%she\":%.2f,\"%sde\":%.1f%s"),
|
||||
strlen(pf) == 0 ? "},\"realtime\":{" : ",",
|
||||
pf,
|
||||
ea->getUseThisHour(),
|
||||
pf,
|
||||
ea->getUseToday(),
|
||||
pf,
|
||||
ea->getUseThisMonth(),
|
||||
pf,
|
||||
ea->getCurrentThreshold(),
|
||||
pf,
|
||||
ea->getMonthMax(),
|
||||
@@ -102,9 +82,6 @@ uint16_t JsonMqttHandler::appendJsonFooter(EnergyAccounting* ea, uint16_t pos) {
|
||||
ea->getProducedThisHour(),
|
||||
pf,
|
||||
ea->getProducedToday(),
|
||||
pf,
|
||||
ea->getProducedThisMonth(),
|
||||
peaks.c_str(),
|
||||
strlen(pf) == 0 ? "}" : ""
|
||||
);
|
||||
}
|
||||
@@ -295,9 +272,9 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
return false;
|
||||
if(!ps->hasPrice())
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@@ -308,7 +285,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[38];
|
||||
for(int i = 0;i < 38; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 38; i++) {
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == PRICE_NO_VALUE) break;
|
||||
@@ -356,89 +333,59 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
memset(ts1hr, 0, 24);
|
||||
if(min1hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts1hr, sizeof(ts1hr));
|
||||
sprintf_P(ts1hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts3hr[24];
|
||||
memset(ts3hr, 0, 24);
|
||||
if(min3hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts3hr, sizeof(ts3hr));
|
||||
sprintf_P(ts3hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts6hr[24];
|
||||
memset(ts6hr, 0, 24);
|
||||
if(min6hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
|
||||
tmElements_t tm;
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
tm.Minute = 0;
|
||||
tm.Second = 0;
|
||||
ts = makeTime(tm);
|
||||
toJsonIsoTimestamp(ts, ts6hr, sizeof(ts6hr));
|
||||
sprintf_P(ts6hr, PSTR("%04d-%02d-%02dT%02d:00:00Z"), tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
if(mqttConfig.payloadFormat == 6) {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
|
||||
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":null,"), i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_%d\":%.4f,"), i, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"pr_min\":%.4f,\"pr_max\":%.4f,\"pr_cheapest1hr\":%s,\"pr_cheapest3hr\":%s,\"pr_cheapest6hr\":%s}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
char pf[4];
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\","), WiFi.macAddress().c_str());
|
||||
if(mqttConfig.payloadFormat != 6) {
|
||||
memset(pf, 0, 4);
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"prices\":{"));
|
||||
} else {
|
||||
uint16_t pos = snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"prices\":{\"import\":["), WiFi.macAddress().c_str());
|
||||
strcpy_P(pf, PSTR("pr_"));
|
||||
}
|
||||
|
||||
uint8_t currentPricePointIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"export\":["));
|
||||
for(int i = currentPricePointIndex; i < numberOfPoints; i++) {
|
||||
float val = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("null,"));
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("%.4f,"), val);
|
||||
}
|
||||
}
|
||||
for(uint8_t i = 0;i < 38; i++) {
|
||||
if(values[i] == PRICE_NO_VALUE) {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":null,"), pf, i);
|
||||
} else {
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%s%d\":%.4f,"), pf, i, values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
pos--;
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("],\"min\":%.4f,\"max\":%.4f,\"cheapest1hr\":%s,\"cheapest3hr\":%s,\"cheapest6hr\":%s}}"),
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
|
||||
pos += snprintf_P(json+pos, BufferSize-pos, PSTR("\"%smin\":%.4f,\"%smax\":%.4f,\"%scheapest1hr\":\"%s\",\"%scheapest3hr\":\"%s\",\"%scheapest6hr\":\"%s\"}"),
|
||||
pf,
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
pf,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
pf,
|
||||
ts1hr,
|
||||
pf,
|
||||
ts3hr,
|
||||
pf,
|
||||
ts6hr
|
||||
);
|
||||
if(mqttConfig.payloadFormat != 6) {
|
||||
json[pos++] = '}';
|
||||
json[pos] = '\0';
|
||||
}
|
||||
bool ret = false;
|
||||
if(mqttConfig.payloadFormat == 5) {
|
||||
@@ -453,7 +400,7 @@ bool JsonMqttHandler::publishPrices(PriceService* ps) {
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
snprintf_P(json, BufferSize, PSTR("{\"id\":\"%s\",\"name\":\"%s\",\"up\":%d,\"vcc\":%.3f,\"rssi\":%d,\"temp\":%.2f,\"version\":\"%s\"}"),
|
||||
@@ -481,20 +428,8 @@ uint8_t JsonMqttHandler::getFormat() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishRaw(uint8_t* raw, size_t length) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
if(length <= 0 || length > BufferSize) return false;
|
||||
|
||||
String str = toHex(raw, length);
|
||||
|
||||
snprintf_P(json, BufferSize, PSTR("{\"data\":\"%s\"}"), str.c_str());
|
||||
char topic[192];
|
||||
snprintf_P(topic, 192, PSTR("%s/data"), mqttConfig.publishTopic);
|
||||
bool ret = mqtt.publish(topic, json);
|
||||
loop();
|
||||
return ret;
|
||||
bool JsonMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishFirmware() {
|
||||
@@ -512,46 +447,11 @@ bool JsonMqttHandler::publishFirmware() {
|
||||
}
|
||||
|
||||
void JsonMqttHandler::onMessage(String &topic, String &payload) {
|
||||
if(strlen(mqttConfig.publishTopic) == 0 || !connected())
|
||||
return;
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Received command [%s] to [%s]\n"), payload.c_str(), topic.c_str());
|
||||
|
||||
if(topic.equals(subTopic)) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR(" - this is our subscribed topic\n"));
|
||||
if(payload.equals("fwupgrade")) {
|
||||
if(strcmp(updater->getNextVersion(), FirmwareVersion::VersionString) != 0) {
|
||||
updater->setTargetVersion(updater->getNextVersion());
|
||||
}
|
||||
} else if(payload.equals("dayplot")) {
|
||||
char pubTopic[192];
|
||||
snprintf_P(pubTopic, 192, PSTR("%s/dayplot"), mqttConfig.publishTopic);
|
||||
AmsJsonGenerator::generateDayPlotJson(ds, json, BufferSize);
|
||||
bool ret = mqtt.publish(pubTopic, json);
|
||||
loop();
|
||||
} else if(payload.equals("monthplot")) {
|
||||
char pubTopic[192];
|
||||
snprintf_P(pubTopic, 192, PSTR("%s/monthplot"), mqttConfig.publishTopic);
|
||||
AmsJsonGenerator::generateMonthPlotJson(ds, json, BufferSize);
|
||||
bool ret = mqtt.publish(pubTopic, json);
|
||||
loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JsonMqttHandler::toJsonIsoTimestamp(time_t t, char* buf, size_t buflen) {
|
||||
memset(buf, 0, buflen);
|
||||
if(t > 0) {
|
||||
tmElements_t tm;
|
||||
breakTime(t, tm);
|
||||
snprintf_P(buf, buflen, PSTR("\"%04d-%02d-%02dT%02d:%02d:%02dZ\""), tm.Year+1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);
|
||||
} else {
|
||||
snprintf_P(buf, buflen, PSTR("null"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ struct AmsOctetTimestamp {
|
||||
class IEC6205675 : public AmsData {
|
||||
public:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger);
|
||||
#else
|
||||
IEC6205675(const char* payload, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger);
|
||||
#endif
|
||||
|
||||
private:
|
||||
@@ -37,9 +37,8 @@ private:
|
||||
float getNumber(uint8_t* obis, int matchlength, const char* ptr);
|
||||
float getNumber(CosemData*);
|
||||
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
|
||||
time_t adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType);
|
||||
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
uint8_t AMS_OBIS_UNKNOWN_1[4] = { 25, 9, 0, 255 };
|
||||
|
||||
uint8_t AMS_OBIS_VERSION[4] = { 0, 2, 129, 255 };
|
||||
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };
|
||||
|
||||
@@ -12,22 +12,7 @@
|
||||
#include "DataParser.h"
|
||||
#include "Cosem.h"
|
||||
|
||||
struct Lng2Data_3p_0b {
|
||||
CosemBasic header;
|
||||
CosemLongUnsigned u1;
|
||||
CosemLongUnsigned u2;
|
||||
CosemLongUnsigned u3;
|
||||
CosemLongUnsigned i1;
|
||||
CosemLongUnsigned i2;
|
||||
CosemLongUnsigned i3;
|
||||
CosemDLongUnsigned activeImport;
|
||||
CosemDLongUnsigned activeExport;
|
||||
CosemDLongUnsigned acumulatedImport;
|
||||
CosemDLongUnsigned accumulatedExport;
|
||||
CosemString meterId;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct Lng2Data_3p_0e {
|
||||
struct Lng2Data_3p {
|
||||
CosemBasic header;
|
||||
CosemLongUnsigned u1;
|
||||
CosemLongUnsigned u2;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#endif
|
||||
#include "AmsData.h"
|
||||
#include "AmsConfiguration.h"
|
||||
#include "AmsMqttHandler.h"
|
||||
|
||||
class MeterCommunicator {
|
||||
public:
|
||||
@@ -25,13 +24,6 @@ public:
|
||||
virtual bool isConfigChanged();
|
||||
virtual void ackConfigChanged();
|
||||
virtual void getCurrentConfig(MeterConfig& meterConfig);
|
||||
virtual void setMqttHandlerForDebugging(AmsMqttHandler* mqttHandler) {
|
||||
this->mqttDebug = mqttHandler;
|
||||
};
|
||||
|
||||
protected:
|
||||
AmsMqttHandler* mqttDebug = NULL;
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "AmsConfiguration.h"
|
||||
#include "DataParsers.h"
|
||||
#include "Timezone.h"
|
||||
#include "AmsMqttHandler.h"
|
||||
#include "PassthroughMqttHandler.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include "SoftwareSerial.h"
|
||||
@@ -36,9 +36,7 @@ public:
|
||||
bool isConfigChanged();
|
||||
void ackConfigChanged();
|
||||
void getCurrentConfig(MeterConfig& meterConfig);
|
||||
void setTimezone(Timezone* tz) {
|
||||
this->tz = tz;
|
||||
};
|
||||
void setPassthroughMqttHandler(PassthroughMqttHandler*);
|
||||
|
||||
HardwareSerial* getHwSerial();
|
||||
void rxerr(int err);
|
||||
@@ -53,6 +51,8 @@ protected:
|
||||
bool configChanged = false;
|
||||
Timezone* tz;
|
||||
|
||||
PassthroughMqttHandler* pt = NULL;
|
||||
|
||||
uint8_t *hanBuffer = NULL;
|
||||
uint16_t hanBufferSize = 0;
|
||||
Stream *hanSerial;
|
||||
|
||||
@@ -12,219 +12,33 @@
|
||||
#include "hexutils.h"
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
|
||||
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, RemoteDebug* debugger) {
|
||||
#else
|
||||
IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
|
||||
IEC6205675::IEC6205675(const char* payload, uint8_t useMeterType, MeterConfig* meterConfig, DataParserContext &ctx, AmsData &state, Stream* debugger) {
|
||||
#endif
|
||||
float val;
|
||||
char str[64];
|
||||
|
||||
this->packageTimestamp = time(nullptr); // ctx.timestamp is mostly garbage, so we use current time as package timestamp
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
Timezone tz(CEST, CET);
|
||||
|
||||
this->packageTimestamp = ctx.timestamp == 0 ? time(nullptr) : ctx.timestamp;
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
|
||||
if(val == NOVALUE) {
|
||||
CosemData* data = getCosemDataAt(1, ((char *) (d)));
|
||||
|
||||
if(useMeterType == AmsTypeIskra) { // Iskra special case
|
||||
meterType = AmsTypeIskra;
|
||||
uint8_t idx = 0;
|
||||
data = getCosemDataAt(idx, ((char *) (d)));
|
||||
if(data->base.length == 0x21) {
|
||||
idx = 4;
|
||||
|
||||
// 1.8.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
// 1.8.1
|
||||
// 1.8.2
|
||||
idx += 2;
|
||||
|
||||
// 2.8.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
// 2.8.1
|
||||
// 2.8.2
|
||||
idx += 2;
|
||||
|
||||
// 5.8.0
|
||||
// 6.8.0
|
||||
// 7.8.0
|
||||
// 8.8.0
|
||||
idx += 4;
|
||||
|
||||
// 1.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 2.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 13.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
powerFactor= ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
// 21.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1activeImportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 41.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2activeImportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 61.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3activeImportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 22.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 42.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 62.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 32.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1voltage = ntohs(data->lu.data) / 10.0;
|
||||
|
||||
// 52.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2voltage = ntohs(data->lu.data) / 10.0;
|
||||
|
||||
// 72.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3voltage = ntohs(data->lu.data) / 10.0;
|
||||
|
||||
// 31.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
// 51.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
// 71.7.0
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
listType = 4;
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(data->base.length == 0x0F) {
|
||||
idx = 1;
|
||||
|
||||
// 1.8.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
// 1.8.1 ?
|
||||
// 1.8.2 ?
|
||||
idx += 2;
|
||||
|
||||
// 2.8.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
// 2.8.1 ?
|
||||
// 2.8.2 ?
|
||||
idx += 2;
|
||||
|
||||
idx++; // Unknown empty octet string
|
||||
|
||||
CosemData* meterTs = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
|
||||
// 2.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 1.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
|
||||
// 31.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
// 51.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
// 71.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3current = ntohs(data->lu.data) / 100.0;
|
||||
|
||||
// 32.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1voltage = ntohs(data->lu.data) / 10.0;
|
||||
|
||||
// 72.7.0 ?
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3voltage = ntohs(data->lu.data) / 10.0;
|
||||
|
||||
// 52.7.0 missing?
|
||||
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60 * (PI/180)), 2) + pow(l3voltage * sin(60 * (PI/180)),2));
|
||||
|
||||
listType = 3;
|
||||
lastUpdateMillis = millis64();
|
||||
} else {
|
||||
idx = 5;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
}
|
||||
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterId = String(str);
|
||||
}
|
||||
|
||||
listType = 4;
|
||||
lastUpdateMillis = millis64();
|
||||
}
|
||||
} else if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) { // Kaifa special case
|
||||
// Kaifa special case...
|
||||
if(useMeterType == AmsTypeKaifa && data->base.type == CosemTypeDLongUnsigned) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
listType = 1;
|
||||
meterType = AmsTypeKaifa;
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(data->base.type == CosemTypeOctetString) { // Assuming first string is a list identifier
|
||||
} else if(data->base.type == CosemTypeOctetString) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
String listId = String(str);
|
||||
@@ -232,7 +46,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
this->listId = listId;
|
||||
meterType = AmsTypeKaifa;
|
||||
|
||||
uint8_t idx = 0;
|
||||
int idx = 0;
|
||||
data = getCosemDataAt(idx, ((char *) (d)));
|
||||
idx+=2;
|
||||
if(data->base.length == 0x0D || data->base.length == 0x12) {
|
||||
@@ -313,7 +127,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
if(data->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
meterTimestamp = tz != NULL ? tz->toUTC(ts) : ts;
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +148,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
this->listId = listId;
|
||||
meterType = AmsTypeIskra;
|
||||
|
||||
uint8_t idx = 0;
|
||||
int idx = 0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data->base.length == 0x12) {
|
||||
apply(state);
|
||||
@@ -749,31 +563,49 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(meterType == AmsTypeUnknown && useMeterType == AmsTypeUnknown) {
|
||||
debugger->println("AMS unknown meter type, trying to identify...");
|
||||
CosemData* d1 = getCosemDataAt(1, ((char *) (d)));
|
||||
CosemData* d2 = getCosemDataAt(2, ((char *) (d)));
|
||||
CosemData* d3 = getCosemDataAt(3, ((char *) (d)));
|
||||
CosemData* d7 = getCosemDataAt(7, ((char *) (d)));
|
||||
CosemData* d8 = getCosemDataAt(8, ((char *) (d)));
|
||||
} else if(useMeterType == AmsTypeIskra && data->base.type == CosemTypeOctetString) { // Iskra special case
|
||||
meterType = AmsTypeIskra;
|
||||
uint8_t idx = 5;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
if(data != NULL) {
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
}
|
||||
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterId = String(str);
|
||||
}
|
||||
|
||||
if(d1->base.type == CosemTypeDLongUnsigned &&
|
||||
d2->base.type == CosemTypeDLongUnsigned &&
|
||||
d3->base.type == CosemTypeDLongUnsigned &&
|
||||
d7->base.type == CosemTypeOctetString &&
|
||||
d8->base.type == CosemTypeOctetString
|
||||
) {
|
||||
meterType = AmsTypeIskra;
|
||||
lastUpdateMillis = millis64();
|
||||
listType = 3;
|
||||
} else if(d1->base.type == CosemTypeOctetString && d2->base.type == CosemTypeOctetString && d3->base.type == CosemTypeOctetString) {
|
||||
meterType = AmsTypeIskra;
|
||||
lastUpdateMillis = millis64();
|
||||
listType = 3;
|
||||
} else {
|
||||
} else if(useMeterType == AmsTypeUnknown) {
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_UNKNOWN_1, sizeof(AMS_OBIS_UNKNOWN_1), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
@@ -784,7 +616,7 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // OBIS code parsing
|
||||
} else {
|
||||
listType = 1;
|
||||
activeImportPower = val;
|
||||
|
||||
@@ -906,7 +738,18 @@ IEC6205675::IEC6205675(const char* d, Timezone* tz, uint8_t useMeterType, MeterC
|
||||
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
this->meterTimestamp = adjustForKnownIssues(amst->dt, tz, meterType == AmsTypeUnknown ? useMeterType : meterType);
|
||||
time_t ts = decodeCosemDateTime(amst->dt);
|
||||
int16_t deviation = ntohs(amst->dt.deviation);
|
||||
if(deviation < -720 || deviation > 720) { // Deviation not specified, adjust from localtime to UTC
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
if(ctx.timestamp > 0) {
|
||||
this->packageTimestamp = tz.toUTC(ctx.timestamp);
|
||||
}
|
||||
} else if(meterType == AmsTypeAidon) {
|
||||
meterTimestamp = ts - 3600; // 21.09.24, the clock is now correct
|
||||
} else {
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
|
||||
@@ -1282,24 +1125,3 @@ time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
time_t IEC6205675::adjustForKnownIssues(CosemDateTime dt, Timezone* tz, uint8_t meterType) {
|
||||
time_t ts = decodeCosemDateTime(dt);
|
||||
int16_t deviation = ntohs(dt.deviation);
|
||||
if(deviation < -720 || deviation > 720) {
|
||||
// Time zone not specified
|
||||
if(meterType == AmsTypeAidon || meterType == AmsTypeKamstrup) {
|
||||
// Special known case
|
||||
// 21.09.24, the clock is now correct for Aidon
|
||||
// 23.10.25, the clock is now correct for Kamstrup
|
||||
ts -= 3600;
|
||||
} else if(tz != NULL) {
|
||||
// Adjust from localtime to UTC
|
||||
ts = tz->toUTC(ts);
|
||||
}
|
||||
} else if(meterType == AmsTypeAidon) {
|
||||
// 21.09.24, the clock is now correct for Aidon
|
||||
ts -= 3600;
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
@@ -14,33 +14,7 @@ LNG2::LNG2(AmsData& meterState, const char* payload, uint8_t useMeterType, Meter
|
||||
meterType = AmsTypeLandisGyr;
|
||||
this->packageTimestamp = ctx.timestamp;
|
||||
|
||||
Lng2Data_3p_0e* d = (Lng2Data_3p_0e*) payload;
|
||||
this->l1voltage = ntohs(d->u1.data);
|
||||
this->l2voltage = ntohs(d->u2.data);
|
||||
this->l3voltage = ntohs(d->u3.data);
|
||||
|
||||
this->l1current = ntohs(d->i1.data) / 100.0;
|
||||
this->l2current = ntohs(d->i2.data) / 100.0;
|
||||
this->l3current = ntohs(d->i3.data) / 100.0;
|
||||
|
||||
this->activeImportPower = ntohl(d->activeImport.data);
|
||||
this->activeExportPower = ntohl(d->activeExport.data);
|
||||
this->activeImportCounter = ntohl(d->acumulatedImport.data) / 1000.0;
|
||||
this->activeExportCounter = ntohl(d->accumulatedExport.data) / 1000.0;
|
||||
|
||||
char str[64];
|
||||
uint8_t str_len = getString((CosemData*) &d->meterId, str);
|
||||
if(str_len > 0) {
|
||||
this->meterId = String(str);
|
||||
}
|
||||
listType = 3;
|
||||
lastUpdateMillis = millis64();
|
||||
} else if(h->length == 0x0b) {
|
||||
apply(meterState);
|
||||
meterType = AmsTypeLandisGyr;
|
||||
this->packageTimestamp = ctx.timestamp;
|
||||
|
||||
Lng2Data_3p_0b* d = (Lng2Data_3p_0b*) payload;
|
||||
Lng2Data_3p* d = (Lng2Data_3p*) payload;
|
||||
this->l1voltage = ntohs(d->u1.data);
|
||||
this->l2voltage = ntohs(d->u2.data);
|
||||
this->l3voltage = ntohs(d->u3.data);
|
||||
|
||||
@@ -174,8 +174,8 @@ bool PassiveMeterCommunicator::loop() {
|
||||
lastError = pos;
|
||||
printHanReadError(pos);
|
||||
len += hanSerial->readBytes(hanBuffer+len, hanBufferSize-len);
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(hanBuffer, len);
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes(hanBuffer, len);
|
||||
}
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
@@ -229,8 +229,8 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
|
||||
char* payload = ((char *) (hanBuffer)) + pos;
|
||||
if(maxDetectedPayloadSize < pos) maxDetectedPayloadSize = pos;
|
||||
if(ctx.type == DATA_TAG_DLMS) {
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw((uint8_t*) payload, ctx.length);
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes((uint8_t*) payload, ctx.length);
|
||||
}
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -278,7 +278,7 @@ AmsData* PassiveMeterCommunicator::getData(AmsData& meterState) {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("DLMS\n"));
|
||||
// TODO: Split IEC6205675 into DataParserKaifa and DataParserObis. This way we can add other means of parsing, for those other proprietary formats
|
||||
data = new IEC6205675(payload, tz, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
|
||||
data = new IEC6205675(payload, meterState.getMeterType(), &meterConfig, ctx, meterState, debugger);
|
||||
}
|
||||
} else if(ctx.type == DATA_TAG_DSMR) {
|
||||
data = new IEC6205621(payload, tz, &meterConfig);
|
||||
@@ -405,8 +405,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("HDLC frame:\n"));
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(buf, curLen);
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes(buf, curLen);
|
||||
}
|
||||
break;
|
||||
case DATA_TAG_MBUS:
|
||||
@@ -414,8 +414,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("MBUS frame:\n"));
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(buf, curLen);
|
||||
if(pt != NULL) {
|
||||
pt->publishBytes(buf, curLen);
|
||||
}
|
||||
break;
|
||||
case DATA_TAG_GBT:
|
||||
@@ -447,8 +447,8 @@ int16_t PassiveMeterCommunicator::unwrapData(uint8_t *buf, DataParserContext &co
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("DSMR frame:\n"));
|
||||
if(mqttDebug != NULL) {
|
||||
mqttDebug->publishRaw(buf, curLen);
|
||||
if(pt != NULL) {
|
||||
pt->publishString((char*) buf);
|
||||
}
|
||||
break;
|
||||
case DATA_TAG_SNRM:
|
||||
@@ -807,7 +807,6 @@ void PassiveMeterCommunicator::rxerr(int err) {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Serial buffer overflow\n"));
|
||||
rxBufferErrors++;
|
||||
#if defined(ESP32)
|
||||
if(rxBufferErrors > 1 && meterConfig.bufferSize < 8) {
|
||||
meterConfig.bufferSize += 2;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -817,7 +816,6 @@ void PassiveMeterCommunicator::rxerr(int err) {
|
||||
configChanged = true;
|
||||
rxBufferErrors = 0;
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case 3:
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
this->topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#else
|
||||
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
PassthroughMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
this->topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -15,23 +15,28 @@
|
||||
#define DOCPOS_MEASUREMENTUNIT 2
|
||||
#define DOCPOS_POSITION 3
|
||||
#define DOCPOS_AMOUNT 4
|
||||
#define DOCPOS_RESOLUTION 5
|
||||
|
||||
class EntsoeA44Parser: public Stream {
|
||||
public:
|
||||
EntsoeA44Parser(PricesContainer *container);
|
||||
EntsoeA44Parser();
|
||||
virtual ~EntsoeA44Parser();
|
||||
|
||||
char* getCurrency();
|
||||
char* getMeasurementUnit();
|
||||
float getPoint(uint8_t position);
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
void flush();
|
||||
size_t write(const uint8_t *buffer, size_t size);
|
||||
size_t write(uint8_t);
|
||||
void get(PricesContainer*);
|
||||
|
||||
private:
|
||||
PricesContainer *container;
|
||||
float multiplier = 1.0;
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
float points[25];
|
||||
|
||||
char buf[64];
|
||||
uint8_t pos = 0;
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
|
||||
#define SSL_BUF_SIZE 512
|
||||
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
#define PRICE_DAY_MO 0x01
|
||||
#define PRICE_DAY_TU 0x02
|
||||
#define PRICE_DAY_WE 0x04
|
||||
@@ -53,13 +57,10 @@ struct PriceConfig {
|
||||
uint8_t end_dayofmonth;
|
||||
};
|
||||
|
||||
struct AmsPriceV2Header {
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
char source[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
struct PricePart {
|
||||
char name[32];
|
||||
char description[32];
|
||||
uint32_t value;
|
||||
};
|
||||
|
||||
class PriceService {
|
||||
@@ -77,25 +78,17 @@ public:
|
||||
char* getCurrency();
|
||||
char* getArea();
|
||||
char* getSource();
|
||||
float getValueForHour(uint8_t direction, int8_t hour);
|
||||
float getValueForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPointsAvailable();
|
||||
uint8_t getCurrentPricePointIndex();
|
||||
|
||||
bool isExportPricesDifferentFromImport();
|
||||
|
||||
bool hasPrice() { return hasPrice(PRICE_DIRECTION_IMPORT); }
|
||||
bool hasPrice(uint8_t direction) { return getCurrentPrice(direction) != PRICE_NO_VALUE; }
|
||||
bool hasPricePoint(uint8_t direction, int8_t point) { return getPricePoint(direction, point) != PRICE_NO_VALUE; }
|
||||
|
||||
float getCurrentPrice(uint8_t direction);
|
||||
float getPricePoint(uint8_t direction, uint8_t point);
|
||||
float getPriceForRelativeHour(uint8_t direction, int8_t hour); // If not 60min interval, average
|
||||
float getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour);
|
||||
|
||||
std::vector<PriceConfig>& getPriceConfig();
|
||||
void setPriceConfig(uint8_t index, PriceConfig &priceConfig);
|
||||
void cropPriceConfig(uint8_t size);
|
||||
|
||||
PricePart getPricePart(uint8_t index);
|
||||
|
||||
int16_t getLastError();
|
||||
|
||||
bool load();
|
||||
@@ -110,7 +103,7 @@ private:
|
||||
PriceServiceConfig* config = NULL;
|
||||
HTTPClient* http = NULL;
|
||||
|
||||
uint8_t currentDay = 0, currentPricePoint = 0;
|
||||
uint8_t currentDay = 0, currentHour = 0;
|
||||
uint8_t tomorrowFetchMinute = 15; // How many minutes over 13:00 should it fetch prices
|
||||
uint8_t nextFetchDelayMinutes = 15;
|
||||
uint64_t lastTodayFetch = 0;
|
||||
@@ -124,6 +117,9 @@ private:
|
||||
Timezone* tz = NULL;
|
||||
Timezone* entsoeTz = NULL;
|
||||
|
||||
static const uint16_t BufferSize = 256;
|
||||
char* buf;
|
||||
|
||||
bool hub = false;
|
||||
uint8_t* key = NULL;
|
||||
uint8_t* auth = NULL;
|
||||
@@ -136,7 +132,5 @@ private:
|
||||
bool retrieve(const char* url, Stream* doc);
|
||||
float getCurrencyMultiplier(const char* from, const char* to, time_t t);
|
||||
bool timeIsInPeriod(tmElements_t tm, PriceConfig pc);
|
||||
float getFixedPrice(uint8_t direction, int8_t point);
|
||||
float getEnergyPricePoint(uint8_t direction, uint8_t point);
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -4,43 +4,15 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef _PRICESCONTAINER_H
|
||||
#define _PRICESCONTAINER_H
|
||||
|
||||
#define PRICE_NO_VALUE -127
|
||||
#define PRICE_DIRECTION_IMPORT 0x01
|
||||
#define PRICE_DIRECTION_EXPORT 0x02
|
||||
#define PRICE_DIRECTION_BOTH 0x03
|
||||
|
||||
class PricesContainer {
|
||||
public:
|
||||
PricesContainer(char* source);
|
||||
|
||||
void setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices);
|
||||
|
||||
char* getSource();
|
||||
void setCurrency(char* currency);
|
||||
char* getCurrency();
|
||||
|
||||
bool isExportPricesDifferentFromImport() {
|
||||
return differentExportPrices;
|
||||
}
|
||||
|
||||
uint8_t getResolutionInMinutes();
|
||||
uint8_t getNumberOfPoints();
|
||||
|
||||
void setPrice(uint8_t point, float value, uint8_t direction);
|
||||
bool hasPrice(uint8_t point, uint8_t direction);
|
||||
float getPrice(uint8_t point, uint8_t direction); // int32_t / 10_000
|
||||
|
||||
private:
|
||||
char source[4];
|
||||
struct PricesContainer {
|
||||
char currency[4];
|
||||
uint8_t resolutionInMinutes;
|
||||
bool differentExportPrices;
|
||||
uint8_t numberOfPoints;
|
||||
int32_t *points;
|
||||
char measurementUnit[4];
|
||||
int32_t points[25];
|
||||
char source[4];
|
||||
};
|
||||
#endif
|
||||
|
||||
@@ -7,14 +7,27 @@
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
EntsoeA44Parser::EntsoeA44Parser(PricesContainer *container) {
|
||||
this->container = container;
|
||||
EntsoeA44Parser::EntsoeA44Parser() {
|
||||
for(int i = 0; i < 25; i++) points[i] = PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
EntsoeA44Parser::~EntsoeA44Parser() {
|
||||
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getMeasurementUnit() {
|
||||
return measurementUnit;
|
||||
}
|
||||
|
||||
float EntsoeA44Parser::getPoint(uint8_t position) {
|
||||
if(position >= 25) return PRICE_NO_VALUE;
|
||||
return points[position];
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::available() {
|
||||
return 0;
|
||||
}
|
||||
@@ -44,7 +57,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
container->setCurrency(buf);
|
||||
memcpy(currency, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@@ -52,7 +65,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
if(strcmp_P(buf, PSTR("MWH"))) multiplier = 0.001;
|
||||
memcpy(measurementUnit, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
@@ -60,7 +73,7 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
long pn = String(buf).toInt() - 1;
|
||||
if(pn < container->getNumberOfPoints()) {
|
||||
if(pn < 25) {
|
||||
pointNum = pn;
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
@@ -72,25 +85,8 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
float val = String(buf).toFloat();
|
||||
for(uint8_t i = pointNum; i < container->getNumberOfPoints(); i++) {
|
||||
container->setPrice(i, val * multiplier, PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(docPos == DOCPOS_RESOLUTION) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
|
||||
// This happens if there are two time series in the XML. We are only interrested in the first one, so we ignore the rest of the document
|
||||
if(container->hasPrice(0, PRICE_DIRECTION_IMPORT)) return 1;
|
||||
|
||||
if(strcmp_P(buf, PSTR("PT15M"))) {
|
||||
container->setup(15, 100, false);
|
||||
} else if(strcmp_P(buf, PSTR("PT60M"))) {
|
||||
container->setup(60, 25, false);
|
||||
for(uint8_t i = pointNum; i < 25; i++) {
|
||||
points[i] = val;
|
||||
}
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
@@ -105,17 +101,15 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
buf[pos] = '\0';
|
||||
if(strcmp_P(buf, PSTR("<currency_Unit.name>")) == 0) {
|
||||
if(strcmp(buf, "<currency_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_CURRENCY;
|
||||
} else if(strcmp(buf, PSTR("<price_Measure_Unit.name>")) == 0) {
|
||||
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_MEASUREMENTUNIT;
|
||||
} else if(strcmp(buf, PSTR("<position>")) == 0) {
|
||||
} else if(strcmp(buf, "<position>") == 0) {
|
||||
docPos = DOCPOS_POSITION;
|
||||
pointNum = 0xFF;
|
||||
} else if(strcmp(buf, PSTR("<price.amount>")) == 0) {
|
||||
} else if(strcmp(buf, "<price.amount>") == 0) {
|
||||
docPos = DOCPOS_AMOUNT;
|
||||
} else if(strcmp(buf, PSTR("<resolution>")) == 0) {
|
||||
docPos = DOCPOS_RESOLUTION;
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
@@ -124,3 +118,15 @@ size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void EntsoeA44Parser::get(PricesContainer* container) {
|
||||
memset(container, 0, sizeof(*container));
|
||||
|
||||
strcpy(container->currency, currency);
|
||||
strcpy(container->measurementUnit, measurementUnit);
|
||||
strcpy(container->source, "EOE");
|
||||
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
container->points[i] = points[i] == PRICE_NO_VALUE ? PRICE_NO_VALUE : points[i] * 10000;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ PriceService::PriceService(RemoteDebug* Debug) : priceConfig(std::vector<PriceCo
|
||||
#else
|
||||
PriceService::PriceService(Stream* Debug) : priceConfig(std::vector<PriceConfig>()) {
|
||||
#endif
|
||||
this->buf = (char*) malloc(BufferSize);
|
||||
|
||||
debugger = Debug;
|
||||
|
||||
// Entso-E uses CET/CEST
|
||||
@@ -41,10 +43,6 @@ void PriceService::setup(PriceServiceConfig& config) {
|
||||
this->config = new PriceServiceConfig();
|
||||
}
|
||||
memcpy(this->config, &config, sizeof(config));
|
||||
if(this->config->resolutionInMinutes != 15 && this->config->resolutionInMinutes != 60) {
|
||||
this->config->resolutionInMinutes = 60;
|
||||
}
|
||||
|
||||
lastTodayFetch = lastTomorrowFetch = lastCurrencyFetch = 0;
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) delete tomorrow;
|
||||
@@ -93,157 +91,55 @@ char* PriceService::getArea() {
|
||||
|
||||
char* PriceService::getSource() {
|
||||
if(this->today != NULL && this->tomorrow != NULL) {
|
||||
if(strcmp(this->today->getSource(), this->tomorrow->getSource()) == 0) {
|
||||
return this->today->getSource();
|
||||
if(strcmp(this->today->source, this->tomorrow->source) == 0) {
|
||||
return this->today->source;
|
||||
} else {
|
||||
return "MIX";
|
||||
}
|
||||
} else if(today != NULL) {
|
||||
return this->today->getSource();
|
||||
return this->today->source;
|
||||
} else if(tomorrow != NULL) {
|
||||
return this->tomorrow->getSource();
|
||||
return this->tomorrow->source;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
uint8_t PriceService::getResolutionInMinutes() {
|
||||
return today != NULL ? today->getResolutionInMinutes() : 60;
|
||||
float PriceService::getValueForHour(uint8_t direction, int8_t hour) {
|
||||
time_t cur = time(nullptr);
|
||||
return getValueForHour(direction, cur, hour);
|
||||
}
|
||||
|
||||
uint8_t PriceService::getNumberOfPointsAvailable() {
|
||||
if(today == NULL) return getResolutionInMinutes() == 15 ? 192 : 48;
|
||||
if(tomorrow != NULL) return today->getNumberOfPoints() + tomorrow->getNumberOfPoints();
|
||||
return today->getNumberOfPoints();
|
||||
}
|
||||
|
||||
bool PriceService::isExportPricesDifferentFromImport() {
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.direction != PRICE_DIRECTION_BOTH) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return today != NULL && today->isExportPricesDifferentFromImport();
|
||||
}
|
||||
|
||||
float PriceService::getPricePoint(uint8_t direction, uint8_t point) {
|
||||
float value = getFixedPrice(direction, point);
|
||||
if(value == PRICE_NO_VALUE) value = getEnergyPricePoint(direction, point);
|
||||
if(value == PRICE_NO_VALUE) return PRICE_NO_VALUE;
|
||||
float PriceService::getValueForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
float ret = getEnergyPriceForHour(direction, ts, hour);
|
||||
if(ret == PRICE_NO_VALUE)
|
||||
return ret;
|
||||
|
||||
tmElements_t tm;
|
||||
time_t ts = time(nullptr);
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
tm.Hour = tm.Minute = tm.Second = 0;
|
||||
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
PriceConfig pc = priceConfig.at(i);
|
||||
if(pc.type == PRICE_TYPE_FIXED) continue;
|
||||
if((pc.direction & direction) != direction) continue;
|
||||
if(!timeIsInPeriod(tm, pc)) continue;
|
||||
float pcVal = pc.value / 10000.0;
|
||||
|
||||
switch(pc.type) {
|
||||
case PRICE_TYPE_ADD:
|
||||
value += pcVal;
|
||||
ret += pc.value / 10000.0;
|
||||
break;
|
||||
case PRICE_TYPE_SUBTRACT:
|
||||
value -= pcVal;
|
||||
ret -= pc.value / 10000.0;
|
||||
break;
|
||||
case PRICE_TYPE_PCT:
|
||||
value += (pcVal * value) / 100.0;
|
||||
ret += ((pc.value / 10000.0) * ret) / 100.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
return ret;
|
||||
}
|
||||
|
||||
float PriceService::getCurrentPrice(uint8_t direction) {
|
||||
time_t ts = time(nullptr);
|
||||
uint8_t pos = getCurrentPricePointIndex();
|
||||
|
||||
return getPricePoint(direction, pos);
|
||||
}
|
||||
|
||||
float PriceService::getEnergyPricePoint(uint8_t direction, uint8_t point) {
|
||||
uint8_t pos = point;
|
||||
|
||||
float multiplier = 1.0;
|
||||
uint8_t numberOfPointsToday = 24;
|
||||
if(today != NULL) {
|
||||
numberOfPointsToday = today->getNumberOfPoints();
|
||||
}
|
||||
|
||||
float value = PRICE_NO_VALUE;
|
||||
if(pos >= numberOfPointsToday) {
|
||||
pos = pos - numberOfPointsToday;
|
||||
if(tomorrow == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(pos >= tomorrow->getNumberOfPoints()) return PRICE_NO_VALUE;
|
||||
if(!tomorrow->hasPrice(pos, direction))
|
||||
return PRICE_NO_VALUE;
|
||||
value = tomorrow->getPrice(pos, direction);
|
||||
float mult = getCurrencyMultiplier(tomorrow->getCurrency(), config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
} else if(pos >= 0) {
|
||||
if(today == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(!today->hasPrice(pos, direction))
|
||||
return PRICE_NO_VALUE;
|
||||
value = today->getPrice(pos, direction);
|
||||
float mult = getCurrencyMultiplier(today->getCurrency(), config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
}
|
||||
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
|
||||
}
|
||||
|
||||
float PriceService::getPriceForRelativeHour(uint8_t direction, int8_t hour) {
|
||||
time_t ts = time(nullptr);
|
||||
float PriceService::getEnergyPriceForHour(uint8_t direction, time_t ts, int8_t hour) {
|
||||
tmElements_t tm;
|
||||
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
uint8_t targetHour = tm.Hour + hour;
|
||||
tm.Hour = tm.Minute = tm.Second = 0;
|
||||
time_t startOfDay = entsoeTz->toUTC(makeTime(tm));
|
||||
|
||||
if((ts + (hour * SECS_PER_HOUR)) < startOfDay) {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
if(getResolutionInMinutes() == 60) {
|
||||
return getPricePoint(direction, targetHour);
|
||||
}
|
||||
|
||||
float valueSum = 0.0f;
|
||||
uint8_t valueCount = 0;
|
||||
float indexIncrements = 60.0 / today->getResolutionInMinutes();
|
||||
uint8_t priceMapIndexStart = (uint8_t) floor(indexIncrements * targetHour);
|
||||
uint8_t priceMapIndexEnd = (uint8_t) ceil(indexIncrements * (targetHour+1));
|
||||
|
||||
for(uint8_t mi = priceMapIndexStart; mi < priceMapIndexEnd; mi++) {
|
||||
float val = getPricePoint(direction, mi);
|
||||
if(val == PRICE_NO_VALUE) continue;
|
||||
valueSum += val;
|
||||
valueCount++;
|
||||
}
|
||||
if(valueCount == 0) return PRICE_NO_VALUE;
|
||||
return valueSum / valueCount;
|
||||
}
|
||||
|
||||
float PriceService::getFixedPrice(uint8_t direction, int8_t point) {
|
||||
time_t ts = time(nullptr);
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
tm.Hour = tm.Minute = tm.Second = 0;
|
||||
ts = entsoeTz->toUTC(makeTime(tm)) + (point * SECS_PER_MIN * getResolutionInMinutes());
|
||||
breakTime(tz->toLocal(ts), tm);
|
||||
tm.Minute = tm.Second = 0;
|
||||
breakTime(tz->toLocal(ts + (hour * SECS_PER_HOUR)), tm);
|
||||
|
||||
float value = PRICE_NO_VALUE;
|
||||
for (uint8_t i = 0; i < priceConfig.size(); i++) {
|
||||
@@ -258,7 +154,68 @@ float PriceService::getFixedPrice(uint8_t direction, int8_t point) {
|
||||
value += pc.value / 10000.0;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
if(value != PRICE_NO_VALUE) return value;
|
||||
|
||||
int8_t pos = hour;
|
||||
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
while(tm.Hour > 0) {
|
||||
ts -= 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
pos++;
|
||||
}
|
||||
uint8_t hoursToday = 0;
|
||||
uint8_t todayDate = tm.Day;
|
||||
while(tm.Day == todayDate) {
|
||||
ts += 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
hoursToday++;
|
||||
}
|
||||
uint8_t hoursTomorrow = 0;
|
||||
uint8_t tomorrowDate = tm.Day;
|
||||
while(tm.Day == tomorrowDate) {
|
||||
ts += 3600;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
hoursTomorrow++;
|
||||
}
|
||||
|
||||
float multiplier = 1.0;
|
||||
if(pos >= hoursToday) {
|
||||
pos = pos - hoursToday;
|
||||
if(pos >= hoursTomorrow) return PRICE_NO_VALUE;
|
||||
if(tomorrow == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(tomorrow->points[pos] == PRICE_NO_VALUE)
|
||||
return PRICE_NO_VALUE;
|
||||
value = tomorrow->points[pos] / 10000.0;
|
||||
if(strcmp(tomorrow->measurementUnit, "KWH") == 0) {
|
||||
// Multiplier is 1
|
||||
} else if(strcmp(tomorrow->measurementUnit, "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
float mult = getCurrencyMultiplier(tomorrow->currency, config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
} else if(pos >= 0) {
|
||||
if(today == NULL)
|
||||
return PRICE_NO_VALUE;
|
||||
if(today->points[pos] == PRICE_NO_VALUE)
|
||||
return PRICE_NO_VALUE;
|
||||
value = today->points[pos] / 10000.0;
|
||||
if(strcmp(today->measurementUnit, "KWH") == 0) {
|
||||
// Multiplier is 1
|
||||
} else if(strcmp(today->measurementUnit, "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return PRICE_NO_VALUE;
|
||||
}
|
||||
float mult = getCurrencyMultiplier(today->currency, config->currency, time(nullptr));
|
||||
if(mult == 0) return PRICE_NO_VALUE;
|
||||
multiplier *= mult;
|
||||
}
|
||||
return value == PRICE_NO_VALUE ? PRICE_NO_VALUE : value * multiplier;
|
||||
}
|
||||
|
||||
bool PriceService::loop() {
|
||||
@@ -266,58 +223,42 @@ bool PriceService::loop() {
|
||||
if(now < 10000) return false; // Grace period
|
||||
|
||||
time_t t = time(nullptr);
|
||||
if(t < FirmwareVersion::BuildEpoch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
if(currentDay == 0) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day init\n"));
|
||||
currentDay = tm.Day;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
}
|
||||
|
||||
if(currentDay != tm.Day) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Day reset\n"));
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) {
|
||||
today = tomorrow;
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentDay = tm.Day;
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
} else if(currentPricePoint != getCurrentPricePointIndex()) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Price point reset\n"));
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
}
|
||||
|
||||
if(!config->enabled)
|
||||
return false;
|
||||
if(t < FirmwareVersion::BuildEpoch) return false;
|
||||
|
||||
#ifndef AMS2MQTT_PRICE_KEY
|
||||
if(strlen(getToken()) == 0) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if(strlen(config->area) == 0){
|
||||
if(strlen(config->area) == 0)
|
||||
return false;
|
||||
}
|
||||
if(strlen(config->currency) == 0) {
|
||||
if(strlen(config->currency) == 0)
|
||||
return false;
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
if(currentDay == 0) {
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
}
|
||||
|
||||
if(currentDay != tm.Day) {
|
||||
if(today != NULL) delete today;
|
||||
if(tomorrow != NULL) {
|
||||
today = tomorrow;
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentDay = tm.Day;
|
||||
currentHour = tm.Hour;
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
} else if(currentHour != tm.Hour) {
|
||||
currentHour = tm.Hour;
|
||||
return today != NULL || (!config->enabled && priceConfig.capacity() != 0); // Only trigger MQTT publish if we have todays prices.
|
||||
}
|
||||
|
||||
if(!config->enabled)
|
||||
return false;
|
||||
|
||||
bool readyToFetchForTomorrow = tomorrow == NULL && (tm.Hour > 13 || (tm.Hour == 13 && tm.Minute >= tomorrowFetchMinute)) && (lastTomorrowFetch == 0 || now - lastTomorrowFetch > (nextFetchDelayMinutes*60000));
|
||||
|
||||
@@ -332,7 +273,6 @@ bool PriceService::loop() {
|
||||
}
|
||||
today = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return today != NULL && !readyToFetchForTomorrow; // Only trigger MQTT publish if we have todays prices and we are not immediately ready to fetch price for tomorrow.
|
||||
}
|
||||
|
||||
@@ -349,7 +289,6 @@ bool PriceService::loop() {
|
||||
}
|
||||
tomorrow = NULL;
|
||||
}
|
||||
currentPricePoint = getCurrentPricePointIndex();
|
||||
return tomorrow != NULL;
|
||||
}
|
||||
|
||||
@@ -428,12 +367,11 @@ float PriceService::getCurrencyMultiplier(const char* from, const char* to, time
|
||||
#endif
|
||||
|
||||
float currencyMultiplier = 0;
|
||||
char buf[80];
|
||||
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
|
||||
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), from);
|
||||
if(retrieve(buf, &p)) {
|
||||
currencyMultiplier = p.getValue();
|
||||
if(strncmp(to, "NOK", 3) != 0) {
|
||||
snprintf_P(buf, 80, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
|
||||
snprintf_P(buf, BufferSize, PSTR("https://data.norges-bank.no/api/data/EXR/B.%s.NOK.SP?lastNObservations=1"), to);
|
||||
if(retrieve(buf, &p)) {
|
||||
if(p.getValue() > 0.0) {
|
||||
currencyMultiplier /= p.getValue();
|
||||
@@ -476,8 +414,7 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
breakTime(e1, d1);
|
||||
breakTime(e2, d2);
|
||||
|
||||
char buf[256];
|
||||
snprintf_P(buf, 256, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
|
||||
snprintf_P(buf, BufferSize, PSTR("https://web-api.tp.entsoe.eu/api?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s"),
|
||||
getToken(),
|
||||
d1.Year+1970, d1.Month, d1.Day, d1.Hour, 00,
|
||||
d2.Year+1970, d2.Month, d2.Day, d2.Hour, 00,
|
||||
@@ -497,30 +434,24 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) url: %s\n"), buf);
|
||||
PricesContainer* ret = new PricesContainer("EOE");
|
||||
EntsoeA44Parser a44(ret);
|
||||
if(retrieve(buf, &a44) && ret->hasPrice(0, PRICE_DIRECTION_IMPORT)) {
|
||||
EntsoeA44Parser a44;
|
||||
if(retrieve(buf, &a44) && a44.getPoint(0) != PRICE_NO_VALUE) {
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
a44.get(ret);
|
||||
return ret;
|
||||
} else {
|
||||
delete ret;
|
||||
return NULL;
|
||||
}
|
||||
} else if(hub) {
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Going to fetch prices from hub\n"));
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(t), tm);
|
||||
|
||||
char buf[128];
|
||||
snprintf_P(buf, 128, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d/pt%dm?currency=%s"),
|
||||
String data;
|
||||
snprintf_P(buf, BufferSize, PSTR("http://hub.amsleser.no/hub/price/%s/%d/%d/%d?currency=%s"),
|
||||
config->area,
|
||||
tm.Year+1970,
|
||||
tm.Month,
|
||||
tm.Day,
|
||||
config->resolutionInMinutes,
|
||||
config->currency
|
||||
);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
@@ -547,53 +478,23 @@ PricesContainer* PriceService::fetchPrices(time_t t) {
|
||||
#endif
|
||||
|
||||
if(status == HTTP_CODE_OK) {
|
||||
uint8_t content[1024];
|
||||
|
||||
WiFiClient* stream = http->getStreamPtr();
|
||||
data = http->getString();
|
||||
http->end();
|
||||
|
||||
uint8_t* content = (uint8_t*) (data.c_str());
|
||||
|
||||
DataParserContext ctx = {0,0,0,0};
|
||||
ctx.length = stream->readBytes(content, http->getSize());
|
||||
http->end();
|
||||
|
||||
ctx.length = data.length();
|
||||
GCMParser gcm(key, auth);
|
||||
int8_t gcmRet = gcm.parse(content, ctx);
|
||||
if(gcmRet > 0) {
|
||||
AmsPriceV2Header* header = (AmsPriceV2Header*) (content+gcmRet);
|
||||
|
||||
PricesContainer* ret = new PricesContainer(header->source);
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::DEBUG))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Setting up price container with pt%dm, %dpts, edi: %d\n"), header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
|
||||
ret->setup(header->resolutionInMinutes, header->numberOfPoints, header->differentExportPrices);
|
||||
ret->setCurrency(header->currency);
|
||||
int32_t* points = (int32_t*) &header[1];
|
||||
|
||||
int32_t intval;
|
||||
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
|
||||
// To avoid alignment issues on ESP8266, we use memcpy
|
||||
memcpy(&intval, &points[i], sizeof(int32_t));
|
||||
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
|
||||
float value = intval / 10000.0;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Import price position and value received: %d :: %.2f\n"), i, value);
|
||||
ret->setPrice(i, value, PRICE_DIRECTION_IMPORT);
|
||||
PricesContainer* ret = new PricesContainer();
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = PRICE_NO_VALUE;
|
||||
}
|
||||
if(header->differentExportPrices) {
|
||||
for(uint8_t i = 0; i < ret->getNumberOfPoints(); i++) {
|
||||
// To avoid alignment issues on ESP8266, we use memcpy
|
||||
memcpy(&intval, &points[ret->getNumberOfPoints()+i], sizeof(int32_t));
|
||||
intval = ntohl(intval); // Change byte order before converting to float, to support negative values
|
||||
float value = intval / 10000.0;
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::VERBOSE))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("(PriceService) Export price position and value received: %d :: %.2f\n"), i, value);
|
||||
ret->setPrice(i, value, PRICE_DIRECTION_EXPORT);
|
||||
}
|
||||
memcpy(ret, content+gcmRet, sizeof(*ret));
|
||||
for(uint8_t i = 0; i < 25; i++) {
|
||||
ret->points[i] = ntohl(ret->points[i]);
|
||||
}
|
||||
lastError = 0;
|
||||
nextFetchDelayMinutes = 1;
|
||||
@@ -757,11 +658,4 @@ bool PriceService::timeIsInPeriod(tmElements_t tm, PriceConfig pc) {
|
||||
}
|
||||
|
||||
return makeTime(tms) <= makeTime(tm) && makeTime(tme) >= makeTime(tm);
|
||||
}
|
||||
|
||||
uint8_t PriceService::getCurrentPricePointIndex() {
|
||||
time_t ts = time(nullptr);
|
||||
tmElements_t tm;
|
||||
breakTime(entsoeTz->toLocal(ts), tm);
|
||||
return ((tm.Hour * 60) + tm.Minute) / getResolutionInMinutes();
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
#include "PricesContainer.h"
|
||||
#include <cstring>
|
||||
|
||||
PricesContainer::PricesContainer(char* source) {
|
||||
strncpy(this->source, source, 4);
|
||||
}
|
||||
|
||||
void PricesContainer::setup(uint8_t resolutionInMinutes, uint8_t numberOfPoints, bool differentExportPrices) {
|
||||
this->resolutionInMinutes = resolutionInMinutes;
|
||||
this->differentExportPrices = differentExportPrices;
|
||||
this->numberOfPoints = numberOfPoints;
|
||||
this->points = new int32_t[numberOfPoints * (differentExportPrices ? 2 : 1)];
|
||||
memset(this->points, PRICE_NO_VALUE * 10000, numberOfPoints * (differentExportPrices ? 2 : 1) * sizeof(int32_t));
|
||||
}
|
||||
|
||||
char* PricesContainer::getSource() {
|
||||
return this->source;
|
||||
}
|
||||
|
||||
void PricesContainer::setCurrency(char* currency) {
|
||||
strncpy(this->currency, currency, 4);
|
||||
}
|
||||
|
||||
char* PricesContainer::getCurrency() {
|
||||
return this->currency;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getResolutionInMinutes() {
|
||||
return this->resolutionInMinutes;
|
||||
}
|
||||
|
||||
uint8_t PricesContainer::getNumberOfPoints() {
|
||||
return this->numberOfPoints;
|
||||
}
|
||||
|
||||
void PricesContainer::setPrice(uint8_t point, float value, uint8_t direction) {
|
||||
if(direction == PRICE_DIRECTION_EXPORT && !differentExportPrices) {
|
||||
return; // Export prices not supported
|
||||
}
|
||||
if(direction != PRICE_DIRECTION_EXPORT) {
|
||||
points[point] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
if(differentExportPrices && direction != PRICE_DIRECTION_IMPORT) {
|
||||
points[point + numberOfPoints] = static_cast<int32_t>(value * 10000);
|
||||
}
|
||||
}
|
||||
|
||||
bool PricesContainer::hasPrice(uint8_t point, uint8_t direction) {
|
||||
float val = getPrice(point, direction);
|
||||
return val != PRICE_NO_VALUE;
|
||||
}
|
||||
|
||||
float PricesContainer::getPrice(uint8_t point, uint8_t direction) {
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_EXPORT) {
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point + numberOfPoints]) / 10000.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if(differentExportPrices && direction == PRICE_DIRECTION_BOTH) return PRICE_NO_VALUE; // Can't get a price for both directions if the export prices are different
|
||||
|
||||
if(point < numberOfPoints) {
|
||||
return static_cast<float>(points[point]) / 10000.0f;
|
||||
}
|
||||
|
||||
return PRICE_NO_VALUE; // Invalid point
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
#else
|
||||
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf, AmsFirmwareUpdater* updater) : AmsMqttHandler(mqttConfig, debugger, buf, updater) {
|
||||
RawMqttHandler(MqttConfig& mqttConfig, Stream* debugger, char* buf) : AmsMqttHandler(mqttConfig, debugger, buf) {
|
||||
full = mqttConfig.payloadFormat == 2;
|
||||
topic = String(mqttConfig.publishTopic);
|
||||
};
|
||||
@@ -26,7 +26,7 @@ public:
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(PriceService*);
|
||||
bool publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea);
|
||||
bool publishRaw(uint8_t* raw, size_t length);
|
||||
bool publishRaw(String data);
|
||||
|
||||
void onMessage(String &topic, String &payload);
|
||||
|
||||
@@ -36,7 +36,6 @@ private:
|
||||
bool full;
|
||||
String topic;
|
||||
uint32_t lastThresholdPublish = 0;
|
||||
bool hasExport = false;
|
||||
|
||||
bool publishList1(AmsData* data, AmsData* meterState);
|
||||
bool publishList2(AmsData* data, AmsData* meterState);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "FirmwareVersion.h"
|
||||
|
||||
bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAccounting* ea, PriceService* ps) {
|
||||
if(topic.isEmpty() || !connected())
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
AmsData data;
|
||||
@@ -41,15 +41,6 @@ bool RawMqttHandler::publish(AmsData* update, AmsData* previousState, EnergyAcco
|
||||
publishList1(&data, previousState);
|
||||
loop();
|
||||
}
|
||||
|
||||
if(data.getListType() >= 2 && data.getActiveExportPower() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(data.getListType() >= 3 && data.getActiveExportCounter() > 0.0) {
|
||||
hasExport = true;
|
||||
}
|
||||
|
||||
if(ea->isInitialized()) {
|
||||
publishRealtime(ea);
|
||||
loop();
|
||||
@@ -237,9 +228,9 @@ bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
if(topic.isEmpty() || !connected())
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
if(!ps->hasPrice())
|
||||
if(ps->getValueForHour(PRICE_DIRECTION_IMPORT, 0) == PRICE_NO_VALUE)
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
@@ -250,7 +241,7 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
float values[34];
|
||||
for(int i = 0;i < 34; i++) values[i] = PRICE_NO_VALUE;
|
||||
for(uint8_t i = 0; i < 34; i++) {
|
||||
float val = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
float val = ps->getValueForHour(PRICE_DIRECTION_IMPORT, now, i);
|
||||
values[i] = val;
|
||||
|
||||
if(i > 23) continue;
|
||||
@@ -317,33 +308,15 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
mqtt.publish(topic + "/price/resolution", String(ps->getResolutionInMinutes()), true, 0);
|
||||
mqtt.loop();
|
||||
|
||||
uint8_t relativeIndex = 0;
|
||||
uint8_t startIndex = ps->getCurrentPricePointIndex();
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
for(int i = startIndex; i < numberOfPoints; i++) {
|
||||
float importVal = ps->getPricePoint(PRICE_DIRECTION_IMPORT, i);
|
||||
if(importVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), "", true, 0);
|
||||
for(int i = 0; i < 34; i++) {
|
||||
float val = values[i];
|
||||
if(val == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/" + String(i), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/import/" + String(relativeIndex), String(importVal, 4), true, 0);
|
||||
mqtt.publish(topic + "/price/" + String(i), String(val, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
if(hasExport && ps->isExportPricesDifferentFromImport()) {
|
||||
float exportVal = ps->getPricePoint(PRICE_DIRECTION_EXPORT, i);
|
||||
if(exportVal == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/price/export/" + String(relativeIndex), String(exportVal, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
}
|
||||
relativeIndex++;
|
||||
}
|
||||
if(min != INT16_MAX) {
|
||||
mqtt.publish(topic + "/price/min", String(min, 4), true, 0);
|
||||
@@ -365,11 +338,20 @@ bool RawMqttHandler::publishPrices(PriceService* ps) {
|
||||
mqtt.publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
|
||||
float exportPrice = ps->getEnergyPriceForHour(PRICE_DIRECTION_EXPORT, now, 0);
|
||||
if(exportPrice == PRICE_NO_VALUE) {
|
||||
mqtt.publish(topic + "/exportprice/0", "", true, 0);
|
||||
mqtt.loop();
|
||||
} else {
|
||||
mqtt.publish(topic + "/exportprice/0", String(exportPrice, 4), true, 0);
|
||||
mqtt.loop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishSystem(HwTools* hw, PriceService* ps, EnergyAccounting* ea) {
|
||||
if(topic.isEmpty() || !connected())
|
||||
if(topic.isEmpty() || !mqtt.connected())
|
||||
return false;
|
||||
|
||||
mqtt.publish(topic + "/id", WiFi.macAddress(), true, 0);
|
||||
@@ -396,16 +378,8 @@ uint8_t RawMqttHandler::getFormat() {
|
||||
return full ? 3 : 2;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishRaw(uint8_t* raw, size_t length) {
|
||||
if(topic.isEmpty() || !connected())
|
||||
return false;
|
||||
|
||||
if(length <= 0 || length > BufferSize) return false;
|
||||
|
||||
String str = toHex(raw, length);
|
||||
bool ret = mqtt.publish(topic + "/data", str);
|
||||
loop();
|
||||
return ret;
|
||||
bool RawMqttHandler::publishRaw(String data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void RawMqttHandler::onMessage(String &topic, String &payload) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# SvelteUi App
|
||||
|
||||
Web interface for AMS Reader firmware built with Svelte 5 and Vite 6.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 20.x or 22.x LTS (required for Vite 6)
|
||||
- npm
|
||||
|
||||
### Local Development Configuration
|
||||
|
||||
To develop against your AMS reader device, you need to configure the proxy target:
|
||||
|
||||
1. Copy the example config file:
|
||||
```bash
|
||||
cp vite.config.local.example.js vite.config.local.js
|
||||
```
|
||||
|
||||
2. Edit `vite.config.local.js` and update the IP address to match your device:
|
||||
```javascript
|
||||
export default {
|
||||
proxyTarget: "http://192.168.1.100" // Your device's IP
|
||||
}
|
||||
```
|
||||
|
||||
3. The `vite.config.local.js` file is gitignored, so your personal settings won't be committed.
|
||||
|
||||
### Running Development Server
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The dev server will proxy API requests to your configured device IP.
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The build output will be in the `dist/` directory.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/` - Application source code
|
||||
- `routes/` - Page components using svelte-spa-router
|
||||
- `lib/` - Shared components and utilities
|
||||
- `public/` - Static assets (favicon, etc.)
|
||||
- `dist/` - Build output (not committed to git)
|
||||
|
||||
## Key Technologies
|
||||
|
||||
- **Svelte 5.17.0** - UI framework
|
||||
- **Vite 6.0.7** - Build tool
|
||||
- **svelte-spa-router 4.0.1** - Hash-based routing
|
||||
- **Tailwind CSS** - Styling
|
||||
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,9 +8,10 @@
|
||||
<link rel="mask-icon" href="/favicon.svg" color="#000000">
|
||||
<title>AMS reader</title>
|
||||
<script type="module" crossorigin src="/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/index.css">
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body class="bg-gray-100 dark:bg-gray-900">
|
||||
<div id="app"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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
2612
lib/SvelteUi/app/package-lock.json
generated
2612
lib/SvelteUi/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,19 +9,24 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"overrides": {
|
||||
"svelte-navigator": {
|
||||
"svelte": ">=4.x"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.1.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"svelte": "^5.17.0",
|
||||
"svelte-spa-router": "^4.0.1",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"svelte-qrcode": "^1.0.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vite": "^6.0.7"
|
||||
"vite": "^4.5.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssnano": "^5.1.15",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
|
||||
<title>Amsleser</title>
|
||||
<g transform="translate(-29.5,-83)">
|
||||
<circle r="4.8016944" cy="123.56455" cx="55.064552"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path d="m 41.298717,103.9049 a 24,24 0 0 1 27.531669,0"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path d="m 35.562952,95.713384 a 34,34 0 0 1 39.003199,-2e-6"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path d="m 47.034482,112.09642 a 14,14 0 0 1 16.06014,0"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:3.3;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle r="3" cy="105.99158" cx="38.181862"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle r="3" cy="97.959579" cx="77.491386"
|
||||
style="fill:none;stroke:#045c7c;stroke-width:2.4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,27 +1,44 @@
|
||||
<script>
|
||||
import Router from "svelte-spa-router";
|
||||
import { push } from "svelte-spa-router";
|
||||
import { getTariff, sysinfoStore, dataStore, getSysinfo } from './lib/DataStores.js';
|
||||
import { Router, Route, navigate } from "svelte-navigator";
|
||||
import { getTariff, tariffStore, sysinfoStore, dataStore, pricesStore, dayPlotStore, monthPlotStore, temperaturesStore, getSysinfo } from './lib/DataStores.js';
|
||||
import { translationsStore, getTranslations } from "./lib/TranslationService.js";
|
||||
import Favicon from './assets/favicon.svg'; // Need this for the build
|
||||
import Header from './lib/Header.svelte';
|
||||
import DashboardRoute from './routes/DashboardRoute.svelte';
|
||||
import ConfigurationRoute from './routes/ConfigurationRoute.svelte';
|
||||
import StatusRoute from './routes/StatusRoute.svelte';
|
||||
import PriceConfigRoute from './routes/PriceConfigRoute.svelte';
|
||||
import MqttCaRoute from './routes/MqttCaRoute.svelte';
|
||||
import MqttCertRoute from './routes/MqttCertRoute.svelte';
|
||||
import MqttKeyRoute from './routes/MqttKeyRoute.svelte';
|
||||
import ConsentRoute from './routes/ConsentRoute.svelte';
|
||||
import SetupRoute from './routes/SetupRoute.svelte';
|
||||
import VendorRoute from './routes/VendorRoute.svelte';
|
||||
import EditDayRoute from './routes/EditDayRoute.svelte';
|
||||
import EditMonthRoute from './routes/EditMonthRoute.svelte';
|
||||
import Dashboard from './lib/Dashboard.svelte';
|
||||
import ConfigurationPanel from './lib/ConfigurationPanel.svelte';
|
||||
import StatusPage from './lib/StatusPage.svelte';
|
||||
import VendorPanel from './lib/VendorPanel.svelte';
|
||||
import SetupPanel from './lib/SetupPanel.svelte';
|
||||
import Mask from './lib/Mask.svelte';
|
||||
import FileUploadComponent from "./lib/FileUploadComponent.svelte";
|
||||
import ConsentComponent from "./lib/ConsentComponent.svelte";
|
||||
import PriceConfig from "./lib/PriceConfig.svelte";
|
||||
import DataEdit from "./lib/DataEdit.svelte";
|
||||
import { updateRealtime } from "./lib/RealtimeStore.js";
|
||||
|
||||
let basepath = document.getElementsByTagName('base')[0].getAttribute("href");
|
||||
if(!basepath) basepath = "/";
|
||||
|
||||
let prices;
|
||||
pricesStore.subscribe(update => {
|
||||
prices = update;
|
||||
});
|
||||
|
||||
let dayPlot;
|
||||
dayPlotStore.subscribe(update => {
|
||||
dayPlot = update;
|
||||
});
|
||||
|
||||
let monthPlot;
|
||||
monthPlotStore.subscribe(update => {
|
||||
monthPlot = update;
|
||||
});
|
||||
|
||||
let temperatures;
|
||||
temperaturesStore.subscribe(update => {
|
||||
temperatures = update;
|
||||
});
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
@@ -34,11 +51,11 @@
|
||||
sysinfoStore.subscribe(update => {
|
||||
sysinfo = update;
|
||||
if(sysinfo.vndcfg === false) {
|
||||
push("/vendor");
|
||||
navigate(basepath + "vendor");
|
||||
} else if(sysinfo.usrcfg === false) {
|
||||
push("/setup");
|
||||
navigate(basepath + "setup");
|
||||
} else if(sysinfo.fwconsent === 0) {
|
||||
push("/consent");
|
||||
navigate(basepath + "consent");
|
||||
}
|
||||
|
||||
if(sysinfo.ui.k === 1) {
|
||||
@@ -72,26 +89,53 @@
|
||||
updateRealtime(update);
|
||||
});
|
||||
|
||||
let tariffData = {};
|
||||
tariffStore.subscribe(update => {
|
||||
tariffData = update;
|
||||
});
|
||||
getTariff();
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto m-3">
|
||||
<Header data={data} basepath={basepath}/>
|
||||
|
||||
<Router routes={{
|
||||
'/': DashboardRoute,
|
||||
'/configuration': ConfigurationRoute,
|
||||
'/priceconfig': PriceConfigRoute,
|
||||
'/status': StatusRoute,
|
||||
'/mqtt-ca': MqttCaRoute,
|
||||
'/mqtt-cert': MqttCertRoute,
|
||||
'/mqtt-key': MqttKeyRoute,
|
||||
'/consent': ConsentRoute,
|
||||
'/setup': SetupRoute,
|
||||
'/vendor': VendorRoute,
|
||||
'/edit-day': EditDayRoute,
|
||||
'/edit-month': EditMonthRoute,
|
||||
}} />
|
||||
<Router basepath={basepath}>
|
||||
<Header data={data} basepath={basepath}/>
|
||||
<Route path="/">
|
||||
<Dashboard data={data} sysinfo={sysinfo} prices={prices} dayPlot={dayPlot} monthPlot={monthPlot} temperatures={temperatures} translations={translations} tariffData={tariffData}/>
|
||||
</Route>
|
||||
<Route path="/configuration">
|
||||
<ConfigurationPanel sysinfo={sysinfo} basepath={basepath} data={data}/>
|
||||
</Route>
|
||||
<Route path="/priceconfig">
|
||||
<PriceConfig basepath={basepath}/>
|
||||
</Route>
|
||||
<Route path="/status">
|
||||
<StatusPage sysinfo={sysinfo} data={data}/>
|
||||
</Route>
|
||||
<Route path="/mqtt-ca">
|
||||
<FileUploadComponent title="CA" action="/mqtt-ca"/>
|
||||
</Route>
|
||||
<Route path="/mqtt-cert">
|
||||
<FileUploadComponent title="certificate" action="/mqtt-cert"/>
|
||||
</Route>
|
||||
<Route path="/mqtt-key">
|
||||
<FileUploadComponent title="private key" action="/mqtt-key"/>
|
||||
</Route>
|
||||
<Route path="/consent">
|
||||
<ConsentComponent sysinfo={sysinfo} basepath={basepath}/>
|
||||
</Route>
|
||||
<Route path="/setup">
|
||||
<SetupPanel sysinfo={sysinfo}/>
|
||||
</Route>
|
||||
<Route path="/vendor">
|
||||
<VendorPanel sysinfo={sysinfo} basepath={basepath}/>
|
||||
</Route>
|
||||
<Route path="/edit-day">
|
||||
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" basepath={basepath}/>
|
||||
</Route>
|
||||
<Route path="/edit-month">
|
||||
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" basepath={basepath}/>
|
||||
</Route>
|
||||
</Router>
|
||||
|
||||
{#if sysinfo.booting}
|
||||
{#if sysinfo.trying}
|
||||
|
||||
@@ -188,22 +188,3 @@ svg {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -9px;
|
||||
border-width: 9px;
|
||||
border-style: solid;
|
||||
border-color: #ddd transparent transparent transparent;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { tooltip } from './tooltip';
|
||||
import { Link } from "svelte-navigator";
|
||||
|
||||
export let config;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{#if config.link}
|
||||
<div class="text-xs text-right">
|
||||
{#if config.link.route}
|
||||
<a href={"#" + config.link.url}>{config.link.text}</a>
|
||||
<Link to={config.link.url}>{config.link.text}</Link>
|
||||
{:else}
|
||||
<a href={config.link.url} target={config.link.target}>{config.link.text}</a>
|
||||
{/if}
|
||||
@@ -61,7 +61,7 @@
|
||||
{#if !isNaN(yScale(tick.value))}
|
||||
<g class="tick tick-{tick.value} tick-{tick.color}" transform="translate(0, {yScale(tick.value)})">
|
||||
<line x2="100%"></line>
|
||||
<text y="-4" x={tick.align == 'right' ? '90%' : ''}>{tick.label}</text>
|
||||
<text y="-4" x={tick.align == 'right' ? '85%' : ''}>{tick.label}</text>
|
||||
</g>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -72,7 +72,7 @@
|
||||
{#each config.x.ticks as point, i}
|
||||
{#if !isNaN(xScale(i))}
|
||||
<g class="tick" transform="translate({xScale(i)},{heightAvailable})">
|
||||
{#if barWidth > 20 || i%2 == 0 || !config.x.ticks[i-1].label}
|
||||
{#if barWidth > 20 || i%2 == 0}
|
||||
<text x="{barWidth/2}" y="-4">{point.label}</text>
|
||||
{/if}
|
||||
</g>
|
||||
@@ -83,25 +83,28 @@
|
||||
<g class='bars'>
|
||||
{#each config.points as point, i}
|
||||
{#if !isNaN(xScale(i)) && !isNaN(yScale(point.value))}
|
||||
<g data-title="{point.title}" use:tooltip>
|
||||
<g>
|
||||
{#if point.value !== undefined}
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(point.value)}"
|
||||
width="{barWidth * 0.95}"
|
||||
width="{barWidth - 4}"
|
||||
height="{yScale(config.y.min) - yScale(Math.min(config.y.min, 0) + point.value)}"
|
||||
fill="{point.color}"
|
||||
/>
|
||||
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth * 0.95}"
|
||||
width="{barWidth - 4}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{barWidth < vertSwitch || point.labelAngle ? 'left' : 'middle'}"
|
||||
fill="{yScale(point.value) > yScale(0)-labelOffset && !config.dark ? point.color : 'white'}"
|
||||
transform="translate({xScale(i) + barWidth/2} {yScale(point.value) > yScale(0) - labelOffset ? yScale(point.value) - labelOffset : yScale(point.value) + 10}) rotate({point.labelAngle ? point.labelAngle : barWidth < vertSwitch ? 90 : 0})"
|
||||
|
||||
>{point.label}</text>
|
||||
{#if point.title}
|
||||
<title>{point.title}</title>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</g>
|
||||
@@ -110,13 +113,13 @@
|
||||
<rect
|
||||
x="{xScale(i) + 2}"
|
||||
y="{yScale(0)}"
|
||||
width="{barWidth * 0.95}"
|
||||
width="{barWidth - 4}"
|
||||
height="{yScale(config.y.min) - yScale(config.y.min + point.value2)}"
|
||||
fill="{point.color2 ? point.color2 : point.color}"
|
||||
/>
|
||||
{#if barWidth > 15}
|
||||
<text
|
||||
width="{barWidth * 0.95}"
|
||||
width="{barWidth - 4}"
|
||||
dominant-baseline="middle"
|
||||
text-anchor="{'middle'}"
|
||||
fill="{yScale(-point.value2) < yScale(0) + 15 && !config.dark ? point.color2 ? point.color2 : point.color : 'white'}"
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
<script>
|
||||
import { getConfiguration, configurationStore } from '../lib/ConfigurationStore'
|
||||
import { sysinfoStore, networksStore, dataStore } from '../lib/DataStores.js';
|
||||
import fetchWithTimeout from '../lib/fetchWithTimeout';
|
||||
import { translationsStore } from '../lib/TranslationService';
|
||||
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, isBusPowered } from '../lib/Helpers.js';
|
||||
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import Badge from '../lib/Badge.svelte';
|
||||
import CountrySelectOptions from '../lib/CountrySelectOptions.svelte';
|
||||
import { push } from 'svelte-spa-router';
|
||||
import SubnetOptions from '../lib/SubnetOptions.svelte';
|
||||
import { getConfiguration, configurationStore } from './ConfigurationStore'
|
||||
import { sysinfoStore, networksStore } from './DataStores.js';
|
||||
import fetchWithTimeout from './fetchWithTimeout';
|
||||
import { translationsStore } from './TranslationService';
|
||||
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern } from './Helpers.js';
|
||||
import UartSelectOptions from './UartSelectOptions.svelte';
|
||||
import Mask from './Mask.svelte'
|
||||
import Badge from './Badge.svelte';
|
||||
import CountrySelectOptions from './CountrySelectOptions.svelte';
|
||||
import { Link, navigate } from 'svelte-navigator';
|
||||
import SubnetOptions from './SubnetOptions.svelte';
|
||||
import QrCode from 'svelte-qrcode';
|
||||
|
||||
let basepath = "/";
|
||||
let sysinfo = {};
|
||||
let data;
|
||||
export let basepath = "/";
|
||||
export let sysinfo = {};
|
||||
export let data;
|
||||
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
dataStore.subscribe(v => data = v);
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
@@ -153,7 +150,7 @@
|
||||
});
|
||||
|
||||
saving = false;
|
||||
push(basepath);
|
||||
navigate(basepath);
|
||||
}
|
||||
|
||||
async function reboot() {
|
||||
@@ -258,7 +255,7 @@
|
||||
{#if configuration?.g}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.general?.title ?? "General"}</strong>
|
||||
<a href="{wiki('general')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('General-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="g" value="true"/>
|
||||
<div class="my-1">
|
||||
<div class="flex">
|
||||
@@ -282,11 +279,11 @@
|
||||
<select name="pr" bind:value={configuration.p.r} on:change={enablePriceFetch} class="in-f w-full">
|
||||
<optgroup label="Norway">
|
||||
{#if !configuration.p.t}
|
||||
<option value="NO1S">NO1 w/support</option>
|
||||
<option value="NO2S">NO2 w/support</option>
|
||||
<option value="NO3S">NO3 w/support</option>
|
||||
<option value="NO4S">NO4 w/support</option>
|
||||
<option value="NO5S">NO5 w/support</option>
|
||||
<option value="NO1S">NO1 with support</option>
|
||||
<option value="NO2S">NO2 with support</option>
|
||||
<option value="NO3S">NO3 with support</option>
|
||||
<option value="NO4S">NO4 with support</option>
|
||||
<option value="NO5S">NO5 with support</option>
|
||||
{/if}
|
||||
<option value="10YNO-1--------2">NO1</option>
|
||||
<option value="10YNO-2--------T">NO2</option>
|
||||
@@ -320,14 +317,6 @@
|
||||
<option value="10YCH-SWISSGRIDZ">Switzerland</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.resolution ?? "Resolution"}<br/>
|
||||
<select name="pm" bind:value={configuration.p.m} class="in-m">
|
||||
{#each [15,60] as m}
|
||||
<option value={m}>{m}M</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{translations.conf?.price?.currency ?? "Currency"}<br/>
|
||||
<select name="pc" bind:value={configuration.p.c} class="in-l">
|
||||
@@ -339,7 +328,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
<a href="#/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</a>
|
||||
<Link to="/priceconfig" class="text-blue-600 hover:text-blue-800">{translations.conf?.price?.conf ?? "Configure"}</Link>
|
||||
</div>
|
||||
<div class="my-1">
|
||||
<label><input type="checkbox" name="pe" value="true" bind:checked={configuration.p.e} class="rounded mb-1"/> {translations.conf?.price?.enabled ?? "Enabled"}</label>
|
||||
@@ -375,7 +364,7 @@
|
||||
{#if configuration?.m}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.meter?.title ?? "Meter"}</strong>
|
||||
<a href="{wiki('meter')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Meter-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="m" value="true"/>
|
||||
<input type="hidden" name="mo" value="1"/>
|
||||
<div class="my-1">
|
||||
@@ -482,7 +471,7 @@
|
||||
{#if configuration?.w}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.connection?.title ?? "Connection"}</strong>
|
||||
<a href="{wiki('connection')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Network-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}>
|
||||
@@ -544,7 +533,7 @@
|
||||
{#if configuration?.n}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.network?.title ?? "Network"}</strong>
|
||||
<a href="{wiki('network')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Network-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<div class="my-1">
|
||||
{translations.conf?.network?.ip ?? "IP"}<br/>
|
||||
<div class="flex">
|
||||
@@ -589,7 +578,7 @@
|
||||
{#if configuration?.q}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.mqtt?.title ?? "MQTT"}</strong>
|
||||
<a href="{wiki('mqtt')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('MQTT-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="q" value="true"/>
|
||||
<div class="my-1">
|
||||
{translations.conf?.mqtt?.server ?? "Server"}
|
||||
@@ -606,28 +595,28 @@
|
||||
<div class="my-1 flex">
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.c}
|
||||
<span class="bd-on"><a href="#/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</a></span>
|
||||
<span class="bd-on"><Link to="/mqtt-ca">{translations.conf?.mqtt?.ca_ok ?? "CA OK"}</Link></span>
|
||||
<span class="bd-off" on:click={askDeleteCa} on:keypress={askDeleteCa}>🗑</span>
|
||||
{:else}
|
||||
<a href="#/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></a>
|
||||
<Link to="/mqtt-ca"><Badge color="blue" text={translations.conf?.mqtt?.btn_ca_upload ?? "Upload CA"} title={translations.conf?.mqtt?.title_ca ?? ""}/></Link>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.r}
|
||||
<span class="bd-on"><a href="#/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</a></span>
|
||||
<span class="bd-on"><Link to="/mqtt-cert">{translations.conf?.mqtt?.crt_ok ?? "Cert OK"}</Link></span>
|
||||
<span class="bd-off" on:click={askDeleteCert} on:keypress={askDeleteCert}>🗑</span>
|
||||
{:else}
|
||||
<a href="#/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></a>
|
||||
<Link to="/mqtt-cert"><Badge color="blue" text={translations.conf?.mqtt?.btn_crt_upload ?? "Upload cert"} title={translations.conf?.mqtt?.title_crt ?? ""}/></Link>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="flex pr-2">
|
||||
{#if configuration.q.s.k}
|
||||
<span class="bd-on"><a href="#/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</a></span>
|
||||
<span class="bd-on"><Link to="/mqtt-key">{translations.conf?.mqtt?.key_ok ?? "Key OK"}</Link></span>
|
||||
<span class="bd-off" on:click={askDeleteKey} on:keypress={askDeleteKey}>🗑</span>
|
||||
{:else}
|
||||
<a href="#/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></a>
|
||||
<Link to="/mqtt-key"><Badge color="blue" text={translations.conf?.mqtt?.btn_key_upload ?? "Upload key"} title={translations.conf?.mqtt?.title_key ?? ""}/></Link>
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
@@ -739,7 +728,7 @@
|
||||
{#if configuration?.c}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.cloud?.title ?? "Cloud connections"}</strong>
|
||||
<a href="{wiki('cloud-connections')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Cloud')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<input type="hidden" name="c" value="true"/>
|
||||
{#if sysinfo?.features?.includes('cloud')}
|
||||
<div class="my-1">
|
||||
@@ -788,7 +777,7 @@
|
||||
{#if configuration?.p?.r?.startsWith("NO") || configuration?.p?.r?.startsWith("10YNO") || configuration?.p?.r?.startsWith('10Y1001A1001A4')}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.thresholds?.title ?? "Thresholds"}</strong>
|
||||
<a href="{wiki('tariff-thresholds')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Threshold-configuration')}" 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}
|
||||
@@ -809,7 +798,7 @@
|
||||
{#if configuration?.u}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.ui?.title ?? "User interface"}</strong>
|
||||
<a href="{wiki('user-interface')}" target="_blank" class="float-right">ⓘ</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}
|
||||
@@ -836,7 +825,7 @@
|
||||
{#if configuration?.i?.h && (sysinfo?.board > 20 || sysinfo?.chip == 'esp8266' || configuration?.i?.d?.d > 0)}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.hw?.title ?? "Hardware"}</strong>
|
||||
<a href="{wiki('hardware')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('GPIO-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
{#if sysinfo.board > 20}
|
||||
<input type="hidden" name="i" value="true"/>
|
||||
<div class="flex flex-wrap">
|
||||
@@ -918,13 +907,6 @@
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
{#if isBusPowered(sysinfo.board)}
|
||||
Power saving:
|
||||
<select name="ip" bind:value={configuration.i.p} class="in-s">
|
||||
<option value={0}>{translations.conf?.hw?.powersaving?.[0] ?? "Normal"}</option>
|
||||
<option value={3}>{translations.conf?.hw?.powersaving?.[3] ?? "Extreme (Experimental)"}</option>
|
||||
</select>
|
||||
{/if}
|
||||
{#if sysinfo.chip == 'esp8266'}
|
||||
<input type="hidden" name="iv" value="true"/>
|
||||
<div class="my-1 flex flex-wrap">
|
||||
@@ -1,19 +1,18 @@
|
||||
<script>
|
||||
import { sysinfoStore } from '../lib/DataStores.js';
|
||||
import { translationsStore } from '../lib/TranslationService.js';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { sysinfoStore } from './DataStores.js';
|
||||
import { translationsStore } from './TranslationService.js';
|
||||
import Mask from './Mask.svelte'
|
||||
import { navigate } from 'svelte-navigator';
|
||||
import { wiki } from './Helpers';
|
||||
|
||||
let basepath = "/";
|
||||
let sysinfo = {};
|
||||
export let basepath = "/";
|
||||
export let sysinfo = {};
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
});
|
||||
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
|
||||
let loadingOrSaving = false;
|
||||
|
||||
async function handleSubmit(e) {
|
||||
@@ -37,7 +36,7 @@
|
||||
s.booting = res.reboot;
|
||||
return s;
|
||||
});
|
||||
push(basepath);
|
||||
navigate(basepath);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -50,7 +49,7 @@
|
||||
<hr/>
|
||||
<div class="my-3">
|
||||
{translations.consent?.one_click ?? "One-click"}<br/>
|
||||
<a href="https://github.com/UtilitechAS/amsreader-firmware/wiki/Data-collection-on-one-click-firmware-upgrade" target="_blank" class="text-blue-600 hover:text-blue-800">{translations.consent?.read_more ?? "Read more"}</a><br/>
|
||||
<a href="{wiki('Data-collection-on-one-click-firmware-upgrade')}" target="_blank" class="text-blue-600 hover:text-blue-800">{translations.consent?.read_more ?? "Read more"}</a><br/>
|
||||
<label><input type="radio" name="sf" value={1} checked={sysinfo.fwconsent === 1} class="rounded m-2" required/> {translations.consent?.yes ?? "Yes"}</label>
|
||||
<label><input type="radio" name="sf" value={2} checked={sysinfo.fwconsent === 2} class="rounded m-2" required/> {translations.consent?.no ?? "No"}</label><br/>
|
||||
</div>
|
||||
@@ -1,38 +1,25 @@
|
||||
<script>
|
||||
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from '../lib/Helpers.js';
|
||||
import PowerGauge from '../lib/PowerGauge.svelte';
|
||||
import VoltPlot from '../lib/VoltPlot.svelte';
|
||||
import ReactiveData from '../lib/ReactiveData.svelte';
|
||||
import AccountingData from '../lib/AccountingData.svelte';
|
||||
import PricePlot from '../lib/PricePlot.svelte';
|
||||
import DayPlot from '../lib/DayPlot.svelte';
|
||||
import MonthPlot from '../lib/MonthPlot.svelte';
|
||||
import TemperaturePlot from '../lib/TemperaturePlot.svelte';
|
||||
import TariffPeakChart from '../lib/TariffPeakChart.svelte';
|
||||
import RealtimePlot from '../lib/RealtimePlot.svelte';
|
||||
import PerPhasePlot from '../lib/PerPhasePlot.svelte';
|
||||
import { dataStore, sysinfoStore, importPricesStore, exportPricesStore, dayPlotStore, monthPlotStore, temperaturesStore, tariffStore } from '../lib/DataStores.js';
|
||||
import { translationsStore } from '../lib/TranslationService.js';
|
||||
import { ampcol, exportcol, metertype, uiVisibility, formatUnit, fmtnum, formatCurrency } from './Helpers.js';
|
||||
import PowerGauge from './PowerGauge.svelte';
|
||||
import VoltPlot from './VoltPlot.svelte';
|
||||
import ReactiveData from './ReactiveData.svelte';
|
||||
import AccountingData from './AccountingData.svelte';
|
||||
import PricePlot from './PricePlot.svelte';
|
||||
import DayPlot from './DayPlot.svelte';
|
||||
import MonthPlot from './MonthPlot.svelte';
|
||||
import TemperaturePlot from './TemperaturePlot.svelte';
|
||||
import TariffPeakChart from './TariffPeakChart.svelte';
|
||||
import RealtimePlot from './RealtimePlot.svelte';
|
||||
import PerPhasePlot from './PerPhasePlot.svelte';
|
||||
|
||||
let data = {}
|
||||
let sysinfo = {}
|
||||
let importPrices = {}
|
||||
let exportPrices = {}
|
||||
let dayPlot = {}
|
||||
let monthPlot = {}
|
||||
let temperatures = {};
|
||||
let translations = {};
|
||||
let tariffData = {};
|
||||
|
||||
dataStore.subscribe(v => data = v);
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
importPricesStore.subscribe(v => importPrices = v);
|
||||
exportPricesStore.subscribe(v => exportPrices = v);
|
||||
dayPlotStore.subscribe(v => dayPlot = v);
|
||||
monthPlotStore.subscribe(v => monthPlot = v);
|
||||
temperaturesStore.subscribe(v => temperatures = v);
|
||||
translationsStore.subscribe(v => translations = v);
|
||||
tariffStore.subscribe(v => tariffData = v);
|
||||
export let data = {}
|
||||
export let sysinfo = {}
|
||||
export let prices = {}
|
||||
export let dayPlot = {}
|
||||
export let monthPlot = {}
|
||||
export let temperatures = {};
|
||||
export let translations = {};
|
||||
export let tariffData = {};
|
||||
|
||||
let it,et,threePhase, l1e, l2e, l3e;
|
||||
$: {
|
||||
@@ -141,7 +128,7 @@
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.t, data.pr && (data.pr.startsWith("NO") || data.pr.startsWith("10YNO") || data.pr.startsWith('10Y1001A1001A4')))}
|
||||
<div class="cnt h-64">
|
||||
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} realtime={data.ea} translations={translations}/>
|
||||
<TariffPeakChart title={translations.dashboard?.tariffpeak ?? "Tariff peaks"} tariffData={tariffData} translations={translations}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.l, data.hm == 1)}
|
||||
@@ -149,20 +136,9 @@
|
||||
<RealtimePlot title={translations.dashboard?.realtime ?? "Real time"}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.p && !Number.isNaN(data.p))}
|
||||
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0)}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title="{translations.dashboard?.price_import ?? "Price import"}" json={importPrices} sysinfo={sysinfo}/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title={translations.dashboard?.price ?? "Price"} json={importPrices} sysinfo={sysinfo}/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if importPrices?.importExportPriceDifferent && (data.om || data.e > 0) && uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.pe))}
|
||||
{#if uiVisibility(sysinfo.ui.p, data.pe && !Number.isNaN(data.p))}
|
||||
<div class="cnt gwf">
|
||||
<PricePlot title={translations.dashboard?.price_export ?? "Price export"} json={exportPrices} sysinfo={sysinfo}/>
|
||||
<PricePlot title={translations.dashboard?.price ?? "Price"} json={prices} sysinfo={sysinfo}/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if uiVisibility(sysinfo.ui.d, dayPlot)}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { translationsStore } from './TranslationService';
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { navigate } from 'svelte-navigator';
|
||||
import Mask from './Mask.svelte'
|
||||
|
||||
export let prefix;
|
||||
@@ -59,7 +59,7 @@
|
||||
let res = (await response.json())
|
||||
|
||||
saving = false;
|
||||
push(basepath);
|
||||
navigate(basepath);
|
||||
}
|
||||
</script>
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
|
||||
@@ -70,7 +70,7 @@
|
||||
{#each importElements as el}
|
||||
<label class="flex w-60 m-1">
|
||||
<span class="in-pre">{el.name}</span>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
|
||||
<span class="in-post">kWh</span>
|
||||
</label>
|
||||
{/each}
|
||||
@@ -82,7 +82,7 @@
|
||||
{#each exportElements as el}
|
||||
<label class="flex w-60 m-1">
|
||||
<span class="in-pre">{el.name}</span>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.001" class="in-txt w-full text-right"/>
|
||||
<input name="{el.key}" bind:value={data[el.key]} type="number" step="0.01" class="in-txt w-full text-right"/>
|
||||
<span class="in-post">kWh</span>
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const dataStore = readable(data, (set) => {
|
||||
lastTemp = data.t;
|
||||
setTimeout(getTemperatures, 2000);
|
||||
}
|
||||
if(data.pe && data.p != lastPrice) {
|
||||
if(lastPrice == null && data.pe && data.p != null) {
|
||||
lastPrice = data.p;
|
||||
getPrices();
|
||||
}
|
||||
@@ -109,31 +109,41 @@ export const dataStore = readable(data, (set) => {
|
||||
}
|
||||
});
|
||||
|
||||
let priceFetchTimeout;
|
||||
let importPrices = {};
|
||||
export const importPricesStore = writable(importPrices);
|
||||
let exportprices = {};
|
||||
export const exportPricesStore = writable(exportprices);
|
||||
let prices = {};
|
||||
let priceShiftTimeout;
|
||||
export const pricesStore = writable(prices);
|
||||
|
||||
export async function shiftPrices() {
|
||||
let fetchUpdate = false;
|
||||
pricesStore.update(p => {
|
||||
for(var i = 0; i < 36; i++) {
|
||||
if(p[zeropad(i)] == null) {
|
||||
fetchUpdate = i < 12;
|
||||
break;
|
||||
}
|
||||
p[zeropad(i)] = p[zeropad(i+1)];
|
||||
}
|
||||
return p;
|
||||
});
|
||||
if(fetchUpdate) {
|
||||
getPrices();
|
||||
} else {
|
||||
let date = new Date();
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPrices() {
|
||||
if(priceFetchTimeout) {
|
||||
clearTimeout(priceFetchTimeout);
|
||||
priceFetchTimeout = 0;
|
||||
}
|
||||
{
|
||||
const response = await fetchWithTimeout("importprice.json");
|
||||
importPrices = (await response.json())
|
||||
importPricesStore.set(importPrices);
|
||||
}
|
||||
|
||||
if(importPrices?.importExportPriceDifferent) {
|
||||
const response = await fetchWithTimeout("exportprice.json");
|
||||
exportprices = (await response.json())
|
||||
exportPricesStore.set(exportprices);
|
||||
if(priceShiftTimeout) {
|
||||
clearTimeout(priceShiftTimeout);
|
||||
priceShiftTimeout = 0;
|
||||
}
|
||||
const response = await fetchWithTimeout("energyprice.json");
|
||||
prices = (await response.json())
|
||||
pricesStore.set(prices);
|
||||
|
||||
let date = new Date();
|
||||
priceFetchTimeout = setTimeout(getPrices, ((24-date.getHours())*3600000)+10)
|
||||
priceShiftTimeout = setTimeout(shiftPrices, ((60-date.getMinutes())*60000))
|
||||
}
|
||||
|
||||
let dayPlot = {};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script>
|
||||
import Mask from "../lib/Mask.svelte";
|
||||
import { translationsStore } from "../lib/TranslationService";
|
||||
import Mask from "./Mask.svelte";
|
||||
import { translationsStore } from "./TranslationService";
|
||||
|
||||
export let action;
|
||||
export let title;
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
@@ -12,12 +15,12 @@
|
||||
|
||||
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
|
||||
<div class="cnt">
|
||||
<strong>{translations.upload?.title ?? "Upload"} CA</strong>
|
||||
<strong>{translations.upload?.title ?? "Upload"} {title}</strong>
|
||||
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
|
||||
<form action="/mqtt-ca" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
|
||||
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
|
||||
<input name="file" type="file">
|
||||
<div class="w-full text-right mt-4">
|
||||
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
|
||||
<button type="submit" class="btn-pri"><p class="mb-4">{translations.btn?.upload ?? "Upload"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import { Link } from "svelte-navigator";
|
||||
import { sysinfoStore } from './DataStores.js';
|
||||
import { upgrade, upgradeWarningText } from './UpgradeHelper';
|
||||
import { boardtype, isBusPowered, wiki, bcol } from './Helpers.js';
|
||||
@@ -45,7 +46,7 @@
|
||||
<nav class="hdr">
|
||||
<div class="flex flex-wrap space-x-4 text-sm text-gray-300">
|
||||
<div class="flex text-lg text-gray-100 p-2">
|
||||
<a href={basepath}>AMS reader <span>{sysinfo.version}</span></a>
|
||||
<Link to="/">AMS reader <span>{sysinfo.version}</span></Link>
|
||||
</div>
|
||||
<div class="flex-none my-auto p-2 flex space-x-4">
|
||||
<div class="flex-none my-auto"><Uptime epoch={data.u}/></div>
|
||||
@@ -78,14 +79,14 @@
|
||||
</div>
|
||||
{#if sysinfo.vndcfg && sysinfo.usrcfg}
|
||||
<div class="flex-none px-1 mt-1" title={translations.header?.config ?? ""}>
|
||||
<a href="#/configuration"><GearIcon/></a>
|
||||
<Link to="/configuration"><GearIcon/></Link>
|
||||
</div>
|
||||
<div class="flex-none px-1 mt-1" title={translations.header?.status ?? ""}>
|
||||
<a href="#/status"><InfoIcon/></a>
|
||||
<Link to="/status"><InfoIcon/></Link>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-none px-1 mt-1" title={translations.header?.doc ?? ""}>
|
||||
<a href="{wiki('')}" target='_blank' rel="noreferrer"><HelpIcon/></a>
|
||||
<a href={wiki('')} target='_blank' rel="noreferrer"><HelpIcon/></a>
|
||||
</div>
|
||||
{#if sysinfo.upgrading}
|
||||
<div class="flex-none mr-3 mt-1 text-yellow-300">Upgrading to {sysinfo.upgrade.t}, {progress.toFixed(1)}%</div>
|
||||
|
||||
@@ -130,11 +130,7 @@ export function uiVisibility(choice, state) {
|
||||
}
|
||||
|
||||
export function wiki(page) {
|
||||
let ret = "https://wiki.amsleser.no";
|
||||
if(page) {
|
||||
ret += "/en/firmware#" + page;
|
||||
}
|
||||
return ret;
|
||||
return "https://github.com/UtilitechAS/amsreader-firmware/wiki/" + page;
|
||||
}
|
||||
|
||||
export function fmtnum(v,d) {
|
||||
@@ -149,11 +145,6 @@ export function addHours(date, hours) {
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMinutes(date, minutes) {
|
||||
date.setTime(date.getTime() + minutes * 60000);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getPriceSourceName(code) {
|
||||
if(code == "EOE") return "ENTSO-E";
|
||||
if(code == "HKS") return "hvakosterstrommen.no";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script>
|
||||
import { priceConfigStore, getPriceConfig } from '../lib/ConfigurationStore'
|
||||
import { translationsStore } from '../lib/TranslationService';
|
||||
import { wiki, zeropad } from '../lib/Helpers.js';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { priceConfigStore, getPriceConfig } from './ConfigurationStore'
|
||||
import { translationsStore } from './TranslationService';
|
||||
import { wiki, zeropad } from './Helpers.js';
|
||||
import Mask from './Mask.svelte'
|
||||
import { navigate } from 'svelte-navigator';
|
||||
|
||||
export let basepath = "/";
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
@@ -51,7 +53,7 @@
|
||||
let res = (await response.json())
|
||||
|
||||
saving = false;
|
||||
push("/configuration");
|
||||
navigate(basepath + "configuration");
|
||||
}
|
||||
|
||||
let toggleDay = function(arr, day) {
|
||||
@@ -106,7 +108,7 @@
|
||||
</script>
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.conf?.price?.title ?? "Price"}</strong>
|
||||
<a href="{wiki('configure-price-price-modifiers')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<a href="{wiki('Price-configuration')}" target="_blank" class="float-right">ⓘ</a>
|
||||
<hr class="m-3"/>
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
|
||||
<input type="hidden" name="r" value="true"/>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script>
|
||||
import { zeropad, addHours, addMinutes, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import { zeropad, addHours, getPriceSourceName, getPriceSourceUrl, formatCurrency } from './Helpers.js';
|
||||
import BarChart from './BarChart.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let title;
|
||||
export let json;
|
||||
@@ -13,124 +12,113 @@
|
||||
|
||||
let dark = document.documentElement.classList.contains('dark');
|
||||
|
||||
let cur = new Date();
|
||||
$: {
|
||||
let currency = json.currency;
|
||||
let hour = new Date().getUTCHours();
|
||||
let i = 0;
|
||||
let val = 0;
|
||||
let h = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
let cur = new Date();
|
||||
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
|
||||
for(i = hour; i<24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
for(i = 0; i < 24; i++) {
|
||||
val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addHours(cur, 1);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
let timeout;
|
||||
function scheduleUpdate() {
|
||||
cur = new Date();
|
||||
timeout = setTimeout(() => {
|
||||
scheduleUpdate();
|
||||
}, (15 - (cur.getMinutes() % 15)) * 60000);
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleUpdate();
|
||||
let points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
});
|
||||
|
||||
$: {
|
||||
if(json?.prices?.length > 0) {
|
||||
cur = new Date();
|
||||
let currency = json?.currency;
|
||||
let val = 0;
|
||||
let yTicks = [];
|
||||
let xTicks = [];
|
||||
let values = [];
|
||||
min = max = 0;
|
||||
addHours(cur, sysinfo.clock_offset - ((24 + cur.getHours() - cur.getUTCHours())%24));
|
||||
let i = json?.cursor ? json.cursor : 0;
|
||||
cur.setMinutes(Math.floor(cur.getMinutes()/json?.resolution)*json?.resolution,0,0);
|
||||
while(i < json?.prices?.length) {
|
||||
val = json.prices[i];
|
||||
if(val == null) break;
|
||||
xTicks.push({
|
||||
label: values.length > 0 && json?.resolution < 60 && cur.getMinutes() != 0 ? '' : zeropad(cur.getHours())
|
||||
});
|
||||
values.push(val*100);
|
||||
min = Math.min(min, val*100);
|
||||
max = Math.max(max, val*100);
|
||||
addMinutes(cur, json?.resolution);
|
||||
i++;
|
||||
}
|
||||
|
||||
let ret = formatCurrency(Math.max(Math.abs(min) / 100.0, Math.abs(max) / 100.0), currency);
|
||||
if(ret && ret[1] && ret[1] != currency) {
|
||||
currency = ret[1];
|
||||
min *= 100;
|
||||
max *= 100;
|
||||
for(i = 0; i < values.length; i++) {
|
||||
values[i] *= 100;
|
||||
}
|
||||
}
|
||||
|
||||
let points = [];
|
||||
for(i = 0; i < values.length; i++) {
|
||||
val = values[i];
|
||||
let disp = val * 0.01;
|
||||
let d = Math.abs(val) < 1000 ? 2 : 0;
|
||||
points.push({
|
||||
label: disp >= 0 ? disp.toFixed(d) : '',
|
||||
title: disp >= 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value: val >= 0 ? Math.abs(val) : 0,
|
||||
label2: disp < 0 ? disp.toFixed(d) : '',
|
||||
title2: disp < 0 ? disp.toFixed(2) + ' ' + currency : '',
|
||||
value2: val < 0 ? Math.abs(val) : 0,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
}
|
||||
let range = Math.max(max, Math.abs(min));
|
||||
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
if(min < 0) {
|
||||
min = Math.min((range/4)*-1, min);
|
||||
let yTicksNum = Math.ceil((Math.abs(min)/range) * 4);
|
||||
let yTickDistDown = min/yTicksNum;
|
||||
for(i = 1; i < yTicksNum+1; i++) {
|
||||
let val = (yTickDistDown*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
max = Math.max((range/4), max);
|
||||
let xTicksNum = Math.ceil((max/range) * 4);
|
||||
let yTickDistUp = max/xTicksNum;
|
||||
for(i = 0; i < xTicksNum+1; i++) {
|
||||
let val = (yTickDistUp*i);
|
||||
yTicks.push({
|
||||
value: val,
|
||||
label: (val/100).toFixed(2)
|
||||
});
|
||||
}
|
||||
|
||||
config = {
|
||||
title: title + " (" + currency + ")",
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 15, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
ticks: yTicks
|
||||
},
|
||||
x: {
|
||||
ticks: xTicks
|
||||
},
|
||||
points: points,
|
||||
link: {
|
||||
text: "Provided by: " + getPriceSourceName(json.source),
|
||||
url: getPriceSourceUrl(json.source),
|
||||
target: '_blank'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
{#if config.points && config.points.length > 0}
|
||||
<BarChart config={config} />
|
||||
{/if}
|
||||
<BarChart config={config} />
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
min = 0;
|
||||
max = 0;
|
||||
|
||||
/*
|
||||
console.log("\n--Realtime plot debug--")
|
||||
console.log("Data length: %d\nSize: %d", realtime?.data?.length, realtime?.size);
|
||||
console.log("Height: %d\nWidth: %d\nBar width: %s", heightAvailable, widthAvailable, barWidth);
|
||||
*/
|
||||
|
||||
if(realtime.data && heightAvailable > 10 && widthAvailable > 100 && barWidth > 0.1) {
|
||||
visible = true;
|
||||
for(let p in realtime.data) {
|
||||
@@ -84,6 +90,9 @@
|
||||
} else {
|
||||
visible = false;
|
||||
}
|
||||
/*
|
||||
console.log("Min: %d\nMax: %d\nShow: %s", min, max, visible);
|
||||
*/
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script>
|
||||
import { sysinfoStore, networksStore } from '../lib/DataStores.js';
|
||||
import { translationsStore } from '../lib/TranslationService.js';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import SubnetOptions from '../lib/SubnetOptions.svelte';
|
||||
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from '../lib/Helpers.js';
|
||||
import { sysinfoStore, networksStore } from './DataStores.js';
|
||||
import { translationsStore } from './TranslationService.js';
|
||||
import Mask from './Mask.svelte'
|
||||
import SubnetOptions from './SubnetOptions.svelte';
|
||||
import { scanForDevice, charAndNumPattern, asciiPatternExt, ipPattern } from './Helpers.js';
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
@@ -16,8 +16,7 @@
|
||||
networks = update;
|
||||
});
|
||||
|
||||
let sysinfo = {}
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
export let sysinfo = {}
|
||||
|
||||
let staticIp = false;
|
||||
let connectionMode = 1;
|
||||
@@ -1,70 +1,15 @@
|
||||
<script>
|
||||
import { metertype, boardtype, isBusPowered, getBaseChip, wiki } from '../lib/Helpers.js';
|
||||
import { getSysinfo, sysinfoStore, dataStore } from '../lib/DataStores.js';
|
||||
import { upgrade, upgradeWarningText } from '../lib/UpgradeHelper';
|
||||
import { translationsStore } from '../lib/TranslationService.js';
|
||||
import Clock from '../lib/Clock.svelte';
|
||||
import Mask from '../lib/Mask.svelte';
|
||||
import { scanForDevice } from '../lib/Helpers.js';
|
||||
import { metertype, boardtype, isBusPowered, getBaseChip } from './Helpers.js';
|
||||
import { getSysinfo, sysinfoStore } from './DataStores.js';
|
||||
import { upgrade, upgradeWarningText } from './UpgradeHelper';
|
||||
import { translationsStore } from './TranslationService.js';
|
||||
import { Link } from 'svelte-navigator';
|
||||
import Clock from './Clock.svelte';
|
||||
import Mask from './Mask.svelte';
|
||||
import { scanForDevice } from './Helpers.js';
|
||||
|
||||
let data;
|
||||
let sysinfo;
|
||||
|
||||
dataStore.subscribe(v => data = v);
|
||||
sysinfoStore.subscribe(v => sysinfo = v);
|
||||
|
||||
// Format IPv6 address to compact form (RFC 5952)
|
||||
const formatIPv6 = (addr) => {
|
||||
if (!addr) return addr;
|
||||
|
||||
// Split into groups
|
||||
const groups = addr.toLowerCase().split(':');
|
||||
|
||||
// Remove leading zeros from each group
|
||||
const normalized = groups.map(g => g.replace(/^0+/, '') || '0');
|
||||
|
||||
// Find longest sequence of consecutive zeros
|
||||
let maxStart = -1, maxLen = 0;
|
||||
let currStart = -1, currLen = 0;
|
||||
|
||||
for (let i = 0; i < normalized.length; i++) {
|
||||
if (normalized[i] === '0') {
|
||||
if (currStart === -1) currStart = i;
|
||||
currLen++;
|
||||
} else {
|
||||
if (currLen > maxLen) {
|
||||
maxStart = currStart;
|
||||
maxLen = currLen;
|
||||
}
|
||||
currStart = -1;
|
||||
currLen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check final sequence
|
||||
if (currLen > maxLen) {
|
||||
maxStart = currStart;
|
||||
maxLen = currLen;
|
||||
}
|
||||
|
||||
// Only compress if we have 2 or more consecutive zeros
|
||||
if (maxLen > 1) {
|
||||
const before = normalized.slice(0, maxStart);
|
||||
const after = normalized.slice(maxStart + maxLen);
|
||||
|
||||
if (before.length === 0 && after.length === 0) {
|
||||
return '::';
|
||||
} else if (before.length === 0) {
|
||||
return '::' + after.join(':');
|
||||
} else if (after.length === 0) {
|
||||
return before.join(':') + '::';
|
||||
} else {
|
||||
return before.join(':') + '::' + after.join(':');
|
||||
}
|
||||
}
|
||||
|
||||
return normalized.join(':');
|
||||
};
|
||||
export let data;
|
||||
export let sysinfo;
|
||||
|
||||
let cfgItems = [{
|
||||
name: 'WiFi',
|
||||
@@ -127,11 +72,11 @@
|
||||
}
|
||||
|
||||
let firmwareFileInput;
|
||||
let firmwareFiles = null;
|
||||
let firmwareFiles = [];
|
||||
let firmwareUploading = false;
|
||||
|
||||
let configFileInput;
|
||||
let configFiles = null;
|
||||
let configFiles = [];
|
||||
let configUploading = false;
|
||||
|
||||
getSysinfo();
|
||||
@@ -163,17 +108,8 @@
|
||||
});
|
||||
};
|
||||
|
||||
function changeFirmwareChannel() {
|
||||
const formData = new FormData();
|
||||
formData.append('channel', sysinfo.upgrade.c);
|
||||
fetch('fwchannel', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
};
|
||||
|
||||
$: {
|
||||
if(configFiles && configFiles.length == 1) {
|
||||
if(configFiles.length == 1) {
|
||||
let file = configFiles[0];
|
||||
let reader = new FileReader();
|
||||
let parseConfigFile = ( e ) => {
|
||||
@@ -200,7 +136,7 @@
|
||||
{translations.status?.device?.chip ?? "Chip"}: {sysinfo.chip} {#if sysinfo.cpu}({sysinfo.cpu}MHz){/if}
|
||||
</div>
|
||||
<div class="my-2">
|
||||
{translations.status?.device?.device ?? "Device"}: <a href="#/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</a>
|
||||
{translations.status?.device?.device ?? "Device"}: <Link to="/vendor">{boardtype(sysinfo.chip, sysinfo.board)}</Link>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
{translations.status?.device?.mac ?? "MAC"}: {sysinfo.mac}
|
||||
@@ -223,9 +159,9 @@
|
||||
{/if}
|
||||
{#if data?.a}
|
||||
<div class="my-2">
|
||||
<a href="#/consent">
|
||||
<Link to="/consent">
|
||||
<span class="btn-pri-sm">{translations.status?.device?.btn_consents ?? "Consents"}</span>
|
||||
</a>
|
||||
</Link>
|
||||
<button on:click={askReboot} class="btn-yellow-sm float-right">{translations.btn?.reboot ?? "Reboot"}</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -262,48 +198,36 @@
|
||||
</div>
|
||||
{#if sysinfo.net.ipv6}
|
||||
<div class="my-2">
|
||||
IPv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.ipv6)}</span>
|
||||
IPv6: <span style="font-size: 14px;">{sysinfo.net.ipv6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>
|
||||
</div>
|
||||
<div class="my-2">
|
||||
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns1v6)}</span>{/if}
|
||||
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{formatIPv6(sysinfo.net.dns2v6)}</span>{/if}
|
||||
{#if sysinfo.net.dns1v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns1v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
|
||||
{#if sysinfo.net.dns2v6}DNSv6: <span style="font-size: 14px;">{sysinfo.net.dns2v6.replace(/\b:?(?:0+:?){2,}/, '::')}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="cnt">
|
||||
<strong class="text-sm">{translations.status?.firmware?.title ?? "Firmware"}</strong>
|
||||
<a href="{wiki('statusinformation-screen')}" target="_blank" class="float-right">ⓘ</a>
|
||||
{#if sysinfo.fwconsent === 1}
|
||||
<div class="my-2">
|
||||
Channel:
|
||||
<select class="in-s w-full" bind:value={sysinfo.upgrade.c} on:change={changeFirmwareChannel}>
|
||||
<option value={0}>Stable</option>
|
||||
<option value={1}>Early</option>
|
||||
<option value={2}>Release Candidate</option>
|
||||
<option value={3} disabled>Snapshot</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="my-2">
|
||||
{translations.status?.firmware?.installed ?? "Installed"}: {sysinfo.version}
|
||||
</div>
|
||||
{#if sysinfo.upgrade.t && sysinfo.upgrade.t != sysinfo.version && sysinfo.upgrade.e != 0 && sysinfo.upgrade.e != 123}
|
||||
<div class="my-2">
|
||||
<div class="bd-yellow">
|
||||
{(translations.status?.firmware?.failed ?? "Upgrade from {0} to {1} failed").replace('{0}', sysinfo.upgrade.f).replace('{1}', sysinfo.upgrade.t)}
|
||||
{(translations.errors?.upgrade?.[sysinfo.upgrade.e] ?? sysinfo.upgrade.e)}
|
||||
</div>
|
||||
<div class="my-2">
|
||||
<div class="bd-yellow">
|
||||
{(translations.status?.firmware?.failed ?? "Upgrade from {0} to {1} failed").replace('{0}', sysinfo.upgrade.f).replace('{1}', sysinfo.upgrade.t)}
|
||||
{(translations.errors?.upgrade?.[sysinfo.upgrade.e] ?? sysinfo.upgrade.e)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if sysinfo.upgrade.n}
|
||||
<div class="my-2 flex">
|
||||
{translations.status?.firmware?.latest ?? "Latest"}:
|
||||
<a href={"https://github.com/UtilitechAS/amsreader-firmware/releases/tag/" + sysinfo.upgrade.n} class="ml-2 text-blue-600 hover:text-blue-800" target='_blank' rel="noreferrer">{sysinfo.upgrade.n}</a>
|
||||
{#if (sysinfo.security == 0 || data.a) && sysinfo.fwconsent === 1 && sysinfo.upgrade.n && sysinfo.upgrade.n != sysinfo.version}
|
||||
<div class="flex-none ml-2 text-green-500" title={translations.status?.firmware?.install ?? "Install"}>
|
||||
<button on:click={askUpgrade}>⇓</button>
|
||||
</div>
|
||||
<div class="flex-none ml-2 text-green-500" title={translations.status?.firmware?.install ?? "Install"}>
|
||||
<button on:click={askUpgrade}>⇓</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if sysinfo.fwconsent === 2}
|
||||
@@ -313,22 +237,22 @@
|
||||
{/if}
|
||||
{/if}
|
||||
{#if (sysinfo.security == 0 || data.a) && isBusPowered(sysinfo.board) }
|
||||
<div class="bd-red">
|
||||
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
|
||||
</div>
|
||||
<div class="bd-red">
|
||||
{upgradeWarningText(boardtype(sysinfo.chip, sysinfo.board))}
|
||||
</div>
|
||||
{/if}
|
||||
{#if sysinfo.security == 0 || data.a}
|
||||
<div class="my-2 flex">
|
||||
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
|
||||
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
|
||||
{#if !firmwareFiles || firmwareFiles.length == 0}
|
||||
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
|
||||
{:else}
|
||||
{firmwareFiles[0].name}
|
||||
<button type="submit" class="btn-pri-sm float-right ml-2">{translations.btn?.upload ?? "Upload"}</button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
<div class="my-2 flex">
|
||||
<form action="firmware" enctype="multipart/form-data" method="post" on:submit={() => firmwareUploading=true} autocomplete="off">
|
||||
<input style="display:none" name="file" type="file" accept=".bin" bind:this={firmwareFileInput} bind:files={firmwareFiles}>
|
||||
{#if firmwareFiles.length == 0}
|
||||
<button type="button" on:click={()=>{firmwareFileInput.click();}} class="btn-pri-sm float-right">{translations.status?.firmware?.btn_select_file ?? "Select file"}</button>
|
||||
{:else}
|
||||
{firmwareFiles[0].name}
|
||||
<button type="submit" class="btn-pri-sm float-right ml-2">{translations.btn?.upload ?? "Upload"}</button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if sysinfo.security == 0 || data.a}
|
||||
@@ -341,13 +265,13 @@
|
||||
{/each}
|
||||
<label class="my-1 mx-3 col-span-2"><input type="checkbox" class="rounded" name="ic" value="true"/> {translations.status?.backup?.secrets ?? "Include secrets"}<br/><small>{translations.status?.backup?.secrets_desc ?? ""}</small></label>
|
||||
</div>
|
||||
{#if !configFiles || configFiles.length == 0}
|
||||
{#if configFiles.length == 0}
|
||||
<button type="submit" class="btn-pri-sm float-right">{translations.status?.backup?.btn_download ?? "Download"}</button>
|
||||
{/if}
|
||||
</form>
|
||||
<form on:submit|preventDefault={uploadConfigFile} autocomplete="off">
|
||||
<input style="display:none" name="file" type="file" accept=".cfg" bind:this={configFileInput} bind:files={configFiles}>
|
||||
{#if !configFiles || configFiles.length == 0}
|
||||
{#if configFiles.length == 0}
|
||||
<button type="button" on:click={()=>{configFileInput.click();}} class="btn-pri-sm">{translations.status?.backup?.btn_select_file ?? "Select file"}</button>
|
||||
{:else}
|
||||
{configFiles[0].name}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { ampcol, zeropad } from './Helpers.js';
|
||||
import { zeropad } from './Helpers.js';
|
||||
import BarChart from './BarChart.svelte';
|
||||
|
||||
export let title;
|
||||
@@ -12,7 +12,6 @@
|
||||
let min = 0;
|
||||
|
||||
export let tariffData;
|
||||
export let realtime;
|
||||
|
||||
$: {
|
||||
let i = 0;
|
||||
@@ -25,42 +24,16 @@
|
||||
label: 0
|
||||
});
|
||||
|
||||
if(tariffData && !isNaN(realtime?.h?.u)) {
|
||||
points.push({
|
||||
label: realtime.h.u.toFixed(2),
|
||||
value: realtime.h.u,
|
||||
title: (translations.common?.now ?? "Now") + ': ' + realtime.h.u.toFixed(2) + ' kWh',
|
||||
color: ampcol(realtime.h.u/tariffData.c*100.0)
|
||||
});
|
||||
xTicks.push({
|
||||
label: translations.common?.now ?? "Now"
|
||||
});
|
||||
}
|
||||
|
||||
if(tariffData && tariffData.p) {
|
||||
for(i = 0; i < tariffData.p.length; i++) {
|
||||
let peak = tariffData.p[i];
|
||||
|
||||
let peakTitle = "";
|
||||
let daylabel = "-";
|
||||
if(peak.d > 0) {
|
||||
daylabel = zeropad(peak.d) + ".";
|
||||
peakTitle = zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1));
|
||||
if(tariffData.p.length < 4) {
|
||||
daylabel = peakTitle;
|
||||
}
|
||||
}
|
||||
if(!isNaN(peak.h))
|
||||
peakTitle = peakTitle + ' ' + zeropad(peak.h) + ':00';
|
||||
peakTitle = peakTitle + ': ' + peak.v.toFixed(2) + ' kWh';
|
||||
points.push({
|
||||
label: peak.v.toFixed(2),
|
||||
value: peak.v,
|
||||
title: peakTitle,
|
||||
color: dark ? '#5c2da5' : '#7c3aed'
|
||||
});
|
||||
xTicks.push({
|
||||
label: daylabel
|
||||
label: peak.d > 0 ? zeropad(peak.d) + "." + (translations.months ? translations.months?.[new Date().getMonth()] : zeropad(new Date().getMonth()+1)) : "-"
|
||||
})
|
||||
max = Math.max(max, peak.v);
|
||||
}
|
||||
@@ -98,7 +71,7 @@
|
||||
config = {
|
||||
title: title,
|
||||
dark: document.documentElement.classList.contains('dark'),
|
||||
padding: { top: 20, right: 20, bottom: 20, left: 20 },
|
||||
padding: { top: 20, right: 35, bottom: 20, left: 35 },
|
||||
y: {
|
||||
min: min,
|
||||
max: max,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script>
|
||||
export let title;
|
||||
export let x;
|
||||
export let y;
|
||||
|
||||
let width;
|
||||
let height;
|
||||
</script>
|
||||
<div
|
||||
class="tooltip"
|
||||
style="top: {y - height - 10}px; left: {x - (width / 2)}px;"
|
||||
bind:clientHeight={height}
|
||||
bind:clientWidth={width}
|
||||
>{title}</div>
|
||||
@@ -1,11 +1,12 @@
|
||||
<script>
|
||||
import { sysinfoStore } from '../lib/DataStores.js';
|
||||
import BoardTypeSelectOptions from '../lib/BoardTypeSelectOptions.svelte';
|
||||
import UartSelectOptions from '../lib/UartSelectOptions.svelte';
|
||||
import Mask from '../lib/Mask.svelte'
|
||||
import { push } from 'svelte-spa-router';
|
||||
import { sysinfoStore } from './DataStores.js';
|
||||
import BoardTypeSelectOptions from './BoardTypeSelectOptions.svelte';
|
||||
import UartSelectOptions from './UartSelectOptions.svelte';
|
||||
import Mask from './Mask.svelte'
|
||||
import { navigate } from 'svelte-navigator';
|
||||
|
||||
let sysinfo = {};
|
||||
export let basepath = "/";
|
||||
export let sysinfo = {};
|
||||
|
||||
let loadingOrSaving = false;
|
||||
async function handleSubmit(e) {
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
return s;
|
||||
});
|
||||
push(sysinfo.usrcfg ? "/" : "/setup");
|
||||
navigate(basepath + (sysinfo.usrcfg ? "" : "setup"));
|
||||
}
|
||||
|
||||
let cc = true;
|
||||
@@ -1,41 +0,0 @@
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
|
||||
export function tooltip(element) {
|
||||
let title;
|
||||
let tooltipComponent;
|
||||
|
||||
function click(event) {
|
||||
if(tooltipComponent) tooltipComponent.$destroy();
|
||||
|
||||
title = element.dataset.title || element.getAttribute('title');
|
||||
var rect = element.getBoundingClientRect();
|
||||
|
||||
tooltipComponent = new Tooltip({
|
||||
props: {
|
||||
title: title,
|
||||
x: rect.left + window.scrollX + (rect.width / 2),
|
||||
y: rect.top + window.scrollY,
|
||||
},
|
||||
target: document.body,
|
||||
});
|
||||
}
|
||||
|
||||
function mouseLeave() {
|
||||
if(tooltipComponent) {
|
||||
setTimeout(() => {
|
||||
tooltipComponent.$destroy();
|
||||
tooltipComponent = null;
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
element.addEventListener('click', click);
|
||||
element.addEventListener('mouseleave', mouseLeave);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
element.removeEventListener('click', click);
|
||||
element.removeEventListener('mouseleave', mouseLeave);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import "./app.postcss";
|
||||
import { mount } from "svelte";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = mount(App, {
|
||||
const app = new App({
|
||||
target: document.getElementById("app"),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import DataEdit from '../lib/DataEdit.svelte';
|
||||
import { dayPlotStore } from '../lib/DataStores.js';
|
||||
|
||||
let basepath = "/";
|
||||
let dayPlot;
|
||||
|
||||
dayPlotStore.subscribe(v => dayPlot = v);
|
||||
</script>
|
||||
|
||||
<DataEdit prefix="UTC Hour" data={dayPlot} url="/dayplot" {basepath} />
|
||||
@@ -1,11 +0,0 @@
|
||||
<script>
|
||||
import DataEdit from '../lib/DataEdit.svelte';
|
||||
import { monthPlotStore } from '../lib/DataStores.js';
|
||||
|
||||
let basepath = "/";
|
||||
let monthPlot;
|
||||
|
||||
monthPlotStore.subscribe(v => monthPlot = v);
|
||||
</script>
|
||||
|
||||
<DataEdit prefix="Day" data={monthPlot} url="/monthplot" {basepath} />
|
||||
@@ -1,25 +0,0 @@
|
||||
<script>
|
||||
import Mask from "../lib/Mask.svelte";
|
||||
import { translationsStore } from "../lib/TranslationService";
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
});
|
||||
|
||||
let uploading = false;
|
||||
</script>
|
||||
|
||||
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
|
||||
<div class="cnt">
|
||||
<strong>{translations.upload?.title ?? "Upload"} certificate</strong>
|
||||
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
|
||||
<form action="/mqtt-cert" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
|
||||
<input name="file" type="file">
|
||||
<div class="w-full text-right mt-4">
|
||||
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script>
|
||||
import Mask from "../lib/Mask.svelte";
|
||||
import { translationsStore } from "../lib/TranslationService";
|
||||
|
||||
let translations = {};
|
||||
translationsStore.subscribe(update => {
|
||||
translations = update;
|
||||
});
|
||||
|
||||
let uploading = false;
|
||||
</script>
|
||||
|
||||
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
|
||||
<div class="cnt">
|
||||
<strong>{translations.upload?.title ?? "Upload"} private key</strong>
|
||||
<p class="mb-4">{translations.upload?.desc ?? ""}</p>
|
||||
<form action="/mqtt-key" enctype="multipart/form-data" method="post" on:submit={() => uploading=true} autocomplete="off">
|
||||
<input name="file" type="file">
|
||||
<div class="w-full text-right mt-4">
|
||||
<button type="submit" class="btn-pri">{translations.btn?.upload ?? "Upload"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Mask active={uploading} message={translations.upload?.mask ?? "Uploading"}/>
|
||||
@@ -16,8 +16,7 @@
|
||||
"day" : "day",
|
||||
"days" : "days",
|
||||
"month" : "month",
|
||||
"unknown" : "Unknown",
|
||||
"now" : "Now"
|
||||
"unknown" : "Unknown"
|
||||
},
|
||||
"btn" : {
|
||||
"reboot" : "Reboot",
|
||||
@@ -41,8 +40,6 @@
|
||||
"tariffpeak" : "Tariff peaks",
|
||||
"realtime" : "Real-time plot",
|
||||
"price" : "Future energy price",
|
||||
"price_import" : "Future import price",
|
||||
"price_export" : "Future export price",
|
||||
"day" : "Energy use last 24 hours",
|
||||
"month" : "Energy use last {0} days",
|
||||
"temperature" : "Temperature sensors"
|
||||
|
||||
@@ -1,62 +1,43 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
// Try to import local config, fall back to default if not found
|
||||
let localConfig = { proxyTarget: "http://192.168.4.1" };
|
||||
try {
|
||||
const imported = await import('./vite.config.local.js');
|
||||
localConfig = imported.default;
|
||||
} catch (e) {
|
||||
console.log('No vite.config.local.js found, using default proxy target:', localConfig.proxyTarget);
|
||||
console.log('Copy vite.config.local.example.js to vite.config.local.js to customize');
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: '.',
|
||||
minify: 'esbuild',
|
||||
target: 'es2020',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: '[name][extname]',
|
||||
chunkFileNames: '[name].js',
|
||||
entryFileNames: '[name].js',
|
||||
manualChunks: undefined
|
||||
entryFileNames: '[name].js'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [svelte({
|
||||
compilerOptions: {
|
||||
dev: false
|
||||
}
|
||||
})],
|
||||
plugins: [svelte()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/data.json": localConfig.proxyTarget,
|
||||
"/energyprice.json": localConfig.proxyTarget,
|
||||
"/importprice.json": localConfig.proxyTarget,
|
||||
"/exportprice.json": localConfig.proxyTarget,
|
||||
"/dayplot.json": localConfig.proxyTarget,
|
||||
"/monthplot.json": localConfig.proxyTarget,
|
||||
"/temperature.json": localConfig.proxyTarget,
|
||||
"/sysinfo.json": localConfig.proxyTarget,
|
||||
"/configuration.json": localConfig.proxyTarget,
|
||||
"/tariff.json": localConfig.proxyTarget,
|
||||
"/realtime.json": localConfig.proxyTarget,
|
||||
"/priceconfig.json": localConfig.proxyTarget,
|
||||
"/translations.json": localConfig.proxyTarget,
|
||||
"/cloudkey.json": localConfig.proxyTarget,
|
||||
"/wifiscan.json": localConfig.proxyTarget,
|
||||
"/save": localConfig.proxyTarget,
|
||||
"/reboot": localConfig.proxyTarget,
|
||||
"/configfile": localConfig.proxyTarget,
|
||||
"/upgrade": localConfig.proxyTarget,
|
||||
"/mqtt-ca": localConfig.proxyTarget,
|
||||
"/mqtt-cert": localConfig.proxyTarget,
|
||||
"/mqtt-key": localConfig.proxyTarget,
|
||||
"/logo.svg": localConfig.proxyTarget,
|
||||
"/data.json": "http://192.168.21.122",
|
||||
"/energyprice.json": "http://192.168.21.122",
|
||||
"/dayplot.json": "http://192.168.21.122",
|
||||
"/monthplot.json": "http://192.168.21.122",
|
||||
"/temperature.json": "http://192.168.21.122",
|
||||
"/sysinfo.json": "http://192.168.21.122",
|
||||
"/configuration.json": "http://192.168.21.122",
|
||||
"/tariff.json": "http://192.168.21.122",
|
||||
"/realtime.json": "http://192.168.21.122",
|
||||
"/priceconfig.json": "http://192.168.21.122",
|
||||
"/translations.json": "http://192.168.21.122",
|
||||
"/cloudkey.json": "http://192.168.21.122",
|
||||
"/wifiscan.json": "http://192.168.21.122",
|
||||
"/save": "http://192.168.21.122",
|
||||
"/reboot": "http://192.168.21.122",
|
||||
"/configfile": "http://192.168.21.122",
|
||||
"/upgrade": "http://192.168.21.122",
|
||||
"/mqtt-ca": "http://192.168.21.122",
|
||||
"/mqtt-cert": "http://192.168.21.122",
|
||||
"/mqtt-key": "http://192.168.21.122",
|
||||
"/logo.svg": "http://192.168.21.122",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copy this file to vite.config.local.js and update with your device's IP address
|
||||
// vite.config.local.js is ignored by git so your settings won't be committed
|
||||
|
||||
export default {
|
||||
// The IP address of your AMS reader device for local development
|
||||
proxyTarget: "http://192.168.4.1"
|
||||
}
|
||||
@@ -125,10 +125,7 @@ private:
|
||||
void dataJson();
|
||||
void dayplotJson();
|
||||
void monthplotJson();
|
||||
void energyPriceJson(); // Deprecated
|
||||
void importPriceJson();
|
||||
void exportPriceJson();
|
||||
void priceJson(uint8_t direction);
|
||||
void energyPriceJson();
|
||||
void temperatureJson();
|
||||
void tariffJson();
|
||||
void realtimeJson();
|
||||
@@ -146,7 +143,6 @@ private:
|
||||
void firmwarePost();
|
||||
void firmwareUpload();
|
||||
void isAliveCheck();
|
||||
void fwchannel();
|
||||
|
||||
void mqttCaUpload();
|
||||
void mqttCaDelete();
|
||||
|
||||
@@ -32,6 +32,5 @@
|
||||
"g": %d
|
||||
},
|
||||
"b": %.1f
|
||||
},
|
||||
"p": %d
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
"e": %s,
|
||||
"t": "%s",
|
||||
"r": "%s",
|
||||
"c": "%s",
|
||||
"m": %d
|
||||
"c": "%s"
|
||||
},
|
||||
@@ -53,7 +53,6 @@
|
||||
"boot_reason": %d,
|
||||
"ex_cause": %d,
|
||||
"upgrade": {
|
||||
"c": %d,
|
||||
"e": %d,
|
||||
"f": "%s",
|
||||
"t": "%s",
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "FirmwareVersion.h"
|
||||
#include "base64.h"
|
||||
#include "hexutils.h"
|
||||
#include "AmsJsonGenerator.h"
|
||||
|
||||
#include "html/index_html.h"
|
||||
#include "html/index_css.h"
|
||||
@@ -127,8 +126,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa
|
||||
server.on(context + F("/dayplot.json"), HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this));
|
||||
server.on(context + F("/monthplot.json"), HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this));
|
||||
server.on(context + F("/energyprice.json"), HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this));
|
||||
server.on(context + F("/importprice.json"), HTTP_GET, std::bind(&AmsWebServer::importPriceJson, this));
|
||||
server.on(context + F("/exportprice.json"), HTTP_GET, std::bind(&AmsWebServer::exportPriceJson, this));
|
||||
server.on(context + F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this));
|
||||
server.on(context + F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this));
|
||||
server.on(context + F("/realtime.json"), HTTP_GET, std::bind(&AmsWebServer::realtimeJson, this));
|
||||
@@ -145,7 +142,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, AmsDa
|
||||
server.on(context + F("/firmware"), HTTP_GET, std::bind(&AmsWebServer::firmwareHtml, this));
|
||||
server.on(context + F("/firmware"), HTTP_POST, std::bind(&AmsWebServer::firmwarePost, this), std::bind(&AmsWebServer::firmwareUpload, this));
|
||||
server.on(context + F("/is-alive"), HTTP_GET, std::bind(&AmsWebServer::isAliveCheck, this));
|
||||
server.on(context + F("/fwchannel"), HTTP_POST, std::bind(&AmsWebServer::fwchannel, this));
|
||||
|
||||
server.on(context + F("/reset"), HTTP_POST, std::bind(&AmsWebServer::factoryResetPost, this));
|
||||
|
||||
@@ -480,7 +476,6 @@ void AmsWebServer::sysinfoJson() {
|
||||
ESP.getResetInfoPtr()->reason,
|
||||
ESP.getResetInfoPtr()->exccause,
|
||||
#endif
|
||||
sys.firmwareChannel,
|
||||
upinfo.errorCode,
|
||||
upinfo.fromVersion,
|
||||
upinfo.toVersion,
|
||||
@@ -516,9 +511,10 @@ void AmsWebServer::sysinfoJson() {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Rebooting\n"));
|
||||
debugger->flush();
|
||||
rdc->cause = REBOOT_CAUSE_WEB_SYSINFO_JSON;
|
||||
delay(1000);
|
||||
rdc->cause = 1;
|
||||
ESP.restart();
|
||||
performRestart = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,8 +583,8 @@ void AmsWebServer::dataJson() {
|
||||
mqttStatus = 3;
|
||||
}
|
||||
|
||||
float price = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_IMPORT);
|
||||
float exportPrice = ps == NULL ? PRICE_NO_VALUE : ps->getCurrentPrice(PRICE_DIRECTION_EXPORT);
|
||||
float price = ea->getPriceForHour(PRICE_DIRECTION_IMPORT, 0);
|
||||
float exportPrice = ea->getPriceForHour(PRICE_DIRECTION_EXPORT, 0);
|
||||
|
||||
String peaks = "";
|
||||
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
|
||||
@@ -686,7 +682,12 @@ void AmsWebServer::dayplotJson() {
|
||||
if(ds == NULL) {
|
||||
notFound();
|
||||
} else {
|
||||
AmsJsonGenerator::generateDayPlotJson(ds, buf, BufferSize);
|
||||
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 0; i < 24; i++) {
|
||||
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getHourImport(i) / 1000.0, i, ds->getHourExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
|
||||
|
||||
addConditionalCloudHeaders();
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
@@ -704,7 +705,12 @@ void AmsWebServer::monthplotJson() {
|
||||
if(ds == NULL) {
|
||||
notFound();
|
||||
} else {
|
||||
AmsJsonGenerator::generateMonthPlotJson(ds, buf, BufferSize);
|
||||
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"unit\":\"kwh\""));
|
||||
for(uint8_t i = 1; i < 32; i++) {
|
||||
pos += snprintf_P(buf+pos, BufferSize-pos, PSTR(",\"i%02d\":%.3f,\"e%02d\":%.3f"), i, ds->getDayImport(i) / 1000.0, i, ds->getDayExport(i) / 1000.0);
|
||||
}
|
||||
snprintf_P(buf+pos, BufferSize-pos, PSTR("}"));
|
||||
|
||||
addConditionalCloudHeaders();
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
@@ -715,24 +721,18 @@ void AmsWebServer::monthplotJson() {
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
void AmsWebServer::energyPriceJson() {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
|
||||
if(ps == NULL || !ps->hasPrice()) {
|
||||
notFound();
|
||||
return;
|
||||
}
|
||||
|
||||
float prices[36];
|
||||
for(int i = 0; i < 36; i++) {
|
||||
prices[i] = ps->getPriceForRelativeHour(PRICE_DIRECTION_IMPORT, i);
|
||||
prices[i] = ps == NULL ? PRICE_NO_VALUE : ps->getValueForHour(PRICE_DIRECTION_IMPORT, i);
|
||||
}
|
||||
|
||||
uint16_t pos = snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\""),
|
||||
ps->getCurrency(),
|
||||
ps->getSource()
|
||||
ps == NULL ? "" : ps->getCurrency(),
|
||||
ps == NULL ? "" : ps->getSource()
|
||||
);
|
||||
|
||||
for(uint8_t i = 0;i < 36; i++) {
|
||||
@@ -753,59 +753,6 @@ void AmsWebServer::energyPriceJson() {
|
||||
server.send(200, MIME_JSON, buf);
|
||||
}
|
||||
|
||||
void AmsWebServer::importPriceJson() {
|
||||
priceJson(PRICE_DIRECTION_IMPORT);
|
||||
}
|
||||
|
||||
void AmsWebServer::exportPriceJson() {
|
||||
priceJson(PRICE_DIRECTION_EXPORT);
|
||||
}
|
||||
|
||||
void AmsWebServer::priceJson(uint8_t direction) {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
|
||||
if(ps == NULL || !ps->hasPrice()) {
|
||||
notFound();
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t numberOfPoints = ps->getNumberOfPointsAvailable();
|
||||
|
||||
float prices[numberOfPoints];
|
||||
for(int i = 0; i < numberOfPoints; i++) {
|
||||
prices[i] = ps->getPricePoint(direction, i);
|
||||
}
|
||||
|
||||
snprintf_P(buf, BufferSize, PSTR("{\"currency\":\"%s\",\"source\":\"%s\",\"resolution\":%d,\"direction\":\"%s\",\"cursor\":%d,\"importExportPriceDifferent\":%s,\"prices\":["),
|
||||
ps->getCurrency(),
|
||||
ps->getSource(),
|
||||
ps->getResolutionInMinutes(),
|
||||
direction == PRICE_DIRECTION_IMPORT ? "import" : direction == PRICE_DIRECTION_EXPORT ? "export" : "both",
|
||||
ps->getCurrentPricePointIndex(),
|
||||
ps->isExportPricesDifferentFromImport() ? "true" : "false"
|
||||
);
|
||||
|
||||
addConditionalCloudHeaders();
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.send(200, MIME_JSON, buf);
|
||||
|
||||
for(uint8_t i = 0;i < numberOfPoints; i++) {
|
||||
if(prices[i] == PRICE_NO_VALUE) {
|
||||
snprintf_P(buf, BufferSize, PSTR("%snull"), i == 0 ? "" : ",");
|
||||
server.sendContent(buf);
|
||||
} else {
|
||||
snprintf_P(buf, BufferSize, PSTR("%s%.4f"), i == 0 ? "" : ",", prices[i]);
|
||||
server.sendContent(buf);
|
||||
}
|
||||
}
|
||||
server.sendContent_P(PSTR("]}"));
|
||||
}
|
||||
|
||||
void AmsWebServer::temperatureJson() {
|
||||
if(!checkSecurity(2))
|
||||
return;
|
||||
@@ -1050,8 +997,7 @@ void AmsWebServer::configurationJson() {
|
||||
price.enabled ? "true" : "false",
|
||||
price.entsoeToken,
|
||||
price.area,
|
||||
price.currency,
|
||||
price.resolutionInMinutes
|
||||
price.currency
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_DEBUG_JSON,
|
||||
@@ -1080,8 +1026,7 @@ void AmsWebServer::configurationJson() {
|
||||
gpioConfig->vccMultiplier / 1000.0,
|
||||
gpioConfig->vccResistorVcc,
|
||||
gpioConfig->vccResistorGnd,
|
||||
gpioConfig->vccBootLimit / 10.0,
|
||||
gpioConfig->powersaving
|
||||
gpioConfig->vccBootLimit / 10.0
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_UI_JSON,
|
||||
@@ -1344,7 +1289,6 @@ void AmsWebServer::handleSave() {
|
||||
|
||||
sys.userConfigured = success;
|
||||
sys.dataCollectionConsent = 0;
|
||||
sys.firmwareChannel = 0;
|
||||
config->setSystemConfig(sys);
|
||||
|
||||
performRestart = true;
|
||||
@@ -1389,29 +1333,27 @@ void AmsWebServer::handleSave() {
|
||||
memset(meterConfig.authenticationKey, 0, 16);
|
||||
}
|
||||
|
||||
meterConfig.wattageMultiplier = server.arg(F("mmw")).toDouble() * 1000.0;
|
||||
meterConfig.voltageMultiplier = server.arg(F("mmv")).toDouble() * 1000.0;
|
||||
meterConfig.amperageMultiplier = server.arg(F("mma")).toDouble() * 1000.0;
|
||||
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toDouble() * 1000.0;
|
||||
meterConfig.wattageMultiplier = server.arg(F("mmw")).toFloat() * 1000;
|
||||
meterConfig.voltageMultiplier = server.arg(F("mmv")).toFloat() * 1000;
|
||||
meterConfig.amperageMultiplier = server.arg(F("mma")).toFloat() * 1000;
|
||||
meterConfig.accumulatedMultiplier = server.arg(F("mmc")).toFloat() * 1000;
|
||||
config->setMeterConfig(meterConfig);
|
||||
}
|
||||
|
||||
if(server.hasArg(F("w")) && server.arg(F("w")) == F("true")) {
|
||||
long mode = server.arg(F("nc")).toInt();
|
||||
if(mode > 0) {
|
||||
if(mode > 0 && mode < 3) {
|
||||
NetworkConfig network;
|
||||
config->getNetworkConfig(network);
|
||||
network.mode = mode;
|
||||
if(mode < 3) {
|
||||
strcpy(network.ssid, server.arg(F("ws")).c_str());
|
||||
String psk = server.arg(F("wp"));
|
||||
if(!psk.equals("***")) {
|
||||
strcpy(network.psk, psk.c_str());
|
||||
}
|
||||
network.power = server.arg(F("ww")).toDouble() * 10.0;
|
||||
network.sleep = server.arg(F("wz")).toInt();
|
||||
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
|
||||
strcpy(network.ssid, server.arg(F("ws")).c_str());
|
||||
String psk = server.arg(F("wp"));
|
||||
if(!psk.equals("***")) {
|
||||
strcpy(network.psk, psk.c_str());
|
||||
}
|
||||
network.power = server.arg(F("ww")).toFloat() * 10;
|
||||
network.sleep = server.arg(F("wz")).toInt();
|
||||
network.use11b = server.hasArg(F("wb")) && server.arg(F("wb")) == F("true");
|
||||
|
||||
if(server.hasArg(F("nm"))) {
|
||||
if(server.arg(F("nm")) == "static") {
|
||||
@@ -1563,15 +1505,10 @@ void AmsWebServer::handleSave() {
|
||||
config->setGpioConfig(*gpioConfig);
|
||||
}
|
||||
|
||||
if(server.hasArg(F("ip"))) {
|
||||
gpioConfig->powersaving = server.hasArg(F("ip")) && !server.arg(F("ip")).isEmpty() ? server.arg(F("ip")).toInt() : 0;
|
||||
config->setGpioConfig(*gpioConfig);
|
||||
}
|
||||
|
||||
if(server.hasArg(F("iv")) && server.arg(F("iv")) == F("true")) {
|
||||
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toDouble() * 100.0 : 0;
|
||||
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toDouble() * 1000.0 : 1000;
|
||||
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toDouble() * 10.0 : 0;
|
||||
gpioConfig->vccOffset = server.hasArg(F("ivo")) && !server.arg(F("ivo")).isEmpty() ? server.arg(F("ivo")).toFloat() * 100 : 0;
|
||||
gpioConfig->vccMultiplier = server.hasArg(F("ivm")) && !server.arg(F("ivm")).isEmpty() ? server.arg(F("ivm")).toFloat() * 1000 : 1000;
|
||||
gpioConfig->vccBootLimit = server.hasArg(F("ivb")) && !server.arg(F("ivb")).isEmpty() ? server.arg(F("ivb")).toFloat() * 10 : 0;
|
||||
config->setGpioConfig(*gpioConfig);
|
||||
}
|
||||
|
||||
@@ -1636,7 +1573,6 @@ void AmsWebServer::handleSave() {
|
||||
strcpy(price.entsoeToken, server.arg(F("pt")).c_str());
|
||||
strcpy(price.area, priceRegion.c_str());
|
||||
strcpy(price.currency, server.arg(F("pc")).c_str());
|
||||
price.resolutionInMinutes = server.arg(F("pm")).toInt();
|
||||
config->setPriceServiceConfig(price);
|
||||
}
|
||||
|
||||
@@ -1686,7 +1622,7 @@ void AmsWebServer::handleSave() {
|
||||
snprintf_P(buf, BufferSize, PSTR("rd%d"), i);
|
||||
pc.direction = server.arg(buf).toInt();
|
||||
snprintf_P(buf, BufferSize, PSTR("rv%d"), i);
|
||||
pc.value = server.arg(buf).toDouble() * 10000.0;
|
||||
pc.value = server.arg(buf).toFloat() * 10000;
|
||||
snprintf_P(buf, BufferSize, PSTR("rn%d"), i);
|
||||
String name = server.arg(buf);
|
||||
strcpy(pc.name, name.c_str());
|
||||
@@ -1786,8 +1722,9 @@ void AmsWebServer::handleSave() {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Rebooting\n"));
|
||||
debugger->flush();
|
||||
rdc->cause = REBOOT_CAUSE_WEB_SAVE;
|
||||
delay(1000);
|
||||
rdc->cause = 2;
|
||||
performRestart = false;
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
@@ -1802,12 +1739,14 @@ void AmsWebServer::reboot() {
|
||||
delay(250);
|
||||
|
||||
#if defined(AMS_REMOTE_DEBUG)
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Rebooting\n"));
|
||||
if (debugger->isActive(RemoteDebug::INFO))
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Rebooting\n"));
|
||||
debugger->flush();
|
||||
rdc->cause = REBOOT_CAUSE_WEB_REBOOT;
|
||||
delay(1000);
|
||||
delay(1000); rdc->cause = 3;
|
||||
|
||||
rdc->cause = 3;
|
||||
performRestart = false;
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
@@ -2028,17 +1967,6 @@ void AmsWebServer::isAliveCheck() {
|
||||
server.send(200);
|
||||
}
|
||||
|
||||
void AmsWebServer::fwchannel() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
SystemConfig sys;
|
||||
config->getSystemConfig(sys);
|
||||
sys.firmwareChannel = server.arg(F("channel")).toInt();
|
||||
config->setSystemConfig(sys);
|
||||
server.send(200);
|
||||
}
|
||||
|
||||
void AmsWebServer::factoryResetPost() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
@@ -2067,8 +1995,8 @@ void AmsWebServer::factoryResetPost() {
|
||||
#endif
|
||||
debugger->printf_P(PSTR("Rebooting\n"));
|
||||
debugger->flush();
|
||||
rdc->cause = REBOOT_CAUSE_WEB_FACTORY_RESET;
|
||||
delay(1000);
|
||||
rdc->cause = 5;
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
@@ -2191,9 +2119,8 @@ void AmsWebServer::tariffJson() {
|
||||
String peaks;
|
||||
for(uint8_t x = 0;x < min((uint8_t) 5, eac->hours); x++) {
|
||||
EnergyAccountingPeak peak = ea->getPeak(x+1);
|
||||
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"h\":%d,\"v\":%.2f}"),
|
||||
int len = snprintf_P(buf, BufferSize, PSTR("{\"d\":%d,\"v\":%.2f}"),
|
||||
peak.day,
|
||||
peak.hour,
|
||||
peak.value / 100.0
|
||||
);
|
||||
buf[len] = '\0';
|
||||
@@ -2691,7 +2618,7 @@ void AmsWebServer::configFileDownload() {
|
||||
EnergyAccountingConfig eac;
|
||||
config->getEnergyAccountingConfig(eac);
|
||||
EnergyAccountingData ead = ea->getData();
|
||||
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %d %d %.2f %.2f %.2f"),
|
||||
server.sendContent(buf, snprintf_P(buf, BufferSize, PSTR("energyaccounting %d %d %.2f %.2f %.2f %.2f %.2f %.2f %d %.2f %d %.2f %d %.2f %d %.2f %d %.2f %.2f %.2f"),
|
||||
ead.version,
|
||||
ead.month,
|
||||
ea->getCostYesterday(),
|
||||
@@ -2701,19 +2628,14 @@ void AmsWebServer::configFileDownload() {
|
||||
ea->getIncomeThisMonth(),
|
||||
ea->getIncomeLastMonth(),
|
||||
ead.peaks[0].day,
|
||||
ead.peaks[0].hour,
|
||||
ead.peaks[0].value / 100.0,
|
||||
ead.peaks[1].day,
|
||||
ead.peaks[1].hour,
|
||||
ead.peaks[1].value / 100.0,
|
||||
ead.peaks[2].day,
|
||||
ead.peaks[2].hour,
|
||||
ead.peaks[2].value / 100.0,
|
||||
ead.peaks[3].day,
|
||||
ead.peaks[3].hour,
|
||||
ead.peaks[3].value / 100.0,
|
||||
ead.peaks[4].day,
|
||||
ead.peaks[4].hour,
|
||||
ead.peaks[4].value / 100.0,
|
||||
ea->getUseLastMonth(),
|
||||
ea->getProducedLastMonth()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
extra_configs = platformio-user.ini
|
||||
|
||||
[common]
|
||||
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsJsonGenerator, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
|
||||
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.2, OneWireNg@0.13.3, DallasTemperature@4.0.4, https://github.com/gskjold/RemoteDebug.git, PaulStoffregen/Time@1.6.1, JChristensen/Timezone@1.2.4, bblanchon/ArduinoJson@7.0.4, FirmwareVersion, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, AmsDecoder, PriceService, EnergyAccounting, AmsFirmwareUpdater, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, PassthroughMqttHandler, RealtimePlot, ConnectionHandler, MeterCommunicators
|
||||
lib_ignore = OneWire
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
@@ -19,7 +19,7 @@ build_flags =
|
||||
-fexceptions
|
||||
|
||||
[esp32]
|
||||
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, bblanchon/ArduinoJson@7.0.4, CloudConnector, ZmartCharge, SvelteUi
|
||||
lib_deps = WiFi, Ethernet, ESPmDNS, WiFiClientSecure, HTTPClient, FS, WebServer, ESP32 Async UDP, ESP32SSDP, mulmer89/ESPRandom@1.5.0, ${common.lib_deps}, CloudConnector, ZmartCharge, SvelteUi
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266@4.2.1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user