mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-03-11 13:05:31 +00:00
Compare commits
254 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b0ec39759 | ||
|
|
1f7d845a32 | ||
|
|
6de5b719f3 | ||
|
|
90859ee6b8 | ||
|
|
78dd856a97 | ||
|
|
9897ccc563 | ||
|
|
701e5904c5 | ||
|
|
54ea17d345 | ||
|
|
327fed6f08 | ||
|
|
8fe1a1edf2 | ||
|
|
6fd8dc0432 | ||
|
|
f586336ae3 | ||
|
|
1527ee7d45 | ||
|
|
c543b38829 | ||
|
|
6d12d7120c | ||
|
|
c1d4ba772e | ||
|
|
a03d4113e7 | ||
|
|
8751b6325d | ||
|
|
ff02dd43a9 | ||
|
|
b772fb6323 | ||
|
|
f5acfce578 | ||
|
|
4b902e2544 | ||
|
|
217247b28e | ||
|
|
4828f5a727 | ||
|
|
062a3634a9 | ||
|
|
376cd0cf90 | ||
|
|
2a20893a58 | ||
|
|
f3dba112de | ||
|
|
b037d6bb64 | ||
|
|
6f7eacddff | ||
|
|
4185411315 | ||
|
|
2a1b5a5f6d | ||
|
|
246a4f96fe | ||
|
|
c7d235b367 | ||
|
|
c5c8fbc2a0 | ||
|
|
8adf591c4e | ||
|
|
aa893f7ede | ||
|
|
f8b1725e94 | ||
|
|
ae7e3d11d5 | ||
|
|
73b20a0766 | ||
|
|
d8cf961258 | ||
|
|
bccd19812d | ||
|
|
6954ff5432 | ||
|
|
6200f31b83 | ||
|
|
5408b3c2a9 | ||
|
|
bd0b3ebb26 | ||
|
|
895a9bc6b1 | ||
|
|
ff935fc920 | ||
|
|
3833421e5f | ||
|
|
ebdc357a47 | ||
|
|
7da617e8c2 | ||
|
|
0b86761d2c | ||
|
|
d697f7e37f | ||
|
|
db859e3ff5 | ||
|
|
94d22957bd | ||
|
|
1ebaf443cc | ||
|
|
a336f711b0 | ||
|
|
e6d3b47d4f | ||
|
|
eb479f8216 | ||
|
|
38cba4e8da | ||
|
|
bc6d45ecf2 | ||
|
|
097131d7fb | ||
|
|
2107ca50e4 | ||
|
|
178f603937 | ||
|
|
21687368c6 | ||
|
|
6054e900e6 | ||
|
|
3da5275624 | ||
|
|
3fda2cfe7a | ||
|
|
2bb651f95c | ||
|
|
e2442a26ee | ||
|
|
cbd2ab4a7a | ||
|
|
76f8e2c343 | ||
|
|
ab101c8622 | ||
|
|
f425abb52d | ||
|
|
dc5bfff655 | ||
|
|
eb59245118 | ||
|
|
d18fd27a24 | ||
|
|
6f09f523e4 | ||
|
|
e78d59c31a | ||
|
|
e7f3217d7b | ||
|
|
ab534ce60a | ||
|
|
933246eae8 | ||
|
|
5c38d1cf3e | ||
|
|
580085f717 | ||
|
|
9831d4aa78 | ||
|
|
a3561d5c58 | ||
|
|
2a524cd0ac | ||
|
|
254e010594 | ||
|
|
24025d6785 | ||
|
|
4c92e592d6 | ||
|
|
f192ddae81 | ||
|
|
1cd2446365 | ||
|
|
6d26102b8e | ||
|
|
8e9da8f255 | ||
|
|
72bdb6e363 | ||
|
|
6df942f488 | ||
|
|
1e323ac3b9 | ||
|
|
05bdbaf1f5 | ||
|
|
6cca25788e | ||
|
|
e929f87ea9 | ||
|
|
21a4102553 | ||
|
|
768645fc0a | ||
|
|
c670549dea | ||
|
|
147b2ca33e | ||
|
|
c645b82ed3 | ||
|
|
26bb8a0fea | ||
|
|
507ed13770 | ||
|
|
a0f53a0c52 | ||
|
|
13bbc81b7f | ||
|
|
7412ba2697 | ||
|
|
dce7b7e64b | ||
|
|
7627d6c369 | ||
|
|
9fc9adea1c | ||
|
|
46df07fe40 | ||
|
|
0aeb5555e7 | ||
|
|
181fe3c909 | ||
|
|
867d6a4ef8 | ||
|
|
d52216a73e | ||
|
|
d70b41a454 | ||
|
|
332c366561 | ||
|
|
b572ad97f8 | ||
|
|
aa307f8690 | ||
|
|
19223312b5 | ||
|
|
74bc5aa7a0 | ||
|
|
e66e8a96ff | ||
|
|
ce95360a64 | ||
|
|
7802aeaab1 | ||
|
|
37ce3566bf | ||
|
|
606bac100a | ||
|
|
be569d1802 | ||
|
|
3e21105b2d | ||
|
|
9946827431 | ||
|
|
90d03ca77f | ||
|
|
7727b17122 | ||
|
|
fbc4c8c502 | ||
|
|
5d6a4ea0a4 | ||
|
|
91ce486bde | ||
|
|
523139749e | ||
|
|
5b9f3b7aed | ||
|
|
af76243761 | ||
|
|
605dc4901c | ||
|
|
54fb950513 | ||
|
|
ff84278edf | ||
|
|
f15cf5d75e | ||
|
|
33070af111 | ||
|
|
53573184f3 | ||
|
|
decc4788a7 | ||
|
|
f9597c786e | ||
|
|
af8f5a7c24 | ||
|
|
a830a52863 | ||
|
|
037bac24de | ||
|
|
837c3cf802 | ||
|
|
75956c087c | ||
|
|
88528b4099 | ||
|
|
6176d34e84 | ||
|
|
f2dda26bbb | ||
|
|
402ecf67d7 | ||
|
|
a83a6c1c53 | ||
|
|
5beb13894c | ||
|
|
c3fa618ab2 | ||
|
|
7713ae8566 | ||
|
|
7ae860ec72 | ||
|
|
376008a735 | ||
|
|
feed10184b | ||
|
|
59ca29f6a8 | ||
|
|
644a3fa40b | ||
|
|
a2c1c8fc61 | ||
|
|
b823f029ea | ||
|
|
fd907deec1 | ||
|
|
c1b56e25ad | ||
|
|
5491088cec | ||
|
|
04271accc1 | ||
|
|
a332e38b97 | ||
|
|
17bd85ebd0 | ||
|
|
4207216770 | ||
|
|
02491e074b | ||
|
|
e79a0585f0 | ||
|
|
859220d33f | ||
|
|
5d47105951 | ||
|
|
e71f937856 | ||
|
|
d730aac33b | ||
|
|
e121ec75d8 | ||
|
|
6479fd6a63 | ||
|
|
603f2925ce | ||
|
|
1284f3f848 | ||
|
|
620e355a27 | ||
|
|
47ddf57547 | ||
|
|
fcbfe4d96f | ||
|
|
d789d6ff3b | ||
|
|
b4f18de030 | ||
|
|
00d5d215cd | ||
|
|
8de5a58a6b | ||
|
|
012794e682 | ||
|
|
38eb2d8c19 | ||
|
|
d95137adbc | ||
|
|
cde3f80fca | ||
|
|
cdc012743f | ||
|
|
23c90315a5 | ||
|
|
8d938f111c | ||
|
|
cff6c02d57 | ||
|
|
0c92f7401c | ||
|
|
195a0d4e77 | ||
|
|
43f50e0e0a | ||
|
|
f2f20afd9c | ||
|
|
f22cfbb223 | ||
|
|
803c5116bd | ||
|
|
a542fbc931 | ||
|
|
85a70016fa | ||
|
|
778daf8645 | ||
|
|
efa99f970c | ||
|
|
953f2d4110 | ||
|
|
c3c0ca0a1b | ||
|
|
dc83853d2e | ||
|
|
f5123e9aa1 | ||
|
|
2da69dd451 | ||
|
|
2858123c1b | ||
|
|
9d4488f8df | ||
|
|
f78c8e3582 | ||
|
|
431d6714b5 | ||
|
|
550a3c1a0b | ||
|
|
b6f5e72638 | ||
|
|
958ff37d7d | ||
|
|
1ea9da22c7 | ||
|
|
0c93c52e3d | ||
|
|
41784511e9 | ||
|
|
a78fdc0b59 | ||
|
|
cc032fdf29 | ||
|
|
f696e0b59b | ||
|
|
f67f12a188 | ||
|
|
719ed56e21 | ||
|
|
cc72d0e0b3 | ||
|
|
ab175ec9ec | ||
|
|
398407350c | ||
|
|
5e33a15e85 | ||
|
|
7f51534e91 | ||
|
|
0f5af6b274 | ||
|
|
7886ce668e | ||
|
|
0f4848c872 | ||
|
|
6911d203ca | ||
|
|
e96b5bbf1b | ||
|
|
8457db50f6 | ||
|
|
279c5f8321 | ||
|
|
0de1d0bb6f | ||
|
|
bf13965a9e | ||
|
|
d9b45d037b | ||
|
|
8c75a9530e | ||
|
|
73d00f786a | ||
|
|
d747c84a14 | ||
|
|
26634f96b0 | ||
|
|
97669cf4ad | ||
|
|
e12980db11 | ||
|
|
732639814b | ||
|
|
c07c2e66d6 | ||
|
|
9b70802450 |
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Hardware information:**
|
||||
- Meter: [e.g. Aidon]
|
||||
- AMS reader: [e.g. Pow-U, ESP32 etc]
|
||||
- M-bus adapter (if applicable):
|
||||
|
||||
**Relevant firmware information:**
|
||||
- Version: [e.g. 1.5.0]
|
||||
- MQTT: [yes/no]
|
||||
- HAN GPIO: [e.g. GPIO5]
|
||||
- HAN baud and parity: [e.g. 2400 8E1]
|
||||
- Temperature sensors [e.g. 3xDS18B20]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Meter configuration
|
||||
url: https://github.com/gskjold/AmsToMqttBridge/wiki/Known-hardware-configurations
|
||||
about: Please check your meter configuration here first.
|
||||
- name: Frequently asked questions
|
||||
url: https://github.com/gskjold/AmsToMqttBridge/wiki/FAQ
|
||||
about: Please check frequently asked questions first.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
22
.github/ISSUE_TEMPLATE/support.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/support.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Support
|
||||
about: Request support
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe your problem**
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
**Hardware information:**
|
||||
- Meter: [e.g. Aidon]
|
||||
- AMS reader: [e.g. Pow-U, ESP32 etc]
|
||||
- M-bus adapter (if applicable):
|
||||
|
||||
**Relevant firmware information:**
|
||||
- Version: [e.g. 1.5.0]
|
||||
- MQTT: [yes/no]
|
||||
- HAN GPIO: [e.g. GPIO5]
|
||||
- Temperature sensors [e.g. 3xDS18B20]
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
- web/**
|
||||
- platformio.ini
|
||||
branches:
|
||||
- master
|
||||
- '*'
|
||||
tags:
|
||||
- '*'
|
||||
- '!v*.*.*'
|
||||
@@ -39,9 +39,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
pip install -U platformio css_html_js_minify
|
||||
- name: Configure build targets
|
||||
run: echo "[platformio]\ndefault_envs = hw1esp12e, esp12e, esp32" > platformio-user.ini
|
||||
run: echo "[platformio]\ndefault_envs = esp8266, esp32" > platformio-user.ini
|
||||
- name: PlatformIO lib install
|
||||
run: pio lib install
|
||||
- name: PlatformIO run
|
||||
|
||||
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@@ -14,12 +14,15 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from repo
|
||||
uses: actions/checkout@v1
|
||||
- name: Get release version from tag
|
||||
- name: Get release version for filenames
|
||||
id: release_tag
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:11})
|
||||
run: echo ::set-env name=GITHUB_TAG::$(echo ${GITHUB_REF##*/})
|
||||
- name: Get release version for code
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo "GITHUB_TAG=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV
|
||||
- name: Cache Python dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -37,7 +40,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U platformio
|
||||
pip install -U platformio css_html_js_minify
|
||||
- name: PlatformIO lib install
|
||||
run: pio lib install
|
||||
- name: PlatformIO run
|
||||
@@ -52,32 +55,14 @@ jobs:
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload hw1esp12e binary to release
|
||||
- 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/hw1esp12e/firmware.bin
|
||||
asset_name: ams2mqtt-hw1esp12e-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload esp12e 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/esp12e/firmware.bin
|
||||
asset_name: ams2mqtt-esp12e-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload d1mini 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/d1mini/firmware.bin
|
||||
asset_name: ams2mqtt-d1mini-${{ steps.release_tag.outputs.tag }}.bin
|
||||
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 esp32 binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -88,21 +73,12 @@ jobs:
|
||||
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 lolind32 binary to release
|
||||
- name: Upload esp32 partitions 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/lolind32/firmware.bin
|
||||
asset_name: ams2mqtt-lolind32-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
- name: Upload featheresp32 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/featheresp32/firmware.bin
|
||||
asset_name: ams2mqtt-featheresp32-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_path: .pio/build/esp32/partitions.bin
|
||||
asset_name: ams2mqtt-esp32-partitions-${{ steps.release_tag.outputs.tag }}.bin
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -10,3 +10,7 @@ platformio-user.ini
|
||||
/src/version.h
|
||||
/src/web/root
|
||||
/src/AmsToMqttBridge.ino.cpp
|
||||
/test
|
||||
/web/test.html
|
||||
/sdkconfig
|
||||
/.tmp
|
||||
|
||||
34
README.md
34
README.md
@@ -1,34 +1,12 @@
|
||||
# AMS <-> MQTT Bridge
|
||||
Orignally designed and coded by [@roarfred](https://github.com/roarfred), see the original repo at [roarfred/AmsToMqttBridge](https://github.com/roarfred/AmsToMqttBridge)
|
||||
# AMS MQTT Bridge
|
||||
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.
|
||||
|
||||
This repository contains the code and schematics necessary to build a device to receive and convert data from AMS electrical meters installed in Norway. The code can be used on both ESP8266 and ESP32, both as custom build devices or built from readily available development modules. It reads data from the HAN port of the meter and sends this to a configured MQTT bus.
|
||||
Later development have added Energy usage graph for both day and month, as well as future energy price (Prices only available for ESP32). The code can run on any ESP8266 or ESP32 hardware which you can read more about in the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki). If you don't have the knowledge to set up a ESP device yourself, have a look at the shop at [amsleser.no](https://amsleser.no/).
|
||||
|
||||
There is also a web interface available on runtime, showing meter data in real time.
|
||||
|
||||
<img src="webui.jpg" width="480">
|
||||
|
||||
## Hardware options
|
||||
Look in [hardware section](/hardware) for more details about supported hardware
|
||||
|
||||
## Release binaries
|
||||
|
||||
In the [Release section](https://github.com/gskjold/AmsToMqttBridge/releases) of this repository, you will find precompiled binaries for some common boards.
|
||||
|
||||
- _hw1esp12e_ :: First version custom hardware with ESP 12E of 12F chip
|
||||
- _esp12e_ :: General ESP8266 board with 12E or 12F chip
|
||||
- _d1mini_ :: Wemos D1 mini
|
||||
- _esp32_ :: General ESP32 board
|
||||
- _lolind32_ :: Wemos D32
|
||||
- _featheresp32_ :: Adafruit ESP32 feather
|
||||
|
||||
### Flashing binaries with [esptool.py](https://github.com/espressif/esptool)
|
||||
|
||||
Linux:
|
||||
```esptool.py --port /dev/ttyUSB0 write_flash 0x0 binary-file.bin```
|
||||
|
||||
Windows:
|
||||
```esptool.py --port COM1 write_flash 0x0 binary-file.bin```
|
||||
<img src="webui.png">
|
||||
|
||||
Go to the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/gskjold/AmsToMqttBridge/releases).
|
||||
|
||||
## Building this project with PlatformIO
|
||||
To build this project, you need [PlatformIO](https://platformio.org/) installed.
|
||||
@@ -39,4 +17,4 @@ It is recommended to use Visual Studio Code with the PlatformIO plugin for devel
|
||||
|
||||
[PlatformIO vscode plugin](https://platformio.org/install/ide?install=vscode)
|
||||
|
||||
Copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile. If you are using the original board design by [@roarfred](https://github.com/roarfred) use build flag -D HW_ROARFRED=1
|
||||
For development purposes, copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile.
|
||||
|
||||
BIN
doc/Blue_Book_Edition_13-Excerpt.pdf
Normal file
BIN
doc/Blue_Book_Edition_13-Excerpt.pdf
Normal file
Binary file not shown.
Binary file not shown.
32
doc/Denmark/Kamstrup_encrypted_OBIS.txt
Normal file
32
doc/Denmark/Kamstrup_encrypted_OBIS.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
1.1.1.8.0.255 - Active+ Energy
|
||||
1.1.2.8.0.255 - Active- Energy
|
||||
1.1.3.8.0.255 - Reactive+ Energy
|
||||
1.1.4.8.0.255 - Reactive- Energy
|
||||
1.1.0.0.1.255 - Electricity ID?
|
||||
1.1.1.7.0.255 - Active+ Instantaneous value
|
||||
1.1.2.7.0.255 - Active- Instantaneous value
|
||||
1.1.3.7.0.255 - Reactive+ Instantaneous value
|
||||
1.1.4.7.0.255 - Reactive- Instantaneous value
|
||||
0.1.1.0.0.255 - Current date/time
|
||||
1.1.32.7.0.255 - L1 Voltage Instantaneous value
|
||||
1.1.52.7.0.255 - L2 Voltage Instantaneous value
|
||||
1.1.72.7.0.255 - L3 Voltage Instantaneous value
|
||||
1.1.31.7.0.255 - L1 Current Instantaneous value
|
||||
1.1.51.7.0.255 - L2 Current Instantaneous value
|
||||
1.1.71.7.0.255 - L3 Current Instantaneous value
|
||||
1.1.21.7.0.255 - L1 Active+ Instantaneous value
|
||||
1.1.41.7.0.255 - L2 Active+ Instantaneous value
|
||||
1.1.61.7.0.255 - L3 Active+ Instantaneous value
|
||||
1.1.33.7.0.255 - L1 (cos.phi) (PF) Instantaneous value
|
||||
1.1.53.7.0.255 - L2 (cos.phi) (PF) Instantaneous value
|
||||
1.1.73.7.0.255 - L3 (cos.phi) (PF) Instantaneous value
|
||||
1.1.13.7.0.255 - Avegage (cos.phi) (PF) Inst. value
|
||||
1.1.22.7.0.255 - L1 Active- Instantaneous value
|
||||
1.1.42.7.0.255 - L2 Active- Instantaneous value
|
||||
1.1.62.7.0.255 - L3 Active- Instantaneous value
|
||||
1.1.22.8.0.255 - L1 Active- Energy
|
||||
1.1.42.8.0.255 - L2 Active- Energy
|
||||
1.1.62.8.0.255 - L3 Active- Energy
|
||||
1.1.21.8.0.255 - L1 Active+ Energy
|
||||
1.1.41.8.0.255 - L2 Active+ Energy
|
||||
1.1.61.8.0.255 - L3 Active+ Energy
|
||||
112
doc/Denmark/Kamstrup_encrypted_data.xml
Normal file
112
doc/Denmark/Kamstrup_encrypted_data.xml
Normal file
@@ -0,0 +1,112 @@
|
||||
<GatewayRequest>
|
||||
<NetworkId Value="231" />
|
||||
<PhysicalDeviceAddress Value="" />
|
||||
<DataNotification>
|
||||
<LongInvokeIdAndPriority Value="40000000" />
|
||||
<DateTime Value="" />
|
||||
<NotificationBody>
|
||||
<DataValue>
|
||||
<Structure Qty="41" >
|
||||
<String Value="Kamstrup_V0001" />
|
||||
<!--1.1.1.8.0.255-->
|
||||
<OctetString Value="0101010800FF" />
|
||||
<UInt32 Value="001194CA" />
|
||||
<!--1.1.2.8.0.255-->
|
||||
<OctetString Value="0101020800FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.3.8.0.255-->
|
||||
<OctetString Value="0101030800FF" />
|
||||
<UInt32 Value="0000127E" />
|
||||
<!--1.1.4.8.0.255-->
|
||||
<OctetString Value="0101040800FF" />
|
||||
<UInt32 Value="0009550E" />
|
||||
<!--1.1.0.0.1.255-->
|
||||
<OctetString Value="0101000001FF" />
|
||||
<UInt32 Value="0144ADE1" />
|
||||
<!--1.1.1.7.0.255-->
|
||||
<OctetString Value="0101010700FF" />
|
||||
<UInt32 Value="00000531" />
|
||||
<!--1.1.2.7.0.255-->
|
||||
<OctetString Value="0101020700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.3.7.0.255-->
|
||||
<OctetString Value="0101030700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.4.7.0.255-->
|
||||
<OctetString Value="0101040700FF" />
|
||||
<UInt32 Value="00000054" />
|
||||
<!--0.1.1.0.0.255-->
|
||||
<OctetString Value="0001010000FF" />
|
||||
<!--2020-05-12 10:24:50-->
|
||||
<OctetString Value="07E4050C020A1832FF800080" />
|
||||
<!--1.1.32.7.0.255-->
|
||||
<OctetString Value="0101200700FF" />
|
||||
<UInt16 Value="00E4" />
|
||||
<!--1.1.52.7.0.255-->
|
||||
<OctetString Value="0101340700FF" />
|
||||
<UInt16 Value="00E5" />
|
||||
<!--1.1.72.7.0.255-->
|
||||
<OctetString Value="0101480700FF" />
|
||||
<UInt16 Value="00E3" />
|
||||
<!--1.1.31.7.0.255-->
|
||||
<OctetString Value="01011F0700FF" />
|
||||
<UInt32 Value="0000004B" />
|
||||
<!--1.1.51.7.0.255-->
|
||||
<OctetString Value="0101330700FF" />
|
||||
<UInt32 Value="00000070" />
|
||||
<!--1.1.71.7.0.255-->
|
||||
<OctetString Value="0101470700FF" />
|
||||
<UInt32 Value="000001E4" />
|
||||
<!--1.1.21.7.0.255-->
|
||||
<OctetString Value="0101150700FF" />
|
||||
<UInt32 Value="00000070" />
|
||||
<!--1.1.41.7.0.255-->
|
||||
<OctetString Value="0101290700FF" />
|
||||
<UInt32 Value="000000B5" />
|
||||
<!--1.1.61.7.0.255-->
|
||||
<OctetString Value="01013D0700FF" />
|
||||
<UInt32 Value="0000040C" />
|
||||
<!--1.1.33.7.0.255-->
|
||||
<OctetString Value="0101210700FF" />
|
||||
<UInt16 Value="004D" />
|
||||
<!--1.1.53.7.0.255-->
|
||||
<OctetString Value="0101350700FF" />
|
||||
<UInt16 Value="004E" />
|
||||
<!--1.1.73.7.0.255-->
|
||||
<OctetString Value="0101490700FF" />
|
||||
<UInt16 Value="0062" />
|
||||
<!--1.1.13.7.0.255-->
|
||||
<OctetString Value="01010D0700FF" />
|
||||
<UInt16 Value="0063" />
|
||||
<!--1.1.22.7.0.255-->
|
||||
<OctetString Value="0101160700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.42.7.0.255-->
|
||||
<OctetString Value="01012A0700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.62.7.0.255-->
|
||||
<OctetString Value="01013E0700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.22.8.0.255-->
|
||||
<OctetString Value="0101160800FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.42.8.0.255-->
|
||||
<OctetString Value="01012A0800FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.62.8.0.255-->
|
||||
<OctetString Value="01013E0800FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<!--1.1.21.8.0.255-->
|
||||
<OctetString Value="0101150800FF" />
|
||||
<UInt32 Value="000A8F97" />
|
||||
<!--1.1.41.8.0.255-->
|
||||
<OctetString Value="0101290800FF" />
|
||||
<UInt32 Value="0004C152" />
|
||||
<!--1.1.61.8.0.255-->
|
||||
<OctetString Value="01013D0800FF" />
|
||||
<UInt32 Value="000243DF" />
|
||||
</Structure>
|
||||
</DataValue>
|
||||
</NotificationBody>
|
||||
</DataNotification>
|
||||
</GatewayRequest>
|
||||
BIN
doc/Encrypted payload.png
Normal file
BIN
doc/Encrypted payload.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
doc/Green_Book_Edition_9-Excerpt.pdf
Normal file
BIN
doc/Green_Book_Edition_9-Excerpt.pdf
Normal file
Binary file not shown.
BIN
doc/Netherlands/Slimme_meter_15_a727fce1f1.pdf
Normal file
BIN
doc/Netherlands/Slimme_meter_15_a727fce1f1.pdf
Normal file
Binary file not shown.
18
doc/Norway/Aidon_OBIS.txt
Normal file
18
doc/Norway/Aidon_OBIS.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
1.1.0.2.129.255 - List version identifier
|
||||
0.0.96.1.0.255 - Meter ID
|
||||
0.0.96.1.7.255 - Meter Model
|
||||
1.0.1.7.0.255 - Active+ Instantaneous value
|
||||
1.0.2.7.0.255 - Active- Instantaneous value
|
||||
1.0.3.7.0.255 - Reactive+ Instantaneous value
|
||||
1.0.4.7.0.255 - Reactive- Instantaneous value
|
||||
1.0.31.7.0.255 - L1 Current Instantaneous value
|
||||
1.0.51.7.0.255 - L2 Current Instantaneous value
|
||||
1.0.71.7.0.255 - L3 Current Instantaneous value
|
||||
1.0.32.7.0.255 - L1 Voltage Instantaneous value
|
||||
1.0.52.7.0.255 - L2 Voltage Instantaneous value
|
||||
1.0.72.7.0.255 - L3 Voltage Instantaneous value
|
||||
0.0.1.0.0.255 - Current date/time
|
||||
1.0.1.8.0.255 - Active+ Energy
|
||||
1.0.2.8.0.255 - Active- Energy
|
||||
1.0.3.8.0.255 - Reactive+ Energy
|
||||
1.0.4.8.0.255 - Reactive- Energy
|
||||
119
doc/Norway/Aidon_data.xml
Normal file
119
doc/Norway/Aidon_data.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<GatewayRequest>
|
||||
<NetworkId Value="231" />
|
||||
<PhysicalDeviceAddress Value="" />
|
||||
<DataNotification>
|
||||
<LongInvokeIdAndPriority Value="40000000" />
|
||||
<DateTime Value="" />
|
||||
<NotificationBody>
|
||||
<DataValue>
|
||||
<Array Qty="0D" >
|
||||
<Structure Qty="02" >
|
||||
<!--1.1.0.2.129.255-->
|
||||
<OctetString Value="0101000281FF" />
|
||||
<String Value="AIDON_V0001" />
|
||||
</Structure>
|
||||
<Structure Qty="02" >
|
||||
<!--0.0.96.1.0.255-->
|
||||
<OctetString Value="0000600100FF" />
|
||||
<String Value="0000000000000000" />
|
||||
</Structure>
|
||||
<Structure Qty="02" >
|
||||
<!--0.0.96.1.7.255-->
|
||||
<OctetString Value="0000600107FF" />
|
||||
<String Value="6534" />
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.1.7.0.255-->
|
||||
<OctetString Value="0100010700FF" />
|
||||
<UInt32 Value="00000339" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="00" />
|
||||
<Enum Value="1B" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.2.7.0.255-->
|
||||
<OctetString Value="0100020700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="00" />
|
||||
<Enum Value="1B" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.3.7.0.255-->
|
||||
<OctetString Value="0100030700FF" />
|
||||
<UInt32 Value="00000000" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="00" />
|
||||
<Enum Value="1D" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.4.7.0.255-->
|
||||
<OctetString Value="0100040700FF" />
|
||||
<UInt32 Value="00000251" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="00" />
|
||||
<Enum Value="1D" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.31.7.0.255-->
|
||||
<OctetString Value="01001F0700FF" />
|
||||
<Int16 Value="0012" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="21" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.51.7.0.255-->
|
||||
<OctetString Value="0100330700FF" />
|
||||
<Int16 Value="0003" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="21" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.71.7.0.255-->
|
||||
<OctetString Value="0100470700FF" />
|
||||
<Int16 Value="0016" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="21" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.32.7.0.255-->
|
||||
<OctetString Value="0100200700FF" />
|
||||
<UInt16 Value="08FE" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="23" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.52.7.0.255-->
|
||||
<OctetString Value="0100340700FF" />
|
||||
<UInt16 Value="08F8" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="23" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
<Structure Qty="03" >
|
||||
<!--1.0.72.7.0.255-->
|
||||
<OctetString Value="0100480700FF" />
|
||||
<UInt16 Value="08F7" />
|
||||
<Structure Qty="02" >
|
||||
<Int8 Value="FF" />
|
||||
<Enum Value="23" />
|
||||
</Structure>
|
||||
</Structure>
|
||||
</Array>
|
||||
</DataValue>
|
||||
</NotificationBody>
|
||||
</DataNotification>
|
||||
</GatewayRequest>
|
||||
BIN
doc/Norway/Kaifa.png
Normal file
BIN
doc/Norway/Kaifa.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
BIN
doc/Sweden/Aidon-RJ12.pdf
Normal file
BIN
doc/Sweden/Aidon-RJ12.pdf
Normal file
Binary file not shown.
30
frames/Aidon-Sweden.raw
Normal file
30
frames/Aidon-Sweden.raw
Normal file
@@ -0,0 +1,30 @@
|
||||
7E A2 43 41 08 83 13 85 EB E6 E7 00 0F 40 00 00 00 00
|
||||
01 1B
|
||||
02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 0C 0A 05 10 39 00 FF 80 00 FF
|
||||
02 03 09 06 01 00 01 07 00 FF 06 00 00 07 E5 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 04 07 00 FF 06 00 00 02 48 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 1F 07 00 FF 10 00 09 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 33 07 00 FF 10 00 25 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 47 07 00 FF 10 00 2E 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 20 07 00 FF 12 08 E3 02 02 0F FF 16 23
|
||||
02 03 09 06 01 00 34 07 00 FF 12 08 D8 02 02 0F FF 16 23
|
||||
02 03 09 06 01 00 48 07 00 FF 12 08 DF 02 02 0F FF 16 23
|
||||
02 03 09 06 01 00 15 07 00 FF 06 00 00 00 D5 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 16 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 17 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 18 07 00 FF 06 00 00 00 36 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 29 07 00 FF 06 00 00 03 0C 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 2A 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 2B 07 00 FF 06 00 00 01 21 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 2C 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 3D 07 00 FF 06 00 00 03 F9 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 3E 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 3F 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 40 07 00 FF 06 00 00 00 E9 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 01 08 00 FF 06 03 C2 5A 64 02 02 0F 00 16 1E
|
||||
02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 00 16 1E
|
||||
02 03 09 06 01 00 03 08 00 FF 06 00 04 5D 06 02 02 0F 00 16 20
|
||||
02 03 09 06 01 00 04 08 00 FF 06 00 B4 9D 89 02 02 0F 00 16 20
|
||||
1C 90 7E
|
||||
33
frames/Aidon-TN-3p.raw
Normal file
33
frames/Aidon-TN-3p.raw
Normal file
@@ -0,0 +1,33 @@
|
||||
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
|
||||
7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 64 02 02 0F 00 16 1B E1
|
||||
7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 6C 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 09 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 41 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 13 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F2 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 D1 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 E8 02 02 0F FF 16 23 8B
|
||||
7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 03 9A 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 0E 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 10 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F4 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 CD 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 DC 02 02 0F FF 16 23 02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 03 18 03 08 00 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06 00 47 F0 34 02 02 0F 01 16 1E 02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02 03 09 06 01 00 03 08 00 FF 06 00 00 21 9E 02 02 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00 08 E0 21 02 02 0F 01 16 20 57
|
||||
|
||||
7E A1 8A 41 08 83 13 EB FD E6 E7 00
|
||||
0F
|
||||
40 00 00 00 00
|
||||
01 12
|
||||
02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31
|
||||
02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37
|
||||
02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34
|
||||
02 03 09 06 01 00 01 07 00 FF 06 00 00 09 6D 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
|
||||
02 03 09 06 01 00 04 07 00 FF 06 00 00 02 5B 02 02 0F 00 16 1D
|
||||
|
||||
Object with three values Value Object with two values
|
||||
| Obis code | | Scaling
|
||||
| | | | | Unit
|
||||
02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 33 07 00 FF 10 00 03 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 47 07 00 FF 10 00 5A 02 02 0F FF 16 21
|
||||
02 03 09 06 01 00 20 07 00 FF 12 09 04 02 02 0F FF 16 23
|
||||
02 03 09 06 01 00 34 07 00 FF 12 09 02 02 02 0F FF 16 23
|
||||
02 03 09 06 01 00 48 07 00 FF 12 08 EC 02 02 0F FF 16 23
|
||||
02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 0A 1F 00 14 00 00 FF 00 00 00
|
||||
02 03 09 06 01 00 01 08 00 FF 06 00 56 9F 52 02 02 0F 01 16 1E
|
||||
02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E
|
||||
02 03 09 06 01 00 03 08 00 FF 06 00 00 22 D0 02 02 0F 01 16 20
|
||||
02 03 09 06 01 00 04 08 00 FF 06 00 0A F5 EC 02 02 0F 01 16 20
|
||||
51 D7
|
||||
7E
|
||||
34
frames/Kaifa-TN-3p.raw
Normal file
34
frames/Kaifa-TN-3p.raw
Normal file
@@ -0,0 +1,34 @@
|
||||
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
|
||||
7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1A 3A FF 80 00 00
|
||||
02 01 // Frame type and size
|
||||
06 00 00 0B F3 // Active power
|
||||
5B 05 7E // CRC and end tag
|
||||
|
||||
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
|
||||
7E A0 78 01 02 01 10 C4 98 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1B 00 FF 80 00 00
|
||||
02 0D // Frame type and size
|
||||
09 07 4B 46 4D 5F 30 30 31 // List version
|
||||
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33 // Meter ID
|
||||
09 07 4D 41 33 30 34 48 34 // Meter type
|
||||
06 00 00 0C 21 // Active import
|
||||
06 00 00 00 00 // Active export
|
||||
06 00 00 00 00 // Reactive import
|
||||
06 00 00 01 9F // Reactive export
|
||||
06 00 00 0B F3 // I1
|
||||
06 00 00 05 0B // I2
|
||||
06 00 00 25 11 // I3
|
||||
06 00 00 09 44 // U1
|
||||
06 00 00 09 49 // U2
|
||||
06 00 00 09 39 // U3
|
||||
C9 95 7E // CRC and end tag
|
||||
|
||||
|
||||
7E A0 9A 01 02 01 10 AA A5 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00
|
||||
02 12 09 07 4B 46 4D 5F 30 30 31
|
||||
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33
|
||||
09 07 4D 41 33 30 34 48 34
|
||||
06 00 00 09 99
|
||||
06 00 00 00 00 06 00 00 00 00 06 00 00 01 67 06 00 00 03 BF 06 00 00 05 05
|
||||
06 00 00 24 34 06 00 00 09 45 06 00 00 09 4F 06 00 00 09 3B
|
||||
09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00 06 01 34 3B 5D 06 00 00 00 00 06 00 00 09 36 06 00 3C 7A 98 DA 15 7E
|
||||
7E A0 79 01 02 01 10 80 93 E6 E7 00 0F 40 00 00 00 09 0C 07 E1 09 0E 04 15 1F 14 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30 36 33 31 34 30 31 37 35 33 39 38 35 09 08 4D 41 33 30 34 48 33 45 06 00 00 04 0C 06 00 00 00 00 06 00 00 00 00 06 00 00 00 4E 06 00 00 07 C1 06 00 00 0C 9E 06 00 00 0D 7E 06 00 00 09 5F 06 00 00 00 00 06 00 00 09 66 87 96 7E
|
||||
51
frames/Kamstup-Encrypted.raw
Normal file
51
frames/Kamstup-Encrypted.raw
Normal file
@@ -0,0 +1,51 @@
|
||||
# After decode:
|
||||
7E
|
||||
A1 E9 // Frame type and size
|
||||
41 03 13 C6 37 E6 E7 00
|
||||
DB // Encrypted
|
||||
08 4B 41 4D 45 01 AC 4D 6E // System title
|
||||
82 // Prefix for 2-byte length
|
||||
01 D0 // Length 464
|
||||
30 // Security tag 0011 0000, 0=Compression off, 0=Unicast, 1=Encryption, 0=Authentication, 0000= Security Suite ID
|
||||
00 00 A3 2F // Frame counter
|
||||
|
||||
// Decrypted frame below
|
||||
0F 00 00 00 00
|
||||
0C 07 E4 05 0C 02 0A 19 00 FF 80 00 80 // Package timestamp
|
||||
|
||||
02 41
|
||||
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31 - List ID
|
||||
09 06 01 01 01 08 00 FF 06 00 11 94 CA - Active+ Energy
|
||||
09 06 01 01 02 08 00 FF 06 00 00 00 00 - Active- Energy
|
||||
09 06 01 01 03 08 00 FF 06 00 00 12 7E - Reactive+ Energy
|
||||
09 06 01 01 04 08 00 FF 06 00 09 55 0E - Reactive- Energy
|
||||
09 06 01 01 00 00 01 FF 06 01 44 AD E1 - Electricity ID?
|
||||
09 06 01 01 01 07 00 FF 06 00 00 05 CC - Active+ Instantaneous value
|
||||
09 06 01 01 02 07 00 FF 06 00 00 00 00 - Active- Instantaneous value
|
||||
09 06 01 01 03 07 00 FF 06 00 00 00 00 - Reactive+ Instantaneous value
|
||||
09 06 01 01 04 07 00 FF 06 00 00 00 17 - Reactive- Instantaneous value
|
||||
09 06 00 01 01 00 00 FF 09 0C 07 E4 05 0C 02 0A 19 00 FF 80 00 80 - Current date/time
|
||||
09 06 01 01 20 07 00 FF 12 00 E5 - L1 Voltage Instantaneous value
|
||||
09 06 01 01 34 07 00 FF 12 00 E5 - L2 Voltage Instantaneous value
|
||||
09 06 01 01 48 07 00 FF 12 00 E3 - L3 Voltage Instantaneous value
|
||||
09 06 01 01 1F 07 00 FF 06 00 00 00 4B - L1 Current Instantaneous value
|
||||
09 06 01 01 33 07 00 FF 06 00 00 00 AA - L2 Current Instantaneous value
|
||||
09 06 01 01 47 07 00 FF 06 00 00 01 E4 - L3 Current Instantaneous value
|
||||
09 06 01 01 15 07 00 FF 06 00 00 00 71 - L1 Active+ Instantaneous value
|
||||
09 06 01 01 29 07 00 FF 06 00 00 01 54 - L2 Active+ Instantaneous value
|
||||
09 06 01 01 3D 07 00 FF 06 00 00 04 07 - L3 Active+ Instantaneous value
|
||||
09 06 01 01 21 07 00 FF 12 00 4D - L1 (cos.phi) (PF) Instantaneous value
|
||||
09 06 01 01 35 07 00 FF 12 00 5F - L2 (cos.phi) (PF) Instantaneous value
|
||||
09 06 01 01 49 07 00 FF 12 00 62 - L3 (cos.phi) (PF) Instantaneous value
|
||||
09 06 01 01 0D 07 00 FF 12 00 63 - Avegage (cos.phi) (PF) Inst. value
|
||||
09 06 01 01 16 07 00 FF 06 00 00 00 00 - L1 Active- Instantaneous value
|
||||
09 06 01 01 2A 07 00 FF 06 00 00 00 00 - L2 Active- Instantaneous value
|
||||
09 06 01 01 3E 07 00 FF 06 00 00 00 00 - L3 Active- Instantaneous value
|
||||
09 06 01 01 16 08 00 FF 06 00 00 00 00 - L1 Active- Energy
|
||||
09 06 01 01 2A 08 00 FF 06 00 00 00 00 - L2 Active- Energy
|
||||
09 06 01 01 3E 08 00 FF 06 00 00 00 00 - L3 Active- Energy
|
||||
09 06 01 01 15 08 00 FF 06 00 0A 8F 97 - L1 Active+ Energy
|
||||
09 06 01 01 29 08 00 FF 06 00 04 C1 53 - L2 Active+ Energy
|
||||
09 06 01 01 3D 08 00 FF 06 00 02 43 E0 - L3 Active+ Energy
|
||||
|
||||
5B C3 CD 5E 79 18 18 DA 9F 97 85 FF 5A 84 7E
|
||||
18
frames/Kamstup-TN-3p.raw
Normal file
18
frames/Kamstup-TN-3p.raw
Normal file
@@ -0,0 +1,18 @@
|
||||
7E A0 E2 2B 21 13 23 9A E6 E7 00 0F 00 00 00 00
|
||||
0C 07 E5 0B 11 03 0B 32 00 FF 80 00 00
|
||||
|
||||
02 19
|
||||
0A 0E 4B 61 6D 73 74 72 75 70 5F 56 30 30 30 31 - List ID
|
||||
09 06 01 01 00 00 05 FF 0A 10 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX - Meter ID
|
||||
09 06 01 01 60 01 01 FF 0A 12 36 38 34 31 31 33 31 42 4E 32 34 33 31 30 31 30 34 30 - Meter model
|
||||
09 06 01 01 01 07 00 FF 06 00 00 05 E6 - Active+
|
||||
09 06 01 01 02 07 00 FF 06 00 00 00 00 - Active-
|
||||
09 06 01 01 03 07 00 FF 06 00 00 00 00 - Reactive+
|
||||
09 06 01 01 04 07 00 FF 06 00 00 01 92 - Reactive-
|
||||
09 06 01 01 1F 07 00 FF 06 00 00 00 A1 - L1 current
|
||||
09 06 01 01 33 07 00 FF 06 00 00 00 C1 - L2 current
|
||||
09 06 01 01 47 07 00 FF 06 00 00 01 8E - L3 current
|
||||
09 06 01 01 20 07 00 FF 12 00 EB - L1 voltage
|
||||
09 06 01 01 34 07 00 FF 12 00 EC - L2 voltage
|
||||
09 06 01 01 48 07 00 FF 12 00 EC - L3 voltage
|
||||
EF 5F 7E
|
||||
45
frames/austria.raw
Normal file
45
frames/austria.raw
Normal file
@@ -0,0 +1,45 @@
|
||||
// HDLC header
|
||||
68
|
||||
01 01 // Format (0x00) and total length (257)
|
||||
|
||||
68 // Start
|
||||
53 // Control field
|
||||
FF // Address (Broadcast address)
|
||||
|
||||
// LLC
|
||||
00 // Control information field
|
||||
01 // Source SAP
|
||||
67 // Destination SAP
|
||||
|
||||
DB // Encrypted
|
||||
08 53 41 47 59 05 E6 D9 FD // System title
|
||||
81 // Prefix for 1-byte length
|
||||
F8 // Length (248), starting from 0xDB and including end byte
|
||||
20 // Security tag 0010 0000, 0=Compression off, 0=Unicast, 1=Encryption, 0=No auth, 0000= Security Suite ID
|
||||
00 72 00 76 // Frame counter
|
||||
|
||||
|
||||
68 01 01 68
|
||||
53 FF 10 01 67
|
||||
DB
|
||||
08 53 41 47 59 05 E6 D9 FD
|
||||
81 F8
|
||||
20
|
||||
00 01 A0 E0
|
||||
0F 80 3E 37 71
|
||||
0C 07 E5 0C 1B 01 0E 00 2D 00 FF C4 02 // Frame timestamp
|
||||
02 23 // 35 items
|
||||
09 0C 07 E5 0C 1B 01 0E 00 2D 00 FF C4 02 // Meter timestamp
|
||||
09 06 01 00 01 08 00 FF 06 00 43 3D 0A 02 02 0F 00 16 1E
|
||||
09 06 01 00 02 08 00 FF 06 00 00 01 03 02 02 0F 00 16 1E
|
||||
09 06 01 00 01 07 00 FF 06 00 00 01 FE 02 02 0F 00 16 1B
|
||||
09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
|
||||
09 06 01 00 20 07 00 FF 12 09 34 02 02 0F FF 16 23
|
||||
09 06 01 00 34 07 00 FF 12 09 34 02 02 0F FF 16 23
|
||||
09 06 01 00 48 07 00 FF 12 09 2D 02 02 0F FF 16 23
|
||||
09 06 01 00 1F 07 00 FF 12 00 63 02 02 0F FE 16 21
|
||||
09 06 01 00 33 07 00 FF 12 00 3F 02 02 0F FE 16 21
|
||||
09 06 01 00 47 07 00 FF 12 00 54 02 02 0F FE 16 21
|
||||
09 06 01 00 0D 07 00 FF 10 03 CF 02 02 0F FD 16 FF // Power factor
|
||||
09 0C 31 37 38 32 31 30 30 31 35 31 36 35 // Meter ID
|
||||
01 67
|
||||
32
frames/decoding.txt
Normal file
32
frames/decoding.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
T = Tag
|
||||
FF = Frame format (4 bit type, 1 bit segmentation, 11 bit frame length)
|
||||
DA = Destination Address (1-4 bytes, LSB=1 terminates)
|
||||
SA = Source address (1-4 bytes, LSB=1 terminates)
|
||||
C = Control (1 byte)
|
||||
HC = HCS (2 bytes)
|
||||
|
||||
LD = LLC Destination
|
||||
LS = LLC Remote
|
||||
LQ = LLC Quality
|
||||
|
||||
AT = Tag (0x0F = unencrypted, 0xDB = encrypted) Not really documented that well...
|
||||
AI = Invoke ID and priority (4 bytes)
|
||||
AD = Date and time
|
||||
AS = System title
|
||||
CT = Cipher frame tag ? Undocumented
|
||||
CL = Lenght of Cipher frame. Length of payload will be this number - 5 (control and counter in start) - 12 (GCM tag appended after payload)
|
||||
CC = Security control
|
||||
CO = Invocation counter
|
||||
|
||||
Security control bits
|
||||
01234567
|
||||
00110000 (0x30)
|
||||
0 = read acces
|
||||
1 = write access
|
||||
2 = Authenticated req
|
||||
3 = Encrypted req
|
||||
4 = Digitally signed req
|
||||
5 = Authenticated res
|
||||
6 = Encrypted res
|
||||
7 = Digitally signed res
|
||||
|
||||
@@ -1,39 +1 @@
|
||||
# Hardware options
|
||||
|
||||
There are currently two possible hardware options for this project, both in need of external power supply. A self powered board is under development by a member of the community and is currently being tested.
|
||||
|
||||
## Hardware v1 by [@roarfred](https://github.com/roarfred)
|
||||
Composed from a ESP12E (or F) chip, this ESP8266 based board is designed specifically for this project with an on board M-bus chip.
|
||||
|
||||
Building this project will require some skills in ordering and assembling electronic circuits as well as programming. No detailed instructions are available.
|
||||
|
||||

|
||||
|
||||
*The completed board mounted in a [3D printed enclosure](/Enclosure)*
|
||||
|
||||
## Assembly of readily available modules
|
||||
You can also use a ESP based development board and combine this with a M-Bus module. Here are a few boards that have been tested, each one has a dedicated firmware file in the releases section.
|
||||
|
||||
### ESP8266 based boards
|
||||
|
||||
[Wemos D1 mini](https://docs.wemos.cc/en/latest/d1/d1_mini.html)
|
||||
- M-Bus connected to GPIO5 (D1)
|
||||
- Jump GPIO4 (D2) to GND to force AP mode during boot
|
||||
- Dallas temp sensor connected to GPIO14 (D5)
|
||||
|
||||
### ESP32 based boards
|
||||
|
||||
[Wemos D32](https://docs.wemos.cc/en/latest/d32/d32.html)
|
||||
- M-Bus connected to GPIO16
|
||||
- Jump GPIO4 to GND to force AP mode during boot
|
||||
- Dallas temp sensor connected to GPIO14
|
||||
|
||||
[Adafruit HUZZAH32](https://www.adafruit.com/product/3405)
|
||||
- M-Bus connected to GPIO16
|
||||
|
||||
|
||||
Combine one of above board with an M-Bus module. Connect 3.3v and GND together between the boards and connect the TX pin from the M-Bus board to the dedicated M-Bus pin on the ESP board.
|
||||
|
||||
[TSS721 M-BUS module board](https://www.aliexpress.com/item/TSS721/32751482255.html)
|
||||
|
||||

|
||||
[See Hardware page in Wiki](https://github.com/gskjold/AmsToMqttBridge/wiki)
|
||||
|
||||
BIN
hardware/img/pow-k-kamstrup.jpg
Normal file
BIN
hardware/img/pow-k-kamstrup.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
hardware/img/pow-u-universal.jpg
Normal file
BIN
hardware/img/pow-u-universal.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
@@ -1,197 +0,0 @@
|
||||
/*
|
||||
* Simple sketch to simulate reading data from a Kamstrup
|
||||
* AMS Meter.
|
||||
*
|
||||
* Created 24. October 2017 by Roar Fredriksen
|
||||
* Modified 06. November 2017 by Ruben Andreassen
|
||||
*/
|
||||
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include <HanReader.h>
|
||||
#include <Kamstrup.h>
|
||||
|
||||
// The HAN Port reader
|
||||
HanReader hanReader;
|
||||
|
||||
// WiFi and MQTT endpoints
|
||||
const char* ssid = "ssid";
|
||||
const char* password = "password";
|
||||
const char* mqtt_server = "ip or dns";
|
||||
const char* mqtt_topic = "sensors/out/espams";
|
||||
const char* device_name = "espams";
|
||||
|
||||
bool enableDebug = false;
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
|
||||
void setup() {
|
||||
//setupDebugPort(); //Comment out this line if you dont need debugging on Serial1
|
||||
setupWiFi();
|
||||
setupMqtt();
|
||||
|
||||
// initialize the HanReader
|
||||
// (passing no han port, as we are feeding data manually, but provide Serial for debugging)
|
||||
if (enableDebug) {
|
||||
hanReader.setup(&Serial, 2400, SERIAL_8N1, &Serial1);
|
||||
} else {
|
||||
hanReader.setup(&Serial, 2400, SERIAL_8N1, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void setupMqtt()
|
||||
{
|
||||
client.setServer(mqtt_server, 1883);
|
||||
}
|
||||
|
||||
void setupDebugPort()
|
||||
{
|
||||
enableDebug = true;
|
||||
// Initialize the Serial port for debugging
|
||||
Serial1.begin(115200);
|
||||
while (!Serial1) {}
|
||||
Serial1.setDebugOutput(true);
|
||||
Serial1.println("Serial1");
|
||||
Serial1.println("Serial debugging port initialized");
|
||||
}
|
||||
|
||||
|
||||
void setupWiFi()
|
||||
{
|
||||
// Initialize wifi
|
||||
if (enableDebug) {
|
||||
Serial1.print("Connecting to ");
|
||||
Serial1.println(ssid);
|
||||
}
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
if (enableDebug) Serial1.print(".");
|
||||
}
|
||||
|
||||
if (enableDebug) {
|
||||
Serial1.println("");
|
||||
Serial1.println("WiFi connected");
|
||||
Serial1.println("IP address: ");
|
||||
Serial1.println(WiFi.localIP());
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
loopMqtt();
|
||||
|
||||
// Read one byte from the port, and see if we got a full package
|
||||
if (hanReader.read())
|
||||
{
|
||||
// Get the list identifier
|
||||
int listSize = hanReader.getListSize();
|
||||
|
||||
if (enableDebug) {
|
||||
Serial1.println("");
|
||||
Serial1.print("List size: ");
|
||||
Serial1.print(listSize);
|
||||
Serial1.print(": ");
|
||||
}
|
||||
|
||||
// Only care for the ACtive Power Imported, which is found in the first list
|
||||
if (listSize == (int)Kamstrup::List1 || listSize == (int)Kamstrup::List2)
|
||||
{
|
||||
// Define a json object to keep the data
|
||||
StaticJsonBuffer<MQTT_MAX_PACKET_SIZE> jsonBuffer;
|
||||
JsonObject& root = jsonBuffer.createObject();
|
||||
|
||||
// Any generic useful info here
|
||||
root["dn"] = device_name;
|
||||
root["up"] = millis();
|
||||
|
||||
// Add a sub-structure to the json object,
|
||||
// to keep the data from the meter itself
|
||||
JsonObject& data = root.createNestedObject("data");
|
||||
|
||||
data["ls"] = listSize;
|
||||
|
||||
data["lvi"] = hanReader.getString((int)Kamstrup_List1::ListVersionIdentifier);
|
||||
data["mid"] = hanReader.getString((int)Kamstrup_List1::MeterID);
|
||||
data["mt"] = hanReader.getString((int)Kamstrup_List1::MeterType);
|
||||
data["t"] = hanReader.getPackageTime();
|
||||
|
||||
data["aip"] = hanReader.getInt((int)Kamstrup_List1::ActiveImportPower); //power
|
||||
data["aep"] = hanReader.getInt((int)Kamstrup_List1::ActiveExportPower);
|
||||
data["rip"] = hanReader.getInt((int)Kamstrup_List1::ReactiveImportPower);
|
||||
data["rep"] = hanReader.getInt((int)Kamstrup_List1::ReactiveExportPower);
|
||||
|
||||
data["al1"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL1) / 100.0;
|
||||
data["al2"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL2) / 100.0;
|
||||
data["al3"] = (float)hanReader.getInt((int)Kamstrup_List1::CurrentL3) / 100.0;
|
||||
|
||||
data["vl1"] = hanReader.getInt((int)Kamstrup_List1::VoltageL1);
|
||||
data["vl2"] = hanReader.getInt((int)Kamstrup_List1::VoltageL2);
|
||||
data["vl3"] = hanReader.getInt((int)Kamstrup_List1::VoltageL3);
|
||||
|
||||
if (listSize == (int)Kamstrup::List2)
|
||||
{
|
||||
data["cl"] = hanReader.getTime((int)Kamstrup_List2::MeterClock);
|
||||
data["caie"] = hanReader.getInt((int)Kamstrup_List2::CumulativeActiveImportEnergy);
|
||||
data["caee"] = hanReader.getInt((int)Kamstrup_List2::CumulativeActiveExportEnergy);
|
||||
data["crie"] = hanReader.getInt((int)Kamstrup_List2::CumulativeReactiveImportEnergy);
|
||||
data["cree"] = hanReader.getInt((int)Kamstrup_List2::CumulativeReactiveExportEnergy);
|
||||
}
|
||||
|
||||
if (enableDebug) {
|
||||
root.printTo(Serial1);
|
||||
Serial1.println("JSON length");
|
||||
Serial1.println(root.measureLength());
|
||||
Serial1.println("");
|
||||
}
|
||||
|
||||
// Publish the json to the MQTT server
|
||||
char msg[MQTT_MAX_PACKET_SIZE];
|
||||
root.printTo(msg, MQTT_MAX_PACKET_SIZE);
|
||||
bool result = client.publish(mqtt_topic, msg);
|
||||
|
||||
if (enableDebug) {
|
||||
Serial1.println("MQTT publish result:");
|
||||
Serial1.println(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ensure the MQTT lirary gets some attention too
|
||||
void loopMqtt()
|
||||
{
|
||||
if (!client.connected()) {
|
||||
reconnectMqtt();
|
||||
}
|
||||
client.loop();
|
||||
}
|
||||
|
||||
void reconnectMqtt() {
|
||||
// Loop until we're reconnected
|
||||
while (!client.connected()) {
|
||||
if (enableDebug) Serial1.print("Attempting MQTT connection...");
|
||||
// Attempt to connect
|
||||
if (client.connect("ESP8266Client")) {
|
||||
if (enableDebug) Serial1.println("connected");
|
||||
// Once connected, publish an announcement...
|
||||
// client.publish("sensors", "hello world");
|
||||
// ... and resubscribe
|
||||
// client.subscribe("inTopic");
|
||||
}
|
||||
else {
|
||||
if (enableDebug) {
|
||||
Serial1.print("failed, rc=");
|
||||
Serial1.print(client.state());
|
||||
Serial1.println(" try again in 5 seconds");
|
||||
}
|
||||
// Wait 5 seconds before retrying
|
||||
delay(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
# Setup
|
||||
|
||||
1. Copy AmsToMqttBridge\Code\Arduino\HanReader\src to Arduino\libraries
|
||||
2. Download the following libraries and put them in Arduino\libraries
|
||||
- ESP8266WiFi
|
||||
- PubSubClient
|
||||
- ArduinoJson
|
||||
3. **Set MQTT_MAX_PACKET_SIZE in PubSubClient.h to at least 512 (i used 1024)**
|
||||
4. Edit the following variables in the project:
|
||||
- ssid
|
||||
- password
|
||||
- mqtt_server
|
||||
- mqtt_topic
|
||||
- device_name
|
||||
|
||||
## Output example:
|
||||
### List 1
|
||||
```
|
||||
{
|
||||
"dn": "espams",
|
||||
"up": 1475902,
|
||||
"data": {
|
||||
"ls": 25,
|
||||
"lvi": "Kamstrup_V0001",
|
||||
"mid": "5706567274389702",
|
||||
"mt": "6841121BN243101040",
|
||||
"t": 1510088840,
|
||||
"aip": 3499,
|
||||
"aep": 0,
|
||||
"rip": 0,
|
||||
"rep": 424,
|
||||
"al1": 10.27,
|
||||
"al2": 6.37,
|
||||
"al3": 11.79,
|
||||
"vl1": 231,
|
||||
"vl2": 226,
|
||||
"vl3": 231
|
||||
}
|
||||
}
|
||||
```
|
||||
### List 2
|
||||
```
|
||||
{
|
||||
"dn": "espams",
|
||||
"up": 1041212,
|
||||
"data": {
|
||||
"ls": 35,
|
||||
"lvi": "Kamstrup_V0001",
|
||||
"mid": "5706567274389702",
|
||||
"mt": "6841121BN243101040",
|
||||
"t": 1510088405,
|
||||
"aip": 4459,
|
||||
"aep": 0,
|
||||
"rip": 0,
|
||||
"rep": 207,
|
||||
"al1": 14.72,
|
||||
"al2": 6.39,
|
||||
"al3": 15.02,
|
||||
"vl1": 231,
|
||||
"vl2": 227,
|
||||
"vl3": 231,
|
||||
"cl": 1510088405,
|
||||
"caie": 588500,
|
||||
"caee": 0,
|
||||
"crie": 93,
|
||||
"cree": 80831
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List 1 and 2 fields
|
||||
- dn = Device Name
|
||||
- up = MS since last reboot
|
||||
- ls = List Size
|
||||
- lvi = List Version Identifier
|
||||
- mid = Meter ID
|
||||
- mt = Meter Type
|
||||
- t = Time
|
||||
- aie = Active Import Power
|
||||
- aep = Active Export Power
|
||||
- rip = Reactive Import Power
|
||||
- rep = Reactive Export Power
|
||||
- al1 = Current L1
|
||||
- al2 = Current L2
|
||||
- al3 = Current L3
|
||||
- cl1 = Voltage L1
|
||||
- cl2 = Voltage L2
|
||||
- cl3 = Voltage L3
|
||||
|
||||
### List 2 additional fields
|
||||
- cl = Meter Clock
|
||||
- caie = Cumulative Active Import Energy
|
||||
- caee = Cumulative Active Export Energy
|
||||
- crie = Cumulative Reactive Import Energy
|
||||
- cree = Cumulative Reactive Export Energy
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* Simple sketch to read MBus data from electrical meter
|
||||
* As the protocol requires "Even" parity, and this is
|
||||
* only supported on the hardware port of the ESP8266,
|
||||
* we'll have to use Serial1 for debugging.
|
||||
*
|
||||
* This means you'll have to program the ESP using the
|
||||
* regular RX/TX port, and then you must remove the FTDI
|
||||
* and connect the MBus signal from the meter to the
|
||||
* RS pin. The FTDI/RX can be moved to Pin2 for debugging
|
||||
*
|
||||
* Created 14. september 2017 by Roar Fredriksen
|
||||
*/
|
||||
|
||||
#include "HanReader.h"
|
||||
#include "Kaifa.h"
|
||||
|
||||
// The HAN Port reader
|
||||
HanReader hanReader;
|
||||
|
||||
void setup() {
|
||||
setupDebugPort();
|
||||
|
||||
// initialize the HanReader
|
||||
// (passing Serial as the HAN port and Serial1 for debugging)
|
||||
hanReader.setup(&Serial, &Serial1);
|
||||
}
|
||||
|
||||
void setupDebugPort()
|
||||
{
|
||||
// Initialize the Serial1 port for debugging
|
||||
// (This port is fixed to Pin2 of the ESP8266)
|
||||
Serial1.begin(115200);
|
||||
while (!Serial1) {}
|
||||
Serial1.setDebugOutput(true);
|
||||
Serial1.println("Serial1");
|
||||
Serial1.println("Serial debugging port initialized");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Read one byte from the port, and see if we got a full package
|
||||
if (hanReader.read())
|
||||
{
|
||||
// Get the list identifier
|
||||
int listSize = hanReader.getListSize();
|
||||
|
||||
Serial1.println("");
|
||||
Serial1.print("List size: ");
|
||||
Serial1.print(listSize);
|
||||
Serial1.print(": ");
|
||||
|
||||
// Only care for the ACtive Power Imported, which is found in the first list
|
||||
if (listSize == (int)Kaifa::List1)
|
||||
{
|
||||
int power = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
|
||||
Serial1.print("Power consumtion is right now: ");
|
||||
Serial1.print(power);
|
||||
Serial1.println(" W");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
/*
|
||||
* Simple sketch to read MBus data from electrical meter
|
||||
* As the protocol requires "Even" parity, and this is
|
||||
* only supported on the hardware port of the ESP8266,
|
||||
* we'll have to use Serial1 for debugging.
|
||||
*
|
||||
* This means you'll have to program the ESP using the
|
||||
* regular RX/TX port, and then you must remove the FTDI
|
||||
* and connect the MBus signal from the meter to the
|
||||
* RS pin. The FTDI/RX can be moved to Pin2 for debugging
|
||||
*
|
||||
* Created 14. september 2017 by Roar Fredriksen
|
||||
*/
|
||||
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <PubSubClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "HanReader.h"
|
||||
#include "Kaifa.h"
|
||||
|
||||
// The HAN Port reader
|
||||
HanReader hanReader;
|
||||
|
||||
// WiFi and MQTT endpoints
|
||||
const char* ssid = "Roar_Etne";
|
||||
const char* password = "**********";
|
||||
const char* mqtt_server = "192.168.10.203";
|
||||
|
||||
WiFiClient espClient;
|
||||
PubSubClient client(espClient);
|
||||
|
||||
void setup() {
|
||||
setupDebugPort();
|
||||
setupWiFi();
|
||||
setupMqtt();
|
||||
|
||||
// initialize the HanReader
|
||||
// (passing Serial as the HAN port and Serial1 for debugging)
|
||||
hanReader.setup(&Serial, &Serial1);
|
||||
}
|
||||
|
||||
void setupMqtt()
|
||||
{
|
||||
client.setServer(mqtt_server, 1883);
|
||||
}
|
||||
|
||||
void setupDebugPort()
|
||||
{
|
||||
// Initialize the Serial1 port for debugging
|
||||
// (This port is fixed to Pin2 of the ESP8266)
|
||||
Serial1.begin(115200);
|
||||
while (!Serial1) {}
|
||||
Serial1.setDebugOutput(true);
|
||||
Serial1.println("Serial1");
|
||||
Serial1.println("Serial debugging port initialized");
|
||||
}
|
||||
|
||||
void setupWiFi()
|
||||
{
|
||||
// Initialize wifi
|
||||
Serial1.print("Connecting to ");
|
||||
Serial1.println(ssid);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial1.print(".");
|
||||
}
|
||||
|
||||
Serial1.println("");
|
||||
Serial1.println("WiFi connected");
|
||||
Serial1.println("IP address: ");
|
||||
Serial1.println(WiFi.localIP());
|
||||
}
|
||||
|
||||
void loop() {
|
||||
loopMqtt();
|
||||
|
||||
// Read one byt from the port, and see if we got a full package
|
||||
if (hanReader.read())
|
||||
{
|
||||
// Get the list identifier
|
||||
int listSize = hanReader.getListSize();
|
||||
|
||||
Serial1.println("");
|
||||
Serial1.print("List size: ");
|
||||
Serial1.print(listSize);
|
||||
Serial1.print(": ");
|
||||
|
||||
// Only care for the ACtive Power Imported, which is found in the first list
|
||||
if (listSize == (int)Kaifa::List1 || listSize == (int)Kaifa::List2 || listSize == (int)Kaifa::List3)
|
||||
{
|
||||
if (listSize == (int)Kaifa::List1)
|
||||
{
|
||||
Serial1.println(" (list #1 has no ID)");
|
||||
}
|
||||
else
|
||||
{
|
||||
String id = hanReader.getString((int)Kaifa_List2::ListVersionIdentifier);
|
||||
Serial1.println(id);
|
||||
}
|
||||
|
||||
// Get the timestamp (as unix time) from the package
|
||||
time_t time = hanReader.getPackageTime();
|
||||
Serial.print("Time of the package is: ");
|
||||
Serial.println(time);
|
||||
|
||||
// Define a json object to keep the data
|
||||
StaticJsonBuffer<500> jsonBuffer;
|
||||
JsonObject& root = jsonBuffer.createObject();
|
||||
|
||||
// Any generic useful info here
|
||||
root["id"] = "espdebugger";
|
||||
root["up"] = millis();
|
||||
root["t"] = time;
|
||||
|
||||
// Add a sub-structure to the json object,
|
||||
// to keep the data from the meter itself
|
||||
JsonObject& data = root.createNestedObject("data");
|
||||
|
||||
// Based on the list number, get all details
|
||||
// according to OBIS specifications for the meter
|
||||
if (listSize == (int)Kaifa::List1)
|
||||
{
|
||||
data["P"] = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
|
||||
}
|
||||
else if (listSize == (int)Kaifa::List2)
|
||||
{
|
||||
data["lv"] = hanReader.getString((int)Kaifa_List2::ListVersionIdentifier);
|
||||
data["id"] = hanReader.getString((int)Kaifa_List2::MeterID);
|
||||
data["type"] = hanReader.getString((int)Kaifa_List2::MeterType);
|
||||
data["P"] = hanReader.getInt((int)Kaifa_List2::ActiveImportPower);
|
||||
data["Q"] = hanReader.getInt((int)Kaifa_List2::ReactiveImportPower);
|
||||
data["I1"] = hanReader.getInt((int)Kaifa_List2::CurrentL1);
|
||||
data["I2"] = hanReader.getInt((int)Kaifa_List2::CurrentL2);
|
||||
data["I3"] = hanReader.getInt((int)Kaifa_List2::CurrentL3);
|
||||
data["U1"] = hanReader.getInt((int)Kaifa_List2::VoltageL1);
|
||||
data["U2"] = hanReader.getInt((int)Kaifa_List2::VoltageL2);
|
||||
data["U3"] = hanReader.getInt((int)Kaifa_List2::VoltageL3);
|
||||
}
|
||||
else if (listSize == (int)Kaifa::List3)
|
||||
{
|
||||
data["lv"] = hanReader.getString((int)Kaifa_List3::ListVersionIdentifier);;
|
||||
data["id"] = hanReader.getString((int)Kaifa_List3::MeterID);
|
||||
data["type"] = hanReader.getString((int)Kaifa_List3::MeterType);
|
||||
data["P"] = hanReader.getInt((int)Kaifa_List3::ActiveImportPower);
|
||||
data["Q"] = hanReader.getInt((int)Kaifa_List3::ReactiveImportPower);
|
||||
data["I1"] = hanReader.getInt((int)Kaifa_List3::CurrentL1);
|
||||
data["I2"] = hanReader.getInt((int)Kaifa_List3::CurrentL2);
|
||||
data["I3"] = hanReader.getInt((int)Kaifa_List3::CurrentL3);
|
||||
data["U1"] = hanReader.getInt((int)Kaifa_List3::VoltageL1);
|
||||
data["U2"] = hanReader.getInt((int)Kaifa_List3::VoltageL2);
|
||||
data["U3"] = hanReader.getInt((int)Kaifa_List3::VoltageL3);
|
||||
data["tPI"] = hanReader.getInt((int)Kaifa_List3::CumulativeActiveImportEnergy);
|
||||
data["tPO"] = hanReader.getInt((int)Kaifa_List3::CumulativeActiveExportEnergy);
|
||||
data["tQI"] = hanReader.getInt((int)Kaifa_List3::CumulativeReactiveImportEnergy);
|
||||
data["tQO"] = hanReader.getInt((int)Kaifa_List3::CumulativeReactiveExportEnergy);
|
||||
}
|
||||
|
||||
// Write the json to the debug port
|
||||
root.printTo(Serial1);
|
||||
Serial1.println();
|
||||
|
||||
// Publish the json to the MQTT server
|
||||
char msg[1024];
|
||||
root.printTo(msg, 1024);
|
||||
client.publish("sensors/out/espdebugger", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the MQTT lirary gets some attention too
|
||||
void loopMqtt()
|
||||
{
|
||||
if (!client.connected()) {
|
||||
reconnectMqtt();
|
||||
}
|
||||
client.loop();
|
||||
}
|
||||
|
||||
void reconnectMqtt() {
|
||||
// Loop until we're reconnected
|
||||
while (!client.connected()) {
|
||||
Serial1.print("Attempting MQTT connection...");
|
||||
// Attempt to connect
|
||||
if (client.connect("ESP8266Client")) {
|
||||
Serial1.println("connected");
|
||||
// Once connected, publish an announcement...
|
||||
// client.publish("sensors", "hello world");
|
||||
// ... and resubscribe
|
||||
// client.subscribe("inTopic");
|
||||
}
|
||||
else {
|
||||
Serial1.print("failed, rc=");
|
||||
Serial1.print(client.state());
|
||||
Serial1.println(" try again in 5 seconds");
|
||||
// Wait 5 seconds before retrying
|
||||
delay(5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
name=HANreader
|
||||
version=1.0.0
|
||||
author=roarfred
|
||||
maintainer=roarfred <not@important.com>
|
||||
sentence=HAN support
|
||||
paragraph=HAN support
|
||||
category=Sensors
|
||||
url=https://github.com/roarfred/AmsToMqttBridge
|
||||
architectures=*
|
||||
@@ -1,19 +0,0 @@
|
||||
Arduino Compatible Cross Platform C++ Library Project : For more information see http://www.visualmicro.com
|
||||
|
||||
This project works exactly the same way as an Arduino library.
|
||||
|
||||
Add this project to any solution that contains an Arduino project and #include <headers.h> in code as you would any normal Arduino library headers.
|
||||
|
||||
To enable intellisense and to support live build discovery outside of the "standard" Arduino library locations, ensure that the library is added as a shared project reference to the master Arduino project. To do this, right click the master project "References" node and then click "Add Reference". A window will open and the library will appear on the "Shared Projects" tab. Click the checkbox next to the library name to add the reference. If this library is moved the shared referencemust be removed and re-added.
|
||||
|
||||
VS2017 has a bug, workround: After moving existing source code within a "library or shared project", close and re-open the solution.
|
||||
|
||||
Visual Studio will display intellisense for libraries based on the platform/board that has been specified for the currently active "Startup Project" of the current solution.
|
||||
|
||||
|
||||
IMPORTANT: The arduino.cc Library Rules must be followed when adding code or restructing libraries.
|
||||
|
||||
|
||||
|
||||
|
||||
blog: http://www.visualmicro.com/post/2017/01/16/Arduino-Cross-Platform-Library-Development.aspx
|
||||
@@ -1,304 +0,0 @@
|
||||
// Aidon.h
|
||||
|
||||
#ifndef _AIDON_h
|
||||
#define _AIDON_h
|
||||
|
||||
enum class Aidon
|
||||
{
|
||||
List1 = 0x01,
|
||||
List1PhaseShort = 0x09,
|
||||
List1PhaseLong = 0x0E,
|
||||
List3PhaseShort = 0x0D,
|
||||
List3PhaseLong = 0x12,
|
||||
List3PhaseITShort = 0x0C,
|
||||
List3PhaseITLong = 0x11,
|
||||
};
|
||||
|
||||
enum class Aidon_List1
|
||||
{
|
||||
ListSize,
|
||||
IGN_0,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
IGN_1,
|
||||
ActiveImportPowerInt8,
|
||||
ActiveImportPowerEnum
|
||||
};
|
||||
|
||||
|
||||
enum class Aidon_List1Phase
|
||||
{
|
||||
ListSize,
|
||||
IGN_0,
|
||||
ListVersionIdentifier_OBIS,
|
||||
ListVersionIdentifier,
|
||||
IGN_1,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
IGN_2,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
IGN_3,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
IGN_4,
|
||||
ActiveImportPowerInt8,
|
||||
ActiveImportPowerEnum,
|
||||
IGN_5,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
IGN_6,
|
||||
ActiveExportPowerInt8,
|
||||
ActiveExportPowerEnum,
|
||||
IGN_7,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
IGN_8,
|
||||
ReactiveImportPowerInt8,
|
||||
ReactiveImportPowerEnum,
|
||||
IGN_9,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
IGN_10,
|
||||
ReactiveExportPowerInt8,
|
||||
ReactiveExportPowerEnum,
|
||||
IGN_11,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
IGN_12,
|
||||
CurrentL1Int8,
|
||||
CurrentL1Enum,
|
||||
IGN_13,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
IGN_14,
|
||||
VoltageL1Int8,
|
||||
VoltageL1Enum,
|
||||
IGN_15,
|
||||
Timestamp_OBIS,
|
||||
Timestamp,
|
||||
IGN_16,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
IGN_17,
|
||||
CumulativeActiveImportEnergyInt8,
|
||||
CumulativeActiveImportEnergyEnum,
|
||||
IGN_18,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
IGN_19,
|
||||
CumulativeActiveExportEnergyInt8,
|
||||
CumulativeActiveExportEnergyEnum,
|
||||
IGN_20,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
IGN_21,
|
||||
CumulativeReactiveImportEnergyInt8,
|
||||
CumulativeReactiveImportEnergyEnum,
|
||||
IGN_22,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy,
|
||||
IGN_23,
|
||||
CumulativeReactiveExportEnergyInt8,
|
||||
CumulativeReactiveExportEnergyEnum
|
||||
};
|
||||
|
||||
enum class Aidon_List3Phase
|
||||
{
|
||||
ListSize,
|
||||
IGN_0,
|
||||
ListVersionIdentifier_OBIS,
|
||||
ListVersionIdentifier,
|
||||
IGN_1,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
IGN_2,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
IGN_3,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
IGN_4,
|
||||
ActiveImportPowerInt8,
|
||||
ActiveImportPowerEnum,
|
||||
IGN_5,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
IGN_6,
|
||||
ActiveExportPowerInt8,
|
||||
ActiveExportPowerEnum,
|
||||
IGN_7,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
IGN_8,
|
||||
ReactiveImportPowerInt8,
|
||||
ReactiveImportPowerEnum,
|
||||
IGN_9,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
IGN_10,
|
||||
ReactiveExportPowerInt8,
|
||||
ReactiveExportPowerEnum,
|
||||
IGN_11,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
IGN_12,
|
||||
CurrentL1Int8,
|
||||
CurrentL1Enum,
|
||||
IGN_13,
|
||||
CurrentL2_OBIS,
|
||||
CurrentL2,
|
||||
IGN_14,
|
||||
CurrentL2Int8,
|
||||
CurrentL2Enum,
|
||||
IGN_15,
|
||||
CurrentL3_OBIS,
|
||||
CurrentL3,
|
||||
IGN_16,
|
||||
CurrentL3Int8,
|
||||
CurrentL3Enum,
|
||||
IGN_17,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
IGN_18,
|
||||
VoltageL1Int8,
|
||||
VoltageL1Enum,
|
||||
IGN_19,
|
||||
VoltageL2_OBIS,
|
||||
VoltageL2,
|
||||
IGN_20,
|
||||
VoltageL2Int8,
|
||||
VoltageL2Enum,
|
||||
IGN_21,
|
||||
VoltageL3_OBIS,
|
||||
VoltageL3,
|
||||
IGN_22,
|
||||
VoltageL3Int8,
|
||||
VoltageL3Enum,
|
||||
IGN_23,
|
||||
Timestamp_OBIS,
|
||||
Timestamp,
|
||||
IGN_24,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
IGN_25,
|
||||
CumulativeActiveImportEnergyInt8,
|
||||
CumulativeActiveImportEnergyEnum,
|
||||
IGN_26,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
IGN_27,
|
||||
CumulativeActiveExportEnergyInt8,
|
||||
CumulativeActiveExportEnergyEnum,
|
||||
IGN_28,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
IGN_29,
|
||||
CumulativeReactiveImportEnergyInt8,
|
||||
CumulativeReactiveImportEnergyEnum,
|
||||
IGN_30,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy,
|
||||
IGN_31,
|
||||
CumulativeReactiveExportEnergyInt8,
|
||||
CumulativeReactiveExportEnergyEnum
|
||||
};
|
||||
|
||||
enum class Aidon_List3PhaseIT
|
||||
{
|
||||
ListSize,
|
||||
IGN_0,
|
||||
ListVersionIdentifier_OBIS,
|
||||
ListVersionIdentifier,
|
||||
IGN_1,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
IGN_2,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
IGN_3,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
IGN_4,
|
||||
ActiveImportPowerInt8,
|
||||
ActiveImportPowerEnum,
|
||||
IGN_5,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
IGN_6,
|
||||
ActiveExportPowerInt8,
|
||||
ActiveExportPowerEnum,
|
||||
IGN_7,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
IGN_8,
|
||||
ReactiveImportPowerInt8,
|
||||
ReactiveImportPowerEnum,
|
||||
IGN_9,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
IGN_10,
|
||||
ReactiveExportPowerInt8,
|
||||
ReactiveExportPowerEnum,
|
||||
IGN_11,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
IGN_12,
|
||||
CurrentL1Int8,
|
||||
CurrentL1Enum,
|
||||
IGN_13,
|
||||
CurrentL3_OBIS,
|
||||
CurrentL3,
|
||||
IGN_14,
|
||||
CurrentL3Int8,
|
||||
CurrentL3Enum,
|
||||
IGN_15,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
IGN_16,
|
||||
VoltageL1Int8,
|
||||
VoltageL1Enum,
|
||||
IGN_17,
|
||||
VoltageL2_OBIS,
|
||||
VoltageL2,
|
||||
IGN_18,
|
||||
VoltageL2Int8,
|
||||
VoltageL2Enum,
|
||||
IGN_19,
|
||||
VoltageL3_OBIS,
|
||||
VoltageL3,
|
||||
IGN_20,
|
||||
VoltageL3Int8,
|
||||
VoltageL3Enum,
|
||||
IGN_21,
|
||||
Timestamp_OBIS,
|
||||
Timestamp,
|
||||
IGN_22,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
IGN_23,
|
||||
CumulativeActiveImportEnergyInt8,
|
||||
CumulativeActiveImportEnergyEnum,
|
||||
IGN_24,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
IGN_25,
|
||||
CumulativeActiveExportEnergyInt8,
|
||||
CumulativeActiveExportEnergyEnum,
|
||||
IGN_26,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
IGN_27,
|
||||
CumulativeReactiveImportEnergyInt8,
|
||||
CumulativeReactiveImportEnergyEnum,
|
||||
IGN_28,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy,
|
||||
IGN_29,
|
||||
CumulativeReactiveExportEnergyInt8,
|
||||
CumulativeReactiveExportEnergyEnum
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "Crc16.h"
|
||||
|
||||
Crc16Class::Crc16Class()
|
||||
{
|
||||
unsigned short value;
|
||||
unsigned short temp;
|
||||
for (unsigned short i = 0; i < 256; ++i)
|
||||
{
|
||||
value = 0;
|
||||
temp = i;
|
||||
for (byte j = 0; j < 8; ++j)
|
||||
{
|
||||
if (((value ^ temp) & 0x0001) != 0)
|
||||
{
|
||||
value = (ushort)((value >> 1) ^ polynomial);
|
||||
}
|
||||
else
|
||||
{
|
||||
value >>= 1;
|
||||
}
|
||||
temp >>= 1;
|
||||
}
|
||||
table[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned short Crc16Class::ComputeChecksum(byte *data, int start, int length)
|
||||
{
|
||||
ushort fcs = 0xffff;
|
||||
for (int i = start; i < (start + length); i++)
|
||||
{
|
||||
byte index = (fcs ^ data[i]) & 0xff;
|
||||
fcs = (ushort)((fcs >> 8) ^ table[index]);
|
||||
}
|
||||
fcs ^= 0xffff;
|
||||
return fcs;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#ifndef _CRC16_h
|
||||
#define _CRC16_h
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
class Crc16Class
|
||||
{
|
||||
public:
|
||||
Crc16Class();
|
||||
unsigned short ComputeChecksum(byte *data, int start, int length);
|
||||
protected:
|
||||
private:
|
||||
const unsigned short polynomial = 0x8408;
|
||||
unsigned short table[256];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#include "DlmsReader.h"
|
||||
|
||||
DlmsReader::DlmsReader()
|
||||
{
|
||||
//this->Clear();
|
||||
}
|
||||
|
||||
void DlmsReader::Clear()
|
||||
{
|
||||
this->position = 0;
|
||||
this->dataLength = 0;
|
||||
this->destinationAddressLength = 0;
|
||||
this->sourceAddressLength = 0;
|
||||
this->frameFormatType = 0;
|
||||
}
|
||||
|
||||
bool DlmsReader::Read(byte data)
|
||||
{
|
||||
if (position == 0 && data != 0x7E)
|
||||
{
|
||||
// we haven't started yet, wait for the start flag (no need to capture any data yet)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have completed reading of one package, so clear and be ready for the next
|
||||
if (dataLength > 0 && position >= dataLength + 2)
|
||||
Clear();
|
||||
|
||||
// Check if we're about to run into a buffer overflow
|
||||
if (position >= DLMS_READER_BUFFER_SIZE)
|
||||
Clear();
|
||||
|
||||
// Check if this is a second start flag, which indicates the previous one was a stop from the last package
|
||||
if (position == 1 && data == 0x7E)
|
||||
{
|
||||
// just return, we can keep the one byte we had in the buffer
|
||||
return false;
|
||||
}
|
||||
|
||||
// We have started, so capture every byte
|
||||
buffer[position++] = data;
|
||||
|
||||
if (position == 1)
|
||||
{
|
||||
// This was the start flag, we're not done yet
|
||||
return false;
|
||||
}
|
||||
else if (position == 2)
|
||||
{
|
||||
// Capture the Frame Format Type
|
||||
frameFormatType = (byte)(data & 0xF0);
|
||||
if (!IsValidFrameFormat(frameFormatType))
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
else if (position == 3)
|
||||
{
|
||||
// Capture the length of the data package
|
||||
dataLength = ((buffer[1] & 0x0F) << 8) | buffer[2];
|
||||
return false;
|
||||
}
|
||||
else if (destinationAddressLength == 0)
|
||||
{
|
||||
// Capture the destination address
|
||||
destinationAddressLength = GetAddress(3, destinationAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE);
|
||||
if (destinationAddressLength > 3)
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
else if (sourceAddressLength == 0)
|
||||
{
|
||||
// Capture the source address
|
||||
sourceAddressLength = GetAddress(3 + destinationAddressLength, sourceAddress, 0, DLMS_READER_MAX_ADDRESS_SIZE);
|
||||
if (sourceAddressLength > 3)
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
else if (position == 4 + destinationAddressLength + sourceAddressLength + 2)
|
||||
{
|
||||
// Verify the header checksum
|
||||
ushort headerChecksum = GetChecksum(position - 3);
|
||||
if (headerChecksum != Crc16.ComputeChecksum(buffer, 1, position - 3))
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
else if (position == dataLength + 1)
|
||||
{
|
||||
// Verify the data package checksum
|
||||
ushort checksum = this->GetChecksum(position - 3);
|
||||
if (checksum != Crc16.ComputeChecksum(buffer, 1, position - 3))
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
else if (position == dataLength + 2)
|
||||
{
|
||||
// We're done, check the stop flag and signal we're done
|
||||
if (data == 0x7E)
|
||||
return true;
|
||||
else
|
||||
{
|
||||
Clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DlmsReader::IsValidFrameFormat(byte frameFormatType)
|
||||
{
|
||||
return frameFormatType == 0xA0;
|
||||
}
|
||||
|
||||
int DlmsReader::GetRawData(byte *dataBuffer, int start, int length)
|
||||
{
|
||||
if (dataLength > 0 && position == dataLength + 2)
|
||||
{
|
||||
int headerLength = 3 + destinationAddressLength + sourceAddressLength + 2;
|
||||
int bytesWritten = 0;
|
||||
for (int i = headerLength + 1; i < dataLength - 1; i++)
|
||||
{
|
||||
dataBuffer[i + start - headerLength - 1] = buffer[i];
|
||||
bytesWritten++;
|
||||
}
|
||||
return bytesWritten;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
int DlmsReader::GetAddress(int addressPosition, byte* addressBuffer, int start, int length)
|
||||
{
|
||||
int addressBufferPos = start;
|
||||
for (int i = addressPosition; i < position; i++)
|
||||
{
|
||||
addressBuffer[addressBufferPos++] = buffer[i];
|
||||
|
||||
// LSB=1 means this was the last address byte
|
||||
if ((buffer[i] & 0x01) == 0x01)
|
||||
break;
|
||||
|
||||
// See if we've reached last byte, try again when we've got more data
|
||||
else if (i == position - 1)
|
||||
return 0;
|
||||
}
|
||||
return addressBufferPos - start;
|
||||
}
|
||||
|
||||
ushort DlmsReader::GetChecksum(int checksumPosition)
|
||||
{
|
||||
return (ushort)(buffer[checksumPosition + 2] << 8 |
|
||||
buffer[checksumPosition + 1]);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#ifndef _DLMSREADER_h
|
||||
#define _DLMSREADER_h
|
||||
|
||||
#include "Crc16.h"
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
#define DLMS_READER_BUFFER_SIZE 512
|
||||
#define DLMS_READER_MAX_ADDRESS_SIZE 5
|
||||
|
||||
class DlmsReader
|
||||
{
|
||||
public:
|
||||
DlmsReader();
|
||||
bool Read(byte data);
|
||||
int GetRawData(byte *buffer, int start, int length);
|
||||
|
||||
protected:
|
||||
Crc16Class Crc16;
|
||||
|
||||
private:
|
||||
byte buffer[DLMS_READER_BUFFER_SIZE];
|
||||
int position;
|
||||
int dataLength;
|
||||
byte frameFormatType;
|
||||
byte destinationAddress[DLMS_READER_MAX_ADDRESS_SIZE];
|
||||
byte destinationAddressLength;
|
||||
byte sourceAddress[DLMS_READER_MAX_ADDRESS_SIZE];
|
||||
byte sourceAddressLength;
|
||||
|
||||
void Clear();
|
||||
int GetAddress(int addressPosition, byte* buffer, int start, int length);
|
||||
unsigned short GetChecksum(int checksumPosition);
|
||||
bool IsValidFrameFormat(byte frameFormatType);
|
||||
void WriteBuffer();
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,288 +0,0 @@
|
||||
#include "HanReader.h"
|
||||
|
||||
HanReader::HanReader()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void HanReader::setup(Stream *hanPort, Stream *debugPort)
|
||||
{
|
||||
han = hanPort;
|
||||
bytesRead = 0;
|
||||
debug = debugPort;
|
||||
if (debug) debug->println("MBUS serial setup complete");
|
||||
}
|
||||
|
||||
void HanReader::setup(Stream *hanPort)
|
||||
{
|
||||
setup(hanPort, NULL);
|
||||
}
|
||||
|
||||
bool HanReader::read(byte data)
|
||||
{
|
||||
if (reader.Read(data))
|
||||
{
|
||||
bytesRead = reader.GetRawData(buffer, 0, 512);
|
||||
if (debug)
|
||||
{
|
||||
debug->print("Got valid DLMS data (");
|
||||
debug->print(bytesRead);
|
||||
debug->println(" bytes):");
|
||||
debugPrint(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
/*
|
||||
Data should start with E6 E7 00 0F
|
||||
and continue with four bytes for the InvokeId
|
||||
*/
|
||||
if (bytesRead < 9)
|
||||
{
|
||||
if (debug) debug->println("Invalid HAN data: Less than 9 bytes received");
|
||||
return false;
|
||||
}
|
||||
else if (
|
||||
buffer[0] != 0xE6 ||
|
||||
buffer[1] != 0xE7 ||
|
||||
buffer[2] != 0x00 ||
|
||||
buffer[3] != 0x0F
|
||||
)
|
||||
{
|
||||
if (debug) debug->println("Invalid HAN data: Start should be E6 E7 00 0F");
|
||||
return false;
|
||||
}
|
||||
|
||||
listSize = getInt(0, buffer, 0, bytesRead);
|
||||
if (debug) debug->print("HAN data is valid, listSize: ");
|
||||
if (debug) debug->println(listSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void HanReader::debugPrint(byte *buffer, int start, int length)
|
||||
{
|
||||
for (int i = start; i < start + length; i++)
|
||||
{
|
||||
if (buffer[i] < 0x10)
|
||||
debug->print("0");
|
||||
debug->print(buffer[i], HEX);
|
||||
debug->print(" ");
|
||||
if ((i - start + 1) % 16 == 0)
|
||||
debug->println("");
|
||||
else if ((i - start + 1) % 4 == 0)
|
||||
debug->print(" ");
|
||||
|
||||
yield(); // Let other get some resources too
|
||||
}
|
||||
debug->println("");
|
||||
}
|
||||
|
||||
bool HanReader::read()
|
||||
{
|
||||
while(han->available()) {
|
||||
if(read(han->read())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int HanReader::getListSize()
|
||||
{
|
||||
return listSize;
|
||||
}
|
||||
|
||||
time_t HanReader::getPackageTime()
|
||||
{
|
||||
int packageTimePosition = dataHeader
|
||||
+ (compensateFor09HeaderBug ? 1 : 0);
|
||||
|
||||
return getTime(buffer, packageTimePosition, bytesRead);
|
||||
}
|
||||
|
||||
time_t HanReader::getTime(int objectId)
|
||||
{
|
||||
return getTime(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
int HanReader::getInt(int objectId)
|
||||
{
|
||||
return getInt(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
String HanReader::getString(int objectId)
|
||||
{
|
||||
return getString(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
|
||||
int HanReader::findValuePosition(int dataPosition, byte *buffer, int start, int length)
|
||||
{
|
||||
// The first byte after the header gives the length
|
||||
// of the extended header information (variable)
|
||||
int headerSize = dataHeader + (compensateFor09HeaderBug ? 1 : 0);
|
||||
int firstData = headerSize + buffer[headerSize] + 1;
|
||||
|
||||
for (int i = start + firstData; i<length; i++)
|
||||
{
|
||||
if (dataPosition-- == 0)
|
||||
return i;
|
||||
else if (buffer[i] == 0x00) // null
|
||||
i += 0;
|
||||
else if (buffer[i] == 0x0A) // String
|
||||
i += buffer[i + 1] + 1;
|
||||
else if (buffer[i] == 0x09) // byte array
|
||||
i += buffer[i + 1] + 1;
|
||||
else if (buffer[i] == 0x01) // array (1 byte for reading size)
|
||||
i += 1;
|
||||
else if (buffer[i] == 0x02) // struct (1 byte for reading size)
|
||||
i += 1;
|
||||
else if (buffer[i] == 0x10) // int16 value (2 bytes)
|
||||
i += 2;
|
||||
else if (buffer[i] == 0x12) // uint16 value (2 bytes)
|
||||
i += 2;
|
||||
else if (buffer[i] == 0x06) // uint32 value (4 bytes)
|
||||
i += 4;
|
||||
else if (buffer[i] == 0x0F) // int8 value (1 bytes)
|
||||
i += 1;
|
||||
else if (buffer[i] == 0x16) // enum (1 bytes)
|
||||
i += 1;
|
||||
else
|
||||
{
|
||||
if (debug)
|
||||
{
|
||||
debug->print("Unknown data type found: 0x");
|
||||
debug->println(buffer[i], HEX);
|
||||
}
|
||||
return 0; // unknown data type found
|
||||
}
|
||||
}
|
||||
|
||||
if (debug)
|
||||
{
|
||||
debug->print("Passed the end of the data. Length was: ");
|
||||
debug->println(length);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
time_t HanReader::getTime(int dataPosition, byte *buffer, int start, int length)
|
||||
{
|
||||
// TODO: check if the time is represented always as a 12 byte string (0x09 0x0C)
|
||||
int timeStart = findValuePosition(dataPosition, buffer, start, length);
|
||||
timeStart += 1;
|
||||
return getTime(buffer, start + timeStart, length - timeStart);
|
||||
}
|
||||
|
||||
time_t HanReader::getTime(byte *buffer, int start, int length)
|
||||
{
|
||||
int pos = start;
|
||||
int dataLength = buffer[pos++];
|
||||
|
||||
if (dataLength == 0x0C)
|
||||
{
|
||||
int year = buffer[pos] << 8 |
|
||||
buffer[pos + 1];
|
||||
|
||||
int month = buffer[pos + 2];
|
||||
int day = buffer[pos + 3];
|
||||
int hour = buffer[pos + 5];
|
||||
int minute = buffer[pos + 6];
|
||||
int second = buffer[pos + 7];
|
||||
|
||||
return toUnixTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Date format not supported
|
||||
return (time_t)0L;
|
||||
}
|
||||
}
|
||||
|
||||
int HanReader::getInt(int dataPosition, byte *buffer, int start, int length)
|
||||
{
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
|
||||
if (valuePosition > 0)
|
||||
{
|
||||
int value = 0;
|
||||
int bytes = 0;
|
||||
switch (buffer[valuePosition++])
|
||||
{
|
||||
case 0x10:
|
||||
bytes = 2;
|
||||
break;
|
||||
case 0x12:
|
||||
bytes = 2;
|
||||
break;
|
||||
case 0x06:
|
||||
bytes = 4;
|
||||
break;
|
||||
case 0x02:
|
||||
bytes = 1;
|
||||
break;
|
||||
case 0x01:
|
||||
bytes = 1;
|
||||
break;
|
||||
case 0x0F:
|
||||
bytes = 1;
|
||||
break;
|
||||
case 0x16:
|
||||
bytes = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = valuePosition; i < valuePosition + bytes; i++)
|
||||
{
|
||||
value = value << 8 | buffer[i];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String HanReader::getString(int dataPosition, byte *buffer, int start, int length)
|
||||
{
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0)
|
||||
{
|
||||
String value = String("");
|
||||
for (int i = valuePosition + 2; i < valuePosition + buffer[valuePosition + 1] + 2; i++)
|
||||
{
|
||||
value += String((char)buffer[i]);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return String("");
|
||||
}
|
||||
|
||||
time_t HanReader::toUnixTime(int year, int month, int day, int hour, int minute, int second)
|
||||
{
|
||||
byte daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
||||
long secondsPerMinute = 60;
|
||||
long secondsPerHour = secondsPerMinute * 60;
|
||||
long secondsPerDay = secondsPerHour * 24;
|
||||
|
||||
long time = (year - 1970) * secondsPerDay * 365L;
|
||||
|
||||
for (int yearCounter = 1970; yearCounter<year; yearCounter++)
|
||||
if ((yearCounter % 4 == 0) && ((yearCounter % 100 != 0) || (yearCounter % 400 == 0)))
|
||||
time += secondsPerDay;
|
||||
|
||||
if (month > 2 && (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)))
|
||||
time += secondsPerDay;
|
||||
|
||||
for (int monthCounter = 1; monthCounter<month; monthCounter++)
|
||||
time += daysInMonth[monthCounter - 1] * secondsPerDay;
|
||||
|
||||
time += (day - 1) * secondsPerDay;
|
||||
time += hour * secondsPerHour;
|
||||
time += minute * secondsPerMinute;
|
||||
time += second;
|
||||
|
||||
return (time_t)time;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
#ifndef _HANREADER_h
|
||||
#define _HANREADER_h
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
|
||||
#include "DlmsReader.h"
|
||||
|
||||
|
||||
class HanReader
|
||||
{
|
||||
public:
|
||||
const uint dataHeader = 8;
|
||||
bool compensateFor09HeaderBug = false;
|
||||
|
||||
HanReader();
|
||||
void setup(Stream *hanPort);
|
||||
void setup(Stream *hanPort, Stream *debugPort);
|
||||
bool read();
|
||||
bool read(byte data);
|
||||
int getListSize();
|
||||
time_t getPackageTime();
|
||||
int getInt(int objectId);
|
||||
String getString(int objectId);
|
||||
time_t getTime(int objectId);
|
||||
|
||||
private:
|
||||
Stream *debug;
|
||||
Stream *han;
|
||||
byte buffer[512];
|
||||
int bytesRead;
|
||||
DlmsReader reader;
|
||||
int listSize;
|
||||
|
||||
int findValuePosition(int dataPosition, byte *buffer, int start, int length);
|
||||
|
||||
time_t getTime(int dataPosition, byte *buffer, int start, int length);
|
||||
time_t getTime(byte *buffer, int start, int length);
|
||||
int getInt(int dataPosition, byte *buffer, int start, int length);
|
||||
String getString(int dataPosition, byte *buffer, int start, int length);
|
||||
|
||||
time_t toUnixTime(int year, int month, int day, int hour, int minute, int second);
|
||||
|
||||
void debugPrint(byte *buffer, int start, int length);
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
@@ -1,57 +0,0 @@
|
||||
#ifndef _KAIFA_h
|
||||
#define _KAIFA_h
|
||||
|
||||
enum class Kaifa : byte {
|
||||
List1 = 0x01,
|
||||
List1PhaseShort = 0x09,
|
||||
List3PhaseShort = 0x0D,
|
||||
List1PhaseLong = 0x0E,
|
||||
List3PhaseLong = 0x12
|
||||
};
|
||||
|
||||
enum class Kaifa_List1 {
|
||||
ListSize,
|
||||
ActivePowerImported
|
||||
};
|
||||
|
||||
enum class Kaifa_List3Phase {
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
MeterID,
|
||||
MeterType,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower,
|
||||
CurrentL1,
|
||||
CurrentL2,
|
||||
CurrentL3,
|
||||
VoltageL1,
|
||||
VoltageL2,
|
||||
VoltageL3,
|
||||
MeterClock,
|
||||
CumulativeActiveImportEnergy,
|
||||
CumulativeActiveExportEnergy,
|
||||
CumulativeReactiveImportEnergy,
|
||||
CumulativeReactiveExportEnergy
|
||||
};
|
||||
|
||||
enum class Kaifa_List1Phase {
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
MeterID,
|
||||
MeterType,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower,
|
||||
CurrentL1,
|
||||
VoltageL1,
|
||||
MeterClock,
|
||||
CumulativeActiveImportEnergy,
|
||||
CumulativeActiveExportEnergy,
|
||||
CumulativeReactiveImportEnergy,
|
||||
CumulativeReactiveExportEnergy
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,87 +0,0 @@
|
||||
// Kamstrup.h
|
||||
|
||||
#ifndef _KAMSTRUP_h
|
||||
#define _KAMSTRUP_h
|
||||
|
||||
enum class Kamstrup
|
||||
{
|
||||
List3PhaseShort = 0x19,
|
||||
List3PhaseLong = 0x23,
|
||||
List1PhaseShort = 0x11,
|
||||
List1PhaseLong = 0x1B
|
||||
};
|
||||
|
||||
enum class Kamstrup_List3Phase
|
||||
{
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
CurrentL2_OBIS,
|
||||
CurrentL2,
|
||||
CurrentL3_OBIS,
|
||||
CurrentL3,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
VoltageL2_OBIS,
|
||||
VoltageL2,
|
||||
VoltageL3_OBIS,
|
||||
VoltageL3,
|
||||
MeterClock_OBIS,
|
||||
MeterClock,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy
|
||||
};
|
||||
|
||||
enum class Kamstrup_List1Phase
|
||||
{
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
MeterClock_OBIS,
|
||||
MeterClock,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,12 +4,18 @@ default_envs = dev
|
||||
[env:dev]
|
||||
platform = espressif8266
|
||||
board = esp12e
|
||||
framework = ${common.framework}
|
||||
board_build.ldscript = eagle.flash.4m2m.ld
|
||||
framework = arduino
|
||||
lib_deps = ${common.lib_deps}
|
||||
lib_ignore = ${common.lib_ignore}
|
||||
lib_compat_mode = off
|
||||
build_flags =
|
||||
-D HW_ROARFRED=1
|
||||
-D WEBSOCKET_DISABLED=1
|
||||
-D DEBUG_MODE=1
|
||||
monitor_speed = 2400
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
monitor_speed = 115200 ; If serial port is shared with HAN, use baud and parity configured for meter
|
||||
monitor_flags =
|
||||
--parity
|
||||
E
|
||||
N
|
||||
|
||||
@@ -1,63 +1,32 @@
|
||||
[platformio]
|
||||
extra_configs = platformio-user.ini
|
||||
|
||||
|
||||
[common]
|
||||
lib_deps = Timezone@1.2.4, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1
|
||||
lib_ignore = OneWire
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266@3.2.0
|
||||
board = esp12e
|
||||
board_build.ldscript = eagle.flash.4m2m.ld
|
||||
framework = arduino
|
||||
lib_deps = HanReader@1.0.0, ArduinoJson@6.14.1, MQTT@2.4.7, DallasTemperature@3.8.1, EspSoftwareSerial@6.7.1, Base64@1.0.0
|
||||
|
||||
[env:hw1esp12e]
|
||||
platform = espressif8266@2.3.3
|
||||
board = esp12e
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
lib_ignore = ${common.lib_ignore}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
build_flags =
|
||||
-D HW_ROARFRED=1
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
-D WEBSOCKET_DISABLED=1
|
||||
|
||||
[env:esp12e]
|
||||
platform = espressif8266@2.3.3
|
||||
board = esp12e
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
|
||||
[env:d1mini]
|
||||
platform = espressif8266@2.3.3
|
||||
board = d1_mini
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
|
||||
[env:esp32]
|
||||
platform = espressif32@1.11.2
|
||||
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
|
||||
board = esp32dev
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
|
||||
[env:lolind32]
|
||||
platform = espressif32@1.11.2
|
||||
board = lolin_d32
|
||||
framework = ${common.framework}
|
||||
lib_deps = ${common.lib_deps}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
|
||||
[env:featheresp32]
|
||||
platform = espressif32@1.11.2
|
||||
board = featheresp32
|
||||
framework = ${common.framework}
|
||||
framework = arduino
|
||||
lib_deps = ${common.lib_deps}
|
||||
lib_ignore = ${common.lib_ignore}
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
scripts/makeweb.py
|
||||
build_flags =
|
||||
-D WEBSOCKET_DISABLED=1
|
||||
|
||||
@@ -2,9 +2,34 @@ import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from css_html_js_minify import html_minify, js_minify, css_minify
|
||||
except:
|
||||
from SCons.Script import (
|
||||
ARGUMENTS,
|
||||
COMMAND_LINE_TARGETS,
|
||||
DefaultEnvironment,
|
||||
)
|
||||
env = DefaultEnvironment()
|
||||
|
||||
env.Execute(
|
||||
env.VerboseAction(
|
||||
'$PYTHONEXE -m pip install "css_html_js_minify" ',
|
||||
"Installing Python dependencies",
|
||||
)
|
||||
)
|
||||
try:
|
||||
from css_html_js_minify import html_minify, js_minify, css_minify
|
||||
except:
|
||||
print("WARN: Unable to load minifier")
|
||||
|
||||
|
||||
webroot = "web"
|
||||
srcroot = "src/web/root"
|
||||
|
||||
version = os.environ.get('GITHUB_TAG')
|
||||
if version == None:
|
||||
version = "SNAPSHOT"
|
||||
|
||||
if os.path.exists(srcroot):
|
||||
shutil.rmtree(srcroot)
|
||||
@@ -20,11 +45,28 @@ for filename in os.listdir(webroot):
|
||||
|
||||
varname = basename.upper()
|
||||
|
||||
with open(srcfile, encoding="utf-8") as f:
|
||||
content = f.read().replace("${version}", version)
|
||||
|
||||
try:
|
||||
if filename.endswith(".html"):
|
||||
content = html_minify(content)
|
||||
elif filename.endswith(".css"):
|
||||
content = css_minify(content)
|
||||
elif (filename.endswith(".js") and filename != 'gaugemeter.js') or filename.endswith(".json"):
|
||||
content = js_minify(content)
|
||||
except:
|
||||
print("WARN: Unable to minify")
|
||||
|
||||
with open(dstfile, "w") as dst:
|
||||
dst.write("const char ")
|
||||
dst.write(varname)
|
||||
dst.write("[] PROGMEM = R\"==\"==(\n")
|
||||
with open(srcfile, "r") as src:
|
||||
for line in src.readlines():
|
||||
dst.write(line)
|
||||
dst.write("\n)==\"==\";\n")
|
||||
dst.write("[] PROGMEM = R\"==\"==(")
|
||||
dst.write(content)
|
||||
dst.write(")==\"==\";\n")
|
||||
dst.write("const int ");
|
||||
dst.write(varname)
|
||||
dst.write("_LEN PROGMEM = ");
|
||||
dst.write(str(len(content)))
|
||||
dst.write(";");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,108 +3,320 @@
|
||||
#include <EEPROM.h>
|
||||
#include "Arduino.h"
|
||||
|
||||
#define EEPROM_SIZE 1024*3
|
||||
#define EEPROM_CHECK_SUM 92 // Used to check if config is stored. Change if structure changes
|
||||
#define EEPROM_CONFIG_ADDRESS 0
|
||||
#define EEPROM_TEMP_CONFIG_ADDRESS 2048
|
||||
|
||||
#define CONFIG_SYSTEM_START 8
|
||||
#define CONFIG_METER_START 224
|
||||
#define CONFIG_GPIO_START 266
|
||||
#define CONFIG_ENTSOE_START 290
|
||||
#define CONFIG_WIFI_START 360
|
||||
#define CONFIG_WEB_START 648
|
||||
#define CONFIG_DEBUG_START 824
|
||||
#define CONFIG_DOMOTICZ_START 856
|
||||
#define CONFIG_NTP_START 872
|
||||
#define CONFIG_MQTT_START 1004
|
||||
|
||||
#define CONFIG_MQTT_START_86 224
|
||||
#define CONFIG_METER_START_87 784
|
||||
#define CONFIG_ENTSOE_START_90 286
|
||||
#define CONFIG_WIFI_START_91 16
|
||||
|
||||
|
||||
struct SystemConfig {
|
||||
uint8_t boardType;
|
||||
}; // 1
|
||||
|
||||
struct WiFiConfig91 {
|
||||
char ssid[32];
|
||||
char psk[64];
|
||||
char ip[15];
|
||||
char gateway[15];
|
||||
char subnet[15];
|
||||
char dns1[15];
|
||||
char dns2[15];
|
||||
char hostname[32];
|
||||
bool mdns;
|
||||
}; // 204
|
||||
|
||||
struct WiFiConfig {
|
||||
char ssid[32];
|
||||
char psk[64];
|
||||
char ip[16];
|
||||
char gateway[16];
|
||||
char subnet[16];
|
||||
char dns1[16];
|
||||
char dns2[16];
|
||||
char hostname[32];
|
||||
bool mdns;
|
||||
}; // 209
|
||||
|
||||
struct MqttConfig86 {
|
||||
char host[128];
|
||||
uint16_t port;
|
||||
char clientId[32];
|
||||
char publishTopic[64];
|
||||
char subscribeTopic[64];
|
||||
char username[64];
|
||||
char password[64];
|
||||
uint8_t payloadFormat;
|
||||
bool ssl;
|
||||
}; // 420
|
||||
|
||||
struct MqttConfig {
|
||||
char host[128];
|
||||
uint16_t port;
|
||||
char clientId[32];
|
||||
char publishTopic[64];
|
||||
char subscribeTopic[64];
|
||||
char username[128];
|
||||
char password[256];
|
||||
uint8_t payloadFormat;
|
||||
bool ssl;
|
||||
}; // 676
|
||||
|
||||
struct WebConfig {
|
||||
uint8_t security;
|
||||
char username[64];
|
||||
char password[64];
|
||||
}; // 129
|
||||
|
||||
struct MeterConfig {
|
||||
uint32_t baud;
|
||||
uint8_t parity;
|
||||
bool invert;
|
||||
uint8_t distributionSystem;
|
||||
uint8_t mainFuse;
|
||||
uint8_t productionCapacity;
|
||||
uint8_t encryptionKey[16];
|
||||
uint8_t authenticationKey[16];
|
||||
}; // 41
|
||||
|
||||
struct MeterConfig87 {
|
||||
uint8_t type;
|
||||
uint8_t distributionSystem;
|
||||
uint8_t mainFuse;
|
||||
uint8_t productionCapacity;
|
||||
uint8_t encryptionKey[16];
|
||||
uint8_t authenticationKey[16];
|
||||
bool substituteMissing;
|
||||
}; // 37
|
||||
|
||||
struct DebugConfig {
|
||||
bool telnet;
|
||||
bool serial;
|
||||
uint8_t level;
|
||||
}; // 3
|
||||
|
||||
struct GpioConfig {
|
||||
uint8_t hanPin;
|
||||
uint8_t apPin;
|
||||
uint8_t ledPin;
|
||||
bool ledInverted;
|
||||
uint8_t ledPinRed;
|
||||
uint8_t ledPinGreen;
|
||||
uint8_t ledPinBlue;
|
||||
bool ledRgbInverted;
|
||||
uint8_t tempSensorPin;
|
||||
uint8_t tempAnalogSensorPin;
|
||||
uint8_t vccPin;
|
||||
int16_t vccOffset;
|
||||
uint16_t vccMultiplier;
|
||||
uint8_t vccBootLimit;
|
||||
uint16_t vccResistorGnd;
|
||||
uint16_t vccResistorVcc;
|
||||
}; // 20
|
||||
|
||||
struct DomoticzConfig {
|
||||
uint16_t elidx;
|
||||
uint16_t vl1idx;
|
||||
uint16_t vl2idx;
|
||||
uint16_t vl3idx;
|
||||
uint16_t cl1idx;
|
||||
}; // 10
|
||||
|
||||
struct NtpConfig {
|
||||
bool enable;
|
||||
bool dhcp;
|
||||
int16_t offset;
|
||||
int16_t summerOffset;
|
||||
char server[64];
|
||||
}; // 70
|
||||
|
||||
struct EntsoeConfig {
|
||||
char token[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint32_t multiplier;
|
||||
}; // 62
|
||||
|
||||
struct ConfigObject83 {
|
||||
uint8_t boardType;
|
||||
char wifiSsid[32];
|
||||
char wifiPassword[64];
|
||||
char wifiIp[15];
|
||||
char wifiGw[15];
|
||||
char wifiSubnet[15];
|
||||
char wifiDns1[15];
|
||||
char wifiDns2[15];
|
||||
char wifiHostname[32];
|
||||
char mqttHost[128];
|
||||
uint16_t mqttPort;
|
||||
char mqttClientId[32];
|
||||
char mqttPublishTopic[64];
|
||||
char mqttSubscribeTopic[64];
|
||||
char mqttUser[64];
|
||||
char mqttPassword[64];
|
||||
uint8_t mqttPayloadFormat;
|
||||
bool mqttSsl;
|
||||
uint8_t authSecurity;
|
||||
char authUser[64];
|
||||
char authPassword[64];
|
||||
|
||||
uint8_t meterType;
|
||||
uint8_t distributionSystem;
|
||||
uint8_t mainFuse;
|
||||
uint8_t productionCapacity;
|
||||
uint8_t meterEncryptionKey[16];
|
||||
uint8_t meterAuthenticationKey[16];
|
||||
bool substituteMissing;
|
||||
bool sendUnknown;
|
||||
|
||||
bool debugTelnet;
|
||||
bool debugSerial;
|
||||
uint8_t debugLevel;
|
||||
|
||||
uint8_t hanPin;
|
||||
uint8_t apPin;
|
||||
uint8_t ledPin;
|
||||
bool ledInverted;
|
||||
uint8_t ledPinRed;
|
||||
uint8_t ledPinGreen;
|
||||
uint8_t ledPinBlue;
|
||||
bool ledRgbInverted;
|
||||
uint8_t tempSensorPin;
|
||||
uint8_t vccPin;
|
||||
int16_t vccOffset;
|
||||
uint16_t vccMultiplier;
|
||||
uint8_t vccBootLimit;
|
||||
|
||||
uint16_t domoELIDX;
|
||||
uint16_t domoVL1IDX;
|
||||
uint16_t domoVL2IDX;
|
||||
uint16_t domoVL3IDX;
|
||||
uint16_t domoCL1IDX;
|
||||
|
||||
bool mDnsEnable;
|
||||
bool ntpEnable;
|
||||
bool ntpDhcp;
|
||||
int16_t ntpOffset;
|
||||
int16_t ntpSummerOffset;
|
||||
char ntpServer[64];
|
||||
|
||||
uint8_t tempAnalogSensorPin;
|
||||
};
|
||||
|
||||
struct TempSensorConfig {
|
||||
uint8_t address[8];
|
||||
char name[16];
|
||||
bool common;
|
||||
};
|
||||
|
||||
class AmsConfiguration {
|
||||
public:
|
||||
bool hasConfig();
|
||||
bool load();
|
||||
bool save();
|
||||
|
||||
String getWifiSsid();
|
||||
void setWifiSsid(String wifiSsid);
|
||||
String getWifiPassword();
|
||||
void setWifiPassword(String wifiPassword);
|
||||
String getWifiIp();
|
||||
void setWifiIp(String wifiIp);
|
||||
String getWifiGw();
|
||||
void setWifiGw(String wifiGw);
|
||||
String getWifiSubnet();
|
||||
void setWifiSubnet(String wifiSubnet);
|
||||
void clearWifiIp();
|
||||
int getConfigVersion();
|
||||
|
||||
bool save();
|
||||
|
||||
bool getSystemConfig(SystemConfig&);
|
||||
bool setSystemConfig(SystemConfig&);
|
||||
|
||||
bool getWiFiConfig(WiFiConfig&);
|
||||
bool setWiFiConfig(WiFiConfig&);
|
||||
void clearWifi(WiFiConfig&);
|
||||
void clearWifiIp(WiFiConfig&);
|
||||
bool isWifiChanged();
|
||||
void ackWifiChange();
|
||||
|
||||
String getMqttHost();
|
||||
void setMqttHost(String mqttHost);
|
||||
int getMqttPort();
|
||||
void setMqttPort(int mqttPort);
|
||||
String getMqttClientId();
|
||||
void setMqttClientId(String mqttClientId);
|
||||
String getMqttPublishTopic();
|
||||
void setMqttPublishTopic(String mqttPublishTopic);
|
||||
String getMqttSubscribeTopic();
|
||||
void setMqttSubscribeTopic(String mqttSubscribeTopic);
|
||||
String getMqttUser();
|
||||
void setMqttUser(String mqttUser);
|
||||
String getMqttPassword();
|
||||
void setMqttPassword(String mqttPassword);
|
||||
void clearMqtt();
|
||||
|
||||
bool getMqttConfig(MqttConfig&);
|
||||
bool setMqttConfig(MqttConfig&);
|
||||
void clearMqtt(MqttConfig&);
|
||||
void setMqttChanged();
|
||||
bool isMqttChanged();
|
||||
void ackMqttChange();
|
||||
|
||||
byte getAuthSecurity();
|
||||
void setAuthSecurity(byte authSecurity);
|
||||
String getAuthUser();
|
||||
void setAuthUser(String authUser);
|
||||
String getAuthPassword();
|
||||
void setAuthPassword(String authPassword);
|
||||
void clearAuth();
|
||||
bool getWebConfig(WebConfig&);
|
||||
bool setWebConfig(WebConfig&);
|
||||
void clearAuth(WebConfig&);
|
||||
|
||||
int getMeterType();
|
||||
void setMeterType(int meterType);
|
||||
int getDistributionSystem();
|
||||
void setDistributionSystem(int distributionSystem);
|
||||
int getMainFuse();
|
||||
void setMainFuse(int mainFuse);
|
||||
int getProductionCapacity();
|
||||
void setProductionCapacity(int productionCapacity);
|
||||
bool getMeterConfig(MeterConfig&);
|
||||
bool setMeterConfig(MeterConfig&);
|
||||
void clearMeter(MeterConfig&);
|
||||
bool isMeterChanged();
|
||||
void ackMeterChanged();
|
||||
|
||||
void print(Stream* debugger);
|
||||
bool getDebugConfig(DebugConfig&);
|
||||
bool setDebugConfig(DebugConfig&);
|
||||
void clearDebug(DebugConfig&);
|
||||
|
||||
bool pinUsed(uint8_t, GpioConfig&);
|
||||
|
||||
bool getGpioConfig(GpioConfig&);
|
||||
bool setGpioConfig(GpioConfig&);
|
||||
void clearGpio(GpioConfig&);
|
||||
|
||||
void print(Print* debugger);
|
||||
|
||||
bool getDomoticzConfig(DomoticzConfig&);
|
||||
bool setDomoticzConfig(DomoticzConfig&);
|
||||
void clearDomo(DomoticzConfig&);
|
||||
bool isDomoChanged();
|
||||
void ackDomoChange();
|
||||
|
||||
bool getNtpConfig(NtpConfig&);
|
||||
bool setNtpConfig(NtpConfig&);
|
||||
void clearNtp(NtpConfig&);
|
||||
bool isNtpChanged();
|
||||
void ackNtpChange();
|
||||
|
||||
bool getEntsoeConfig(EntsoeConfig&);
|
||||
bool setEntsoeConfig(EntsoeConfig&);
|
||||
void clearEntsoe(EntsoeConfig&);
|
||||
bool isEntsoeChanged();
|
||||
void ackEntsoeChange();
|
||||
|
||||
void loadTempSensors();
|
||||
void saveTempSensors();
|
||||
uint8_t getTempSensorCount();
|
||||
TempSensorConfig* getTempSensorConfig(uint8_t address[8]);
|
||||
void updateTempSensorConfig(uint8_t address[8], const char name[32], bool common);
|
||||
|
||||
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
|
||||
|
||||
void clear();
|
||||
|
||||
protected:
|
||||
|
||||
private:
|
||||
String wifiSsid;
|
||||
String wifiPassword;
|
||||
String wifiIp;
|
||||
String wifiGw;
|
||||
String wifiSubnet;
|
||||
bool wifiChanged;
|
||||
|
||||
String mqttHost;
|
||||
int mqttPort;
|
||||
String mqttClientId;
|
||||
String mqttPublishTopic;
|
||||
String mqttSubscribeTopic;
|
||||
String mqttUser;
|
||||
String mqttPassword;
|
||||
bool mqttChanged;
|
||||
uint8_t configVersion = 0;
|
||||
|
||||
byte authSecurity;
|
||||
String authUser;
|
||||
String authPassword;
|
||||
bool wifiChanged, mqttChanged, meterChanged = true, domoChanged, ntpChanged = true, entsoeChanged = false;
|
||||
|
||||
int meterType, distributionSystem, mainFuse, productionCapacity;
|
||||
uint8_t tempSensorCount = 0;
|
||||
TempSensorConfig** tempSensors = NULL;
|
||||
|
||||
const int EEPROM_SIZE = 512;
|
||||
const int EEPROM_CHECK_SUM = 80; // Used to check if config is stored. Change if structure changes
|
||||
const int EEPROM_CONFIG_ADDRESS = 0;
|
||||
bool loadConfig83(int address);
|
||||
bool relocateConfig86();
|
||||
bool relocateConfig87();
|
||||
bool relocateConfig90(); // 2.0.0
|
||||
bool relocateConfig91(); // 2.0.2
|
||||
|
||||
bool loadConfig72(int address);
|
||||
bool loadConfig75(int address);
|
||||
bool loadConfig80(int address);
|
||||
|
||||
int saveString(int pAddress, const char* pString);
|
||||
int readString(int pAddress, char* pString[]);
|
||||
int saveInt(int pAddress, int pValue);
|
||||
int readInt(int pAddress, int *pValue);
|
||||
int saveBool(int pAddress, bool pValue);
|
||||
int readBool(int pAddress, bool *pValue);
|
||||
int saveByte(int pAddress, byte pValue);
|
||||
int readByte(int pAddress, byte *pValue);
|
||||
|
||||
template <class T> int writeAnything(int ee, const T& value);
|
||||
template <class T> int readAnything(int ee, T& value);
|
||||
};
|
||||
#endif
|
||||
|
||||
331
src/AmsData.cpp
331
src/AmsData.cpp
@@ -1,251 +1,61 @@
|
||||
#include "AmsData.h"
|
||||
#include "Kaifa.h"
|
||||
#include "Aidon.h"
|
||||
#include "Kamstrup.h"
|
||||
|
||||
AmsData::AmsData() {}
|
||||
|
||||
AmsData::AmsData(int meterType, HanReader& hanReader) {
|
||||
lastUpdateMillis = millis();
|
||||
packageTimestamp = hanReader.getPackageTime();
|
||||
|
||||
int listSize = hanReader.getListSize();
|
||||
switch(meterType) {
|
||||
case METER_TYPE_KAIFA:
|
||||
extractFromKaifa(hanReader, listSize);
|
||||
break;
|
||||
case METER_TYPE_AIDON:
|
||||
extractFromAidon(hanReader, listSize);
|
||||
break;
|
||||
case METER_TYPE_KAMSTRUP:
|
||||
extractFromKamstrup(hanReader, listSize);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromKaifa(HanReader& hanReader, int listSize) {
|
||||
switch(listSize) {
|
||||
case (int)Kaifa::List1:
|
||||
listType = 1;
|
||||
break;
|
||||
case (int)Kaifa::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (int)Kaifa::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (int)Kaifa::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (int)Kaifa::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
if(listSize == (int)Kaifa::List1) {
|
||||
activeImportPower = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
|
||||
} else {
|
||||
switch(listSize) {
|
||||
case (int)Kaifa::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock);
|
||||
activeImportCounter = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeActiveImportEnergy);
|
||||
activeExportCounter = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeActiveExportEnergy);
|
||||
reactiveImportCounter = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeReactiveImportEnergy);
|
||||
reactiveExportCounter = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeReactiveExportEnergy);
|
||||
case (int)Kaifa::List3PhaseShort:
|
||||
listId = hanReader.getString( (int)Kaifa_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kaifa_List3Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Kaifa_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Kaifa_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Kaifa_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL1)) / 1000;
|
||||
l2current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL2)) / 1000;
|
||||
l3current = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL3)) / 1000;
|
||||
l1voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL1)) / 10;
|
||||
l2voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL2)) / 10;
|
||||
l3voltage = ((double) hanReader.getInt((int)Kaifa_List3Phase::VoltageL3)) / 10;
|
||||
break;
|
||||
case (int)Kaifa::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock);
|
||||
activeImportCounter = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeActiveImportEnergy);
|
||||
activeExportCounter = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeActiveExportEnergy);
|
||||
reactiveImportCounter = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeReactiveImportEnergy);
|
||||
reactiveExportCounter = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeReactiveExportEnergy);
|
||||
case (int)Kaifa::List1PhaseShort:
|
||||
listId = hanReader.getString( (int)Kaifa_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kaifa_List1Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Kaifa_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Kaifa_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Kaifa_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt((int)Kaifa_List1Phase::CurrentL1)) / 1000;
|
||||
l1voltage = ((double) hanReader.getInt((int)Kaifa_List1Phase::VoltageL1)) / 10;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromAidon(HanReader& hanReader, int listSize) {
|
||||
switch(listSize) {
|
||||
case (int)Aidon::List1:
|
||||
listType = 1;
|
||||
break;
|
||||
case (int)Aidon::List3PhaseITShort:
|
||||
case (int)Aidon::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (int)Aidon::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (int)Aidon::List3PhaseITLong:
|
||||
case (int)Aidon::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (int)Aidon::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
if(listSize == (int)Aidon::List1) {
|
||||
activeImportPower = hanReader.getInt((int)Aidon_List1::ActiveImportPower);
|
||||
} else {
|
||||
switch(listSize) {
|
||||
case (int)Aidon::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Aidon_List3Phase::Timestamp);
|
||||
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (int)Aidon::List3PhaseShort:
|
||||
listId = hanReader.getString( (int)Aidon_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Aidon_List3Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Aidon_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Aidon_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Aidon_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Aidon_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Aidon_List3Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL1)) / 10;
|
||||
l2current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL2)) / 10;
|
||||
l3current = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL3)) / 10;
|
||||
l1voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL1)) / 10;
|
||||
l2voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL2)) / 10;
|
||||
l3voltage = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL3)) / 10;
|
||||
break;
|
||||
case (int)Aidon::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Aidon_List1Phase::Timestamp);
|
||||
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (int)Aidon::List1PhaseShort:
|
||||
listId = hanReader.getString( (int)Aidon_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Aidon_List1Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Aidon_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Aidon_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Aidon_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Aidon_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Aidon_List1Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt( (int)Aidon_List1Phase::CurrentL1)) / 10;
|
||||
l1voltage = ((double) hanReader.getInt( (int)Aidon_List1Phase::VoltageL1)) / 10;
|
||||
break;
|
||||
case (int)Aidon::List3PhaseITLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Aidon_List3PhaseIT::Timestamp);
|
||||
activeImportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (int)Aidon::List3PhaseITShort:
|
||||
listId = hanReader.getString( (int)Aidon_List3PhaseIT::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Aidon_List3PhaseIT::MeterID);
|
||||
meterType = hanReader.getString( (int)Aidon_List3PhaseIT::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL1)) / 10;
|
||||
l2current = 0;
|
||||
l3current = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL3)) / 10;
|
||||
l1voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL1)) / 10;
|
||||
l2voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL2)) / 10;
|
||||
l3voltage = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL3)) / 10;
|
||||
//l2current = ((activeImportPower * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
|
||||
threePhase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromKamstrup(HanReader& hanReader, int listSize) {
|
||||
switch(listSize) {
|
||||
case (int)Kamstrup::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (int)Kamstrup::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (int)Kamstrup::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (int)Kamstrup::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(listSize) {
|
||||
case (int)Kamstrup::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock);
|
||||
activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (int)Kamstrup::List3PhaseShort:
|
||||
listId = hanReader.getString( (int)Kamstrup_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kamstrup_List3Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Kamstrup_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL1)) / 100;
|
||||
l2current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL2)) / 100;
|
||||
l3current = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL3)) / 100;
|
||||
l1voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL1);
|
||||
l2voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL2);
|
||||
l3voltage = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL3);
|
||||
break;
|
||||
case (int)Kamstrup::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kamstrup_List1Phase::MeterClock);
|
||||
activeImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (int)Kamstrup::List1PhaseShort:
|
||||
listId = hanReader.getString( (int)Kamstrup_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kamstrup_List1Phase::MeterID);
|
||||
meterType = hanReader.getString( (int)Kamstrup_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveExportPower);
|
||||
l1current = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CurrentL1)) / 100;
|
||||
l1voltage = hanReader.getInt( (int)Kamstrup_List1Phase::VoltageL1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::apply(AmsData& other) {
|
||||
if(other.getListType() < 3) {
|
||||
unsigned long ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastUpdateMillis;
|
||||
|
||||
if(ms > 0) {
|
||||
if(other.getActiveImportPower() > 0) {
|
||||
float add = other.getActiveImportPower() * (((float) ms) / 3600000.0);
|
||||
activeImportCounter += add / 1000.0;
|
||||
//Serial.printf("%dW, %dms, %.6fkWh added\n", other.getActiveImportPower(), ms, add);
|
||||
}
|
||||
|
||||
if(other.getListType() > 1) {
|
||||
ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastList2;
|
||||
if(other.getActiveExportPower() > 0) {
|
||||
float add = other.getActiveExportPower() * (((float) ms) / 3600000.0);
|
||||
activeExportCounter += add / 1000.0;
|
||||
}
|
||||
if(other.getReactiveImportPower() > 0) {
|
||||
float add = other.getReactiveImportPower() * (((float) ms) / 3600000.0);
|
||||
reactiveImportCounter += add / 1000.0;
|
||||
}
|
||||
if(other.getReactiveExportPower() > 0) {
|
||||
float add = other.getReactiveExportPower() * (((float) ms) / 3600000.0);
|
||||
reactiveExportCounter += add / 1000.0;
|
||||
}
|
||||
}
|
||||
counterEstimated = true;
|
||||
}
|
||||
}
|
||||
|
||||
this->lastUpdateMillis = other.getLastUpdateMillis();
|
||||
if(other.getListType() > 1) {
|
||||
this->lastList2 = this->lastUpdateMillis;
|
||||
}
|
||||
this->packageTimestamp = other.getPackageTimestamp();
|
||||
this->listType = max(this->listType, other.getListType());
|
||||
if(other.getListType() > this->listType)
|
||||
this->listType = other.getListType();
|
||||
switch(other.getListType()) {
|
||||
case 3:
|
||||
this->powerFactor = other.getPowerFactor();
|
||||
this->l1PowerFactor = other.getL1PowerFactor();
|
||||
this->l2PowerFactor = other.getL2PowerFactor();
|
||||
this->l3PowerFactor = other.getL3PowerFactor();
|
||||
this->meterTimestamp = other.getMeterTimestamp();
|
||||
this->activeImportCounter = other.getActiveImportCounter();
|
||||
this->activeExportCounter = other.getActiveExportCounter();
|
||||
this->reactiveImportCounter = other.getReactiveImportCounter();
|
||||
this->reactiveExportCounter = other.getReactiveExportCounter();
|
||||
this->counterEstimated = false;
|
||||
case 2:
|
||||
this->listId = other.getListId();
|
||||
this->meterId = other.getMeterId();
|
||||
this->meterType = other.getMeterType();
|
||||
this->meterModel = other.getMeterModel();
|
||||
this->reactiveImportPower = other.getReactiveImportPower();
|
||||
this->activeExportPower = other.getActiveExportPower();
|
||||
this->reactiveExportPower = other.getReactiveExportPower();
|
||||
@@ -256,6 +66,7 @@ void AmsData::apply(AmsData& other) {
|
||||
this->l2voltage = other.getL2Voltage();
|
||||
this->l3voltage = other.getL3Voltage();
|
||||
this->threePhase = other.isThreePhase();
|
||||
this->twoPhase = other.isTwoPhase();
|
||||
case 1:
|
||||
this->activeImportPower = other.getActiveImportPower();
|
||||
}
|
||||
@@ -265,11 +76,11 @@ unsigned long AmsData::getLastUpdateMillis() {
|
||||
return this->lastUpdateMillis;
|
||||
}
|
||||
|
||||
unsigned long AmsData::getPackageTimestamp() {
|
||||
time_t AmsData::getPackageTimestamp() {
|
||||
return this->packageTimestamp;
|
||||
}
|
||||
|
||||
int AmsData::getListType() {
|
||||
uint8_t AmsData::getListType() {
|
||||
return this->listType;
|
||||
}
|
||||
|
||||
@@ -281,70 +92,94 @@ String AmsData::getMeterId() {
|
||||
return this->meterId;
|
||||
}
|
||||
|
||||
String AmsData::getMeterType() {
|
||||
uint8_t AmsData::getMeterType() {
|
||||
return this->meterType;
|
||||
}
|
||||
|
||||
unsigned long AmsData::getMeterTimestamp() {
|
||||
String AmsData::getMeterModel() {
|
||||
return this->meterModel;
|
||||
}
|
||||
|
||||
time_t AmsData::getMeterTimestamp() {
|
||||
return this->meterTimestamp;
|
||||
}
|
||||
|
||||
int AmsData::getActiveImportPower() {
|
||||
uint16_t AmsData::getActiveImportPower() {
|
||||
return this->activeImportPower;
|
||||
}
|
||||
|
||||
int AmsData::getReactiveImportPower() {
|
||||
uint16_t AmsData::getReactiveImportPower() {
|
||||
return this->reactiveImportPower;
|
||||
}
|
||||
|
||||
int AmsData::getActiveExportPower() {
|
||||
uint16_t AmsData::getActiveExportPower() {
|
||||
return this->activeExportPower;
|
||||
}
|
||||
|
||||
int AmsData::getReactiveExportPower() {
|
||||
uint16_t AmsData::getReactiveExportPower() {
|
||||
return this->reactiveExportPower;
|
||||
}
|
||||
|
||||
double AmsData::getL1Voltage() {
|
||||
float AmsData::getL1Voltage() {
|
||||
return this->l1voltage;
|
||||
}
|
||||
|
||||
double AmsData::getL2Voltage() {
|
||||
float AmsData::getL2Voltage() {
|
||||
return this->l2voltage;
|
||||
}
|
||||
|
||||
double AmsData::getL3Voltage() {
|
||||
float AmsData::getL3Voltage() {
|
||||
return this->l3voltage;
|
||||
}
|
||||
|
||||
double AmsData::getL1Current() {
|
||||
float AmsData::getL1Current() {
|
||||
return this->l1current;
|
||||
}
|
||||
|
||||
double AmsData::getL2Current() {
|
||||
float AmsData::getL2Current() {
|
||||
return this->l2current;
|
||||
}
|
||||
|
||||
double AmsData::getL3Current() {
|
||||
float AmsData::getL3Current() {
|
||||
return this->l3current;
|
||||
}
|
||||
|
||||
double AmsData::getActiveImportCounter() {
|
||||
float AmsData::getPowerFactor() {
|
||||
return this->powerFactor;
|
||||
}
|
||||
|
||||
float AmsData::getL1PowerFactor() {
|
||||
return this->l1PowerFactor;
|
||||
}
|
||||
|
||||
float AmsData::getL2PowerFactor() {
|
||||
return this->l2PowerFactor;
|
||||
}
|
||||
|
||||
float AmsData::getL3PowerFactor() {
|
||||
return this->l3PowerFactor;
|
||||
}
|
||||
|
||||
float AmsData::getActiveImportCounter() {
|
||||
return this->activeImportCounter;
|
||||
}
|
||||
|
||||
double AmsData::getReactiveImportCounter() {
|
||||
float AmsData::getReactiveImportCounter() {
|
||||
return this->reactiveImportCounter;
|
||||
}
|
||||
|
||||
double AmsData::getActiveExportCounter() {
|
||||
float AmsData::getActiveExportCounter() {
|
||||
return this->activeExportCounter;
|
||||
}
|
||||
|
||||
double AmsData::getReactiveExportCounter() {
|
||||
float AmsData::getReactiveExportCounter() {
|
||||
return this->reactiveExportCounter;
|
||||
}
|
||||
|
||||
bool AmsData::isThreePhase() {
|
||||
return this->threePhase;
|
||||
}
|
||||
|
||||
bool AmsData::isTwoPhase() {
|
||||
return this->twoPhase;
|
||||
}
|
||||
|
||||
@@ -2,65 +2,77 @@
|
||||
#define _AMSDATA_H
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "HanReader.h"
|
||||
#include <Timezone.h>
|
||||
|
||||
#define METER_TYPE_KAIFA 1
|
||||
#define METER_TYPE_AIDON 2
|
||||
#define METER_TYPE_KAMSTRUP 3
|
||||
enum AmsType {
|
||||
AmsTypeAutodetect = 0x00,
|
||||
AmsTypeAidon = 0x01,
|
||||
AmsTypeKaifa = 0x02,
|
||||
AmsTypeKamstrup = 0x03,
|
||||
AmsTypeIskra = 0x08,
|
||||
AmsTypeLandis = 0x09,
|
||||
AmsTypeSagemcom = 0x0A,
|
||||
AmsTypeCustom = 0x88,
|
||||
AmsTypeUnknown = 0xFF
|
||||
};
|
||||
|
||||
class AmsData {
|
||||
public:
|
||||
AmsData();
|
||||
AmsData(int meterType, HanReader& hanReader);
|
||||
|
||||
void apply(AmsData& other);
|
||||
|
||||
unsigned long getLastUpdateMillis();
|
||||
|
||||
unsigned long getPackageTimestamp();
|
||||
time_t getPackageTimestamp();
|
||||
|
||||
int getListType();
|
||||
uint8_t getListType();
|
||||
|
||||
String getListId();
|
||||
String getMeterId();
|
||||
String getMeterType();
|
||||
uint8_t getMeterType();
|
||||
String getMeterModel();
|
||||
|
||||
unsigned long getMeterTimestamp();
|
||||
time_t getMeterTimestamp();
|
||||
|
||||
int getActiveImportPower();
|
||||
int getReactiveImportPower();
|
||||
int getActiveExportPower();
|
||||
int getReactiveExportPower();
|
||||
uint16_t getActiveImportPower();
|
||||
uint16_t getReactiveImportPower();
|
||||
uint16_t getActiveExportPower();
|
||||
uint16_t getReactiveExportPower();
|
||||
|
||||
double getL1Voltage();
|
||||
double getL2Voltage();
|
||||
double getL3Voltage();
|
||||
float getL1Voltage();
|
||||
float getL2Voltage();
|
||||
float getL3Voltage();
|
||||
|
||||
double getL1Current();
|
||||
double getL2Current();
|
||||
double getL3Current();
|
||||
float getL1Current();
|
||||
float getL2Current();
|
||||
float getL3Current();
|
||||
|
||||
double getActiveImportCounter();
|
||||
double getReactiveImportCounter();
|
||||
double getActiveExportCounter();
|
||||
double getReactiveExportCounter();
|
||||
float getPowerFactor();
|
||||
float getL1PowerFactor();
|
||||
float getL2PowerFactor();
|
||||
float getL3PowerFactor();
|
||||
|
||||
float getActiveImportCounter();
|
||||
float getReactiveImportCounter();
|
||||
float getActiveExportCounter();
|
||||
float getReactiveExportCounter();
|
||||
|
||||
bool isThreePhase();
|
||||
bool isTwoPhase();
|
||||
|
||||
private:
|
||||
protected:
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
int listType = 0;
|
||||
unsigned long packageTimestamp = 0;
|
||||
String listId, meterId, meterType;
|
||||
unsigned long meterTimestamp = 0;
|
||||
int activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
|
||||
double l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
|
||||
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
|
||||
bool threePhase = false;
|
||||
|
||||
void extractFromKaifa(HanReader& hanReader, int listSize);
|
||||
void extractFromAidon(HanReader& hanReader, int listSize);
|
||||
void extractFromKamstrup(HanReader& hanReader, int listSize);
|
||||
unsigned long lastList2 = 0;
|
||||
uint8_t listType = 0, meterType = AmsTypeUnknown;
|
||||
time_t packageTimestamp = 0;
|
||||
String listId, meterId, meterModel;
|
||||
time_t meterTimestamp = 0;
|
||||
uint16_t activeImportPower = 0, reactiveImportPower = 0, activeExportPower = 0, reactiveExportPower = 0;
|
||||
float l1voltage = 0, l2voltage = 0, l3voltage = 0, l1current = 0, l2current = 0, l3current = 0;
|
||||
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
|
||||
float activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
|
||||
bool threePhase = false, twoPhase = false, counterEstimated = false;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
343
src/AmsDataStorage.cpp
Normal file
343
src/AmsDataStorage.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
#include "AmsDataStorage.h"
|
||||
#include <lwip/apps/sntp.h>
|
||||
#include "EEPROM.h"
|
||||
#include "LittleFS.h"
|
||||
#include "AmsStorage.h"
|
||||
|
||||
AmsDataStorage::AmsDataStorage(RemoteDebug* debugger) {
|
||||
day.version = 3;
|
||||
month.version = 4;
|
||||
this->debugger = debugger;
|
||||
}
|
||||
|
||||
void AmsDataStorage::setTimezone(Timezone* tz) {
|
||||
this->tz = tz;
|
||||
}
|
||||
|
||||
bool AmsDataStorage::update(AmsData* data) {
|
||||
time_t now = time(nullptr);
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) {
|
||||
debugger->printf("(AmsDataStorage) Time is: %d\n", now);
|
||||
}
|
||||
if(now < EPOCH_2021_01_01) {
|
||||
if(data->getMeterTimestamp() > 0) {
|
||||
now = data->getMeterTimestamp();
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Using meter timestamp, which is: %d\n", now);
|
||||
}
|
||||
} else if(data->getPackageTimestamp() > 0) {
|
||||
now = data->getPackageTimestamp();
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Using package timestamp, which is: %d\n", now);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(now < EPOCH_2021_01_01) {
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) {
|
||||
debugger->printf("(AmsDataStorage) Invalid time: %d\n", now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(now-day.lastMeterReadTime < 3595) {
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) {
|
||||
debugger->printf("(AmsDataStorage) It is only %d seconds since last update, ignoring\n", (now-day.lastMeterReadTime));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
tmElements_t tm, last;
|
||||
breakTime(now, tm);
|
||||
|
||||
if(day.lastMeterReadTime > EPOCH_2021_01_01) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Last day update: %d\n", day.lastMeterReadTime);
|
||||
}
|
||||
breakTime(day.lastMeterReadTime, last);
|
||||
for(int i = last.Hour; i < tm.Hour; i++) {
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) {
|
||||
debugger->printf("(AmsDataStorage) Clearing hour: %d\n", i);
|
||||
}
|
||||
setHour(i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(month.lastMeterReadTime > EPOCH_2021_01_01) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Last month update: %d\n", month.lastMeterReadTime);
|
||||
}
|
||||
if(tz != NULL) {
|
||||
breakTime(tz->toLocal(now), tm);
|
||||
breakTime(tz->toLocal(month.lastMeterReadTime), last);
|
||||
} else {
|
||||
breakTime(now, tm);
|
||||
breakTime(month.lastMeterReadTime, last);
|
||||
}
|
||||
|
||||
for(int i = last.Day; i < tm.Day; i++) {
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) {
|
||||
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
|
||||
}
|
||||
setDay(i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if(day.lastMeterReadTime > now) {
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) {
|
||||
debugger->printf("(AmsDataStorage) Invalid future timestamp for day plot, resetting\n");
|
||||
}
|
||||
day.activeImport = data->getActiveImportCounter() * 1000;
|
||||
day.activeExport = data->getActiveExportCounter() * 1000;
|
||||
day.lastMeterReadTime = now;
|
||||
}
|
||||
|
||||
if(data->getListType() != 3) return false;
|
||||
else if(tm.Minute > 5) return false;
|
||||
|
||||
// Update day plot
|
||||
if(day.activeImport == 0 || now - day.lastMeterReadTime > 86400) {
|
||||
day.activeImport = data->getActiveImportCounter() * 1000;
|
||||
day.activeExport = data->getActiveExportCounter() * 1000;
|
||||
day.lastMeterReadTime = now;
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) {
|
||||
debugger->printf("(AmsDataStorage) Too long since last day update, clearing data\n");
|
||||
}
|
||||
for(int i = 0; i<24; i++) {
|
||||
setHour(i, 0);
|
||||
}
|
||||
} else if(now - day.lastMeterReadTime < 4000) {
|
||||
breakTime(now - 3600, tm);
|
||||
int16_t val = (((data->getActiveImportCounter() * 1000) - day.activeImport) - ((data->getActiveExportCounter() * 1000) - day.activeExport));
|
||||
setHour(tm.Hour, val);
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) {
|
||||
debugger->printf("(AmsDataStorage) Usage for hour %d: %d\n", tm.Hour, val);
|
||||
}
|
||||
|
||||
day.activeImport = data->getActiveImportCounter() * 1000;
|
||||
day.activeExport = data->getActiveExportCounter() * 1000;
|
||||
day.lastMeterReadTime = now;
|
||||
} else {
|
||||
float mins = (now - day.lastMeterReadTime) / 60.0;
|
||||
uint16_t im = ((data->getActiveImportCounter() * 1000) - day.activeImport);
|
||||
uint16_t ex = ((data->getActiveExportCounter() * 1000) - day.activeExport);
|
||||
float ipm = im / mins;
|
||||
float epm = ex / mins;
|
||||
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Since last day update, minutes: %.1f, import: %d (%.2f/min), export: %d (%.2f/min)\n", mins, im, ipm, ex, epm);
|
||||
}
|
||||
|
||||
breakTime(day.lastMeterReadTime, tm);
|
||||
day.lastMeterReadTime = day.lastMeterReadTime - (tm.Minute * 60) - tm.Second;
|
||||
breakTime(now, tm);
|
||||
time_t stopAt = now - (tm.Minute * 60) - tm.Second;
|
||||
while(day.lastMeterReadTime < stopAt) {
|
||||
time_t cur = min(day.lastMeterReadTime + 3600, stopAt);
|
||||
uint8_t minutes = round((cur - day.lastMeterReadTime) / 60.0);
|
||||
if(minutes < 1) break;
|
||||
|
||||
breakTime(day.lastMeterReadTime, last);
|
||||
float val = ((ipm * minutes) - (epm * minutes));
|
||||
setHour(last.Hour, val);
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) {
|
||||
debugger->printf("(AmsDataStorage) Estimated usage for hour %u: %.1f (%lu)\n", last.Hour, val, cur);
|
||||
}
|
||||
|
||||
day.activeImport += ipm * minutes;
|
||||
day.activeExport += epm * minutes;
|
||||
day.lastMeterReadTime = cur;
|
||||
}
|
||||
}
|
||||
|
||||
// Update month plot
|
||||
if(tz != NULL) {
|
||||
breakTime(tz->toLocal(now), tm);
|
||||
} else {
|
||||
breakTime(now, tm);
|
||||
}
|
||||
|
||||
if(month.lastMeterReadTime > now) {
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) {
|
||||
debugger->printf("(AmsDataStorage) Invalid future timestamp for month plot, resetting\n");
|
||||
}
|
||||
month.activeImport = data->getActiveImportCounter() * 1000;
|
||||
month.activeExport = data->getActiveExportCounter() * 1000;
|
||||
month.lastMeterReadTime = now;
|
||||
}
|
||||
|
||||
if(tm.Hour == 0 && now - month.lastMeterReadTime > 86300) {
|
||||
if(month.activeImport == 0 || now - month.lastMeterReadTime > 2678400) {
|
||||
month.activeImport = data->getActiveImportCounter() * 1000;
|
||||
month.activeExport = data->getActiveExportCounter() * 1000;
|
||||
month.lastMeterReadTime = now;
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) {
|
||||
debugger->printf("(AmsDataStorage) Too long since last month update, clearing data\n");
|
||||
}
|
||||
for(int i = 1; i<=31; i++) {
|
||||
setDay(i, 0);
|
||||
}
|
||||
} else if(now - month.lastMeterReadTime < 87000) {
|
||||
int32_t val = (month.activeImport == 0 ? 0 : ((data->getActiveImportCounter() * 1000) - month.activeImport) - ((data->getActiveExportCounter() * 1000) - month.activeExport));
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) {
|
||||
debugger->printf("(AmsDataStorage) Usage for day %d: %d\n", tm.Day, val);
|
||||
}
|
||||
|
||||
time_t yesterday = now - 3600;
|
||||
breakTime(yesterday, tm);
|
||||
setDay(tm.Day, val);
|
||||
|
||||
month.activeImport = data->getActiveImportCounter() * 1000;
|
||||
month.activeExport = data->getActiveExportCounter() * 1000;
|
||||
month.lastMeterReadTime = now;
|
||||
} else {
|
||||
float hrs = (now - month.lastMeterReadTime) / 3600.0;
|
||||
uint16_t im = ((data->getActiveImportCounter() * 1000) - month.activeImport);
|
||||
uint16_t ex = ((data->getActiveExportCounter() * 1000) - month.activeExport);
|
||||
float iph = im / hrs;
|
||||
float eph = ex / hrs;
|
||||
|
||||
// There is something wacky going on when it ends up here. The total value (im) is way way lower than it should be, which in
|
||||
// turn causes low values for all estimates. And then when it returns to the normal case above, the value is waaay higher.
|
||||
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Since last month update, hours: %.1f, import: %d (%.2f/hr), export: %d (%.2f/hr)\n", hrs, im, iph, ex, eph);
|
||||
}
|
||||
|
||||
// Make sure last month read is at midnight
|
||||
if(tz != NULL) {
|
||||
breakTime(tz->toLocal(month.lastMeterReadTime), tm);
|
||||
} else {
|
||||
breakTime(month.lastMeterReadTime, tm);
|
||||
}
|
||||
month.lastMeterReadTime = month.lastMeterReadTime - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Last month read after resetting to midnight: %lu\n", month.lastMeterReadTime);
|
||||
}
|
||||
|
||||
if(tz != NULL) {
|
||||
breakTime(tz->toLocal(now), tm);
|
||||
} else {
|
||||
breakTime(now, tm);
|
||||
}
|
||||
time_t stopAt = now - (tm.Hour * 3600) - (tm.Minute * 60) - tm.Second;
|
||||
while(month.lastMeterReadTime < stopAt) {
|
||||
time_t cur = min(month.lastMeterReadTime + 86400, stopAt);
|
||||
uint8_t hours = round((cur - month.lastMeterReadTime) / 3600.0);
|
||||
|
||||
if(tz != NULL) {
|
||||
breakTime(tz->toLocal(month.lastMeterReadTime), last);
|
||||
} else {
|
||||
breakTime(month.lastMeterReadTime, last);
|
||||
}
|
||||
|
||||
float val = ((iph * hours) - (eph * hours));
|
||||
setDay(last.Day, val);
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) {
|
||||
debugger->printf("(AmsDataStorage) Estimated usage for day %u: %.1f (%lu)\n", last.Day, val, cur);
|
||||
}
|
||||
|
||||
month.activeImport += iph * hours;
|
||||
month.activeExport += eph * hours;
|
||||
month.lastMeterReadTime = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AmsDataStorage::setHour(uint8_t hour, int32_t val) {
|
||||
if(hour < 0 || hour > 24) return;
|
||||
day.points[hour] = val / 10;
|
||||
}
|
||||
|
||||
int16_t AmsDataStorage::getHour(uint8_t hour) {
|
||||
if(hour < 0 || hour > 24) return 0;
|
||||
return day.points[hour] * 10;
|
||||
}
|
||||
|
||||
void AmsDataStorage::setDay(uint8_t day, int32_t val) {
|
||||
if(day < 1 || day > 31) return;
|
||||
month.points[day-1] = val / 10;
|
||||
}
|
||||
|
||||
int32_t AmsDataStorage::getDay(uint8_t day) {
|
||||
if(day < 1 || day > 31) return 0;
|
||||
return (month.points[day-1] * 10);
|
||||
}
|
||||
|
||||
bool AmsDataStorage::load() {
|
||||
if(!LittleFS.begin()) {
|
||||
if(debugger->isActive(RemoteDebug::ERROR)) {
|
||||
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
if(LittleFS.exists(FILE_DAYPLOT)) {
|
||||
File file = LittleFS.open(FILE_DAYPLOT, "r");
|
||||
char buf[file.size()];
|
||||
file.readBytes(buf, file.size());
|
||||
DayDataPoints* day = (DayDataPoints*) buf;
|
||||
file.close();
|
||||
|
||||
if(day->version == 3) {
|
||||
memcpy(&this->day, day, sizeof(this->day));
|
||||
ret = true;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(LittleFS.exists(FILE_MONTHPLOT)) {
|
||||
File file = LittleFS.open(FILE_MONTHPLOT, "r");
|
||||
char buf[file.size()];
|
||||
file.readBytes(buf, file.size());
|
||||
MonthDataPoints* month = (MonthDataPoints*) buf;
|
||||
file.close();
|
||||
|
||||
if(month->version == 4) {
|
||||
memcpy(&this->month, month, sizeof(this->month));
|
||||
ret = ret && true;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
}
|
||||
|
||||
LittleFS.end();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsDataStorage::save() {
|
||||
if(!LittleFS.begin()) {
|
||||
if(debugger->isActive(RemoteDebug::ERROR)) {
|
||||
debugger->printf("(AmsDataStorage) Unable to load LittleFS\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
{
|
||||
File file = LittleFS.open(FILE_DAYPLOT, "w");
|
||||
char buf[sizeof(day)];
|
||||
memcpy(buf, &day, sizeof(day));
|
||||
for(int i = 0; i < sizeof(day); i++) {
|
||||
file.write(buf[i]);
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
{
|
||||
File file = LittleFS.open(FILE_MONTHPLOT, "w");
|
||||
char buf[sizeof(month)];
|
||||
memcpy(buf, &month, sizeof(month));
|
||||
for(int i = 0; i < sizeof(month); i++) {
|
||||
file.write(buf[i]);
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
LittleFS.end();
|
||||
return true;
|
||||
}
|
||||
49
src/AmsDataStorage.h
Normal file
49
src/AmsDataStorage.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef _AMSDATASTORAGE_H
|
||||
#define _AMSDATASTORAGE_H
|
||||
#include "Arduino.h"
|
||||
#include "AmsData.h"
|
||||
#include "RemoteDebug.h"
|
||||
#include "Timezone.h"
|
||||
|
||||
#define EPOCH_2021_01_01 1609459200
|
||||
|
||||
struct DayDataPoints {
|
||||
uint8_t version;
|
||||
int16_t points[24];
|
||||
time_t lastMeterReadTime;
|
||||
uint32_t activeImport;
|
||||
uint32_t activeExport;
|
||||
}; // 37 bytes
|
||||
|
||||
struct MonthDataPoints {
|
||||
uint8_t version;
|
||||
int16_t points[31];
|
||||
time_t lastMeterReadTime;
|
||||
uint32_t activeImport;
|
||||
uint32_t activeExport;
|
||||
}; // 75 bytes
|
||||
|
||||
class AmsDataStorage {
|
||||
public:
|
||||
AmsDataStorage(RemoteDebug*);
|
||||
void setTimezone(Timezone*);
|
||||
bool update(AmsData*);
|
||||
int16_t getHour(uint8_t);
|
||||
int32_t getDay(uint8_t);
|
||||
bool load();
|
||||
bool save();
|
||||
|
||||
private:
|
||||
Timezone* tz;
|
||||
DayDataPoints day = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
MonthDataPoints month = {
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
RemoteDebug* debugger;
|
||||
void setHour(uint8_t, int32_t);
|
||||
void setDay(uint8_t, int32_t);
|
||||
};
|
||||
|
||||
#endif
|
||||
13
src/AmsStorage.h
Normal file
13
src/AmsStorage.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#ifndef _AMSSTORAGE_H
|
||||
#define _AMSSTORAGE_H
|
||||
|
||||
#define FILE_FIRMWARE "/firmware.bin"
|
||||
|
||||
#define FILE_MQTT_CA "/mqtt-ca.pem"
|
||||
#define FILE_MQTT_CERT "/mqtt-cert.pem"
|
||||
#define FILE_MQTT_KEY "/mqtt-key.pem"
|
||||
|
||||
#define FILE_DAYPLOT "/dayplot.bin"
|
||||
#define FILE_MONTHPLOT "/monthplot.bin"
|
||||
|
||||
#endif
|
||||
@@ -1,68 +1,24 @@
|
||||
#ifndef _AMSTOMQTTBRIDGE_H
|
||||
#define _AMSTOMQTTBRIDGE_H
|
||||
|
||||
#define WIFI_CONNECTION_TIMEOUT 30000;
|
||||
#define WIFI_CONNECTION_TIMEOUT 60000;
|
||||
|
||||
#define INVALID_BUTTON_PIN 0xFFFFFFFF
|
||||
|
||||
#define MAX_PEM_SIZE 4096
|
||||
|
||||
#include <SoftwareSerial.h>
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESP8266mDNS.h>
|
||||
#elif defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include "Update.h"
|
||||
#endif
|
||||
|
||||
// Build settings for custom hardware by Roar Fredriksen
|
||||
#if HW_ROARFRED
|
||||
#define LED_PIN 2 // The blue on-board LED of the ESP8266 custom AMS board
|
||||
#define LED_ACTIVE_HIGH 0
|
||||
#define AP_BUTTON_PIN 0
|
||||
|
||||
#if DEBUG_MODE
|
||||
#if SOFTWARE_SERIAL
|
||||
#include <SoftwareSerial.h>
|
||||
SoftwareSerial *hanSerial = new SoftwareSerial(3);
|
||||
#else
|
||||
HardwareSerial *hanSerial = &Serial;
|
||||
#endif
|
||||
#else
|
||||
HardwareSerial *hanSerial = &Serial;
|
||||
#endif
|
||||
|
||||
// Build settings for Wemos Lolin D32
|
||||
#elif defined(ARDUINO_LOLIN_D32)
|
||||
#define LED_PIN 5
|
||||
#define LED_ACTIVE_HIGH 0
|
||||
#define AP_BUTTON_PIN 4
|
||||
|
||||
HardwareSerial *hanSerial = &Serial2;
|
||||
|
||||
// Build settings for Wemos D1 mini
|
||||
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
|
||||
#define LED_PIN D4
|
||||
#define LED_ACTIVE_HIGH 0
|
||||
#define AP_BUTTON_PIN D2
|
||||
|
||||
#define SOFTWARE_SERIAL 1
|
||||
#include <SoftwareSerial.h>
|
||||
SoftwareSerial *hanSerial = new SoftwareSerial(D1);
|
||||
|
||||
// Build settings for Adafruit Feather ESP32
|
||||
#elif defined(ARDUINO_FEATHER_ESP32)
|
||||
#define LED_PIN LED_BUILTIN
|
||||
#define LED_ACTIVE_HIGH 1
|
||||
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
|
||||
|
||||
HardwareSerial *hanSerial = &Serial2;
|
||||
|
||||
// Default build settings
|
||||
#else
|
||||
#define LED_PIN 2
|
||||
#define LED_ACTIVE_HIGH 0
|
||||
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
|
||||
#define SOFTWARE_SERIAL 1
|
||||
#include <SoftwareSerial.h>
|
||||
SoftwareSerial *hanSerial = new SoftwareSerial(5);
|
||||
#endif
|
||||
#include "LittleFS.h"
|
||||
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
||||
#include "HanToJson.h"
|
||||
|
||||
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature) {
|
||||
json["id"] = WiFi.macAddress();
|
||||
json["up"] = millis();
|
||||
json["t"] = data.getPackageTimestamp();
|
||||
|
||||
double vcc = hw.getVcc();
|
||||
if(vcc > 0) {
|
||||
json["vcc"] = serialized(String(vcc, 3));
|
||||
}
|
||||
|
||||
json["rssi"] = hw.getWifiRssi();
|
||||
|
||||
if(temperature != DEVICE_DISCONNECTED_C) {
|
||||
json["temp"] = serialized(String(temperature, 2));
|
||||
}
|
||||
|
||||
// Add a sub-structure to the json object,
|
||||
// to keep the data from the meter itself
|
||||
JsonObject jd = json.createNestedObject("data");
|
||||
|
||||
switch(data.getListType()) {
|
||||
case 3:
|
||||
jd["rtc"] = data.getMeterTimestamp();
|
||||
jd["tPI"] = data.getActiveImportCounter();
|
||||
jd["tPO"] = data.getActiveExportCounter();
|
||||
jd["tQI"] = data.getReactiveImportCounter();
|
||||
jd["tQO"] = data.getReactiveExportCounter();
|
||||
case 2:
|
||||
jd["lv"] = data.getListId();
|
||||
jd["id"] = data.getMeterId();
|
||||
jd["type"] = data.getMeterType();
|
||||
jd["Q"] = data.getReactiveImportPower();
|
||||
jd["PO"] = data.getActiveExportPower();
|
||||
jd["QO"] = data.getReactiveExportPower();
|
||||
jd["I1"] = data.getL1Current();
|
||||
jd["I2"] = data.getL2Current();
|
||||
jd["I3"] = data.getL3Current();
|
||||
jd["U1"] = data.getL1Voltage();
|
||||
jd["U2"] = data.getL2Voltage();
|
||||
jd["U3"] = data.getL3Voltage();
|
||||
case 1:
|
||||
jd["P"] = data.getActiveImportPower();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#ifndef _HANTOJSON_h
|
||||
#define _HANTOJSON_h
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include "AmsData.h"
|
||||
#include "HwTools.h"
|
||||
|
||||
void hanToJson(JsonDocument& json, AmsData& data, HwTools& hw, double temperature);
|
||||
|
||||
#endif
|
||||
411
src/HwTools.cpp
411
src/HwTools.cpp
@@ -1,40 +1,399 @@
|
||||
#include "HwTools.h"
|
||||
|
||||
double HwTools::getVcc() {
|
||||
#if defined(ARDUINO_ESP8266_WEMOS_D1MINI)
|
||||
return (((double) ESP.getVcc()) / 900); // This board has a voltage divider on VCC. Yes, 900 is correct
|
||||
#elif defined(ESP8266)
|
||||
#if defined(ESP_VCC_CALIB_FACTOR)
|
||||
return ((double) ESP.getVcc()) / 1024 * ESP_VCC_CALIB_FACTOR;
|
||||
#else
|
||||
return ((double) ESP.getVcc()) / 1024;
|
||||
void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
|
||||
this->config = config;
|
||||
this->amsConf = amsConf;
|
||||
this->tempSensorInit = false;
|
||||
if(sensorApi != NULL)
|
||||
delete sensorApi;
|
||||
if(oneWire != NULL)
|
||||
delete oneWire;
|
||||
if(config->tempSensorPin > 0 && config->tempSensorPin < 40) {
|
||||
pinMode(config->tempSensorPin, INPUT);
|
||||
} else {
|
||||
config->tempSensorPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->vccPin > 0 && config->vccPin < 40) {
|
||||
getAdcChannel(config->vccPin, voltAdc);
|
||||
if(voltAdc.unit != 0xFF) {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
if(voltAdc.unit == ADC_UNIT_1) {
|
||||
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
|
||||
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
|
||||
adc1_config_channel_atten((adc1_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
|
||||
} else if(voltAdc.unit == ADC_UNIT_2) {
|
||||
voltAdcChar = (esp_adc_cal_characteristics_t*) calloc(1, sizeof(esp_adc_cal_characteristics_t));
|
||||
esp_adc_cal_value_t adcVal = esp_adc_cal_characterize((adc_unit_t) voltAdc.unit, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, 1100, voltAdcChar);
|
||||
adc2_config_channel_atten((adc2_channel_t) voltAdc.channel, ADC_ATTEN_DB_6);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
pinMode(config->vccPin, INPUT);
|
||||
}
|
||||
} else {
|
||||
voltAdc.unit = 0xFF;
|
||||
voltAdc.channel = 0xFF;
|
||||
config->vccPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->tempAnalogSensorPin > 0 && config->tempAnalogSensorPin < 40) {
|
||||
pinMode(config->tempAnalogSensorPin, INPUT);
|
||||
} else {
|
||||
config->tempAnalogSensorPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->ledPin > 0 && config->ledPin < 40) {
|
||||
pinMode(config->ledPin, OUTPUT);
|
||||
ledOff(LED_INTERNAL);
|
||||
} else {
|
||||
config->ledPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->ledPinRed > 0 && config->ledPinRed < 40) {
|
||||
pinMode(config->ledPinRed, OUTPUT);
|
||||
ledOff(LED_RED);
|
||||
} else {
|
||||
config->ledPinRed = 0xFF;
|
||||
}
|
||||
|
||||
if(config->ledPinGreen > 0 && config->ledPinGreen < 40) {
|
||||
pinMode(config->ledPinGreen, OUTPUT);
|
||||
ledOff(LED_GREEN);
|
||||
} else {
|
||||
config->ledPinGreen = 0xFF;
|
||||
}
|
||||
|
||||
if(config->ledPinBlue > 0 && config->ledPinBlue < 40) {
|
||||
pinMode(config->ledPinBlue, OUTPUT);
|
||||
ledOff(LED_BLUE);
|
||||
} else {
|
||||
config->ledPinBlue = 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
void HwTools::getAdcChannel(uint8_t pin, AdcConfig& config) {
|
||||
config.unit = 0xFF;
|
||||
config.channel = 0xFF;
|
||||
#if defined(ESP32)
|
||||
switch(pin) {
|
||||
case ADC1_CHANNEL_0_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_0;
|
||||
break;
|
||||
case ADC1_CHANNEL_1_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_1;
|
||||
break;
|
||||
case ADC1_CHANNEL_2_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_2;
|
||||
break;
|
||||
case ADC1_CHANNEL_3_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_3;
|
||||
break;
|
||||
case ADC1_CHANNEL_4_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_4;
|
||||
break;
|
||||
case ADC1_CHANNEL_5_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_5;
|
||||
break;
|
||||
case ADC1_CHANNEL_6_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_6;
|
||||
break;
|
||||
case ADC1_CHANNEL_7_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_1;
|
||||
config.channel = ADC1_CHANNEL_7;
|
||||
break;
|
||||
case ADC2_CHANNEL_0_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_0;
|
||||
break;
|
||||
case ADC2_CHANNEL_1_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_1;
|
||||
break;
|
||||
case ADC2_CHANNEL_2_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_2;
|
||||
break;
|
||||
case ADC2_CHANNEL_3_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_3;
|
||||
break;
|
||||
case ADC2_CHANNEL_4_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_4;
|
||||
break;
|
||||
case ADC2_CHANNEL_5_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_5;
|
||||
break;
|
||||
case ADC2_CHANNEL_6_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_6;
|
||||
break;
|
||||
case ADC2_CHANNEL_7_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_7;
|
||||
break;
|
||||
case ADC2_CHANNEL_8_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_8;
|
||||
break;
|
||||
case ADC2_CHANNEL_9_GPIO_NUM:
|
||||
config.unit = ADC_UNIT_2;
|
||||
config.channel = ADC2_CHANNEL_9;
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
double HwTools::getVcc() {
|
||||
double volts = 0.0;
|
||||
if(config->vccPin != 0xFF) {
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32)
|
||||
if(voltAdc.unit != 0xFF) {
|
||||
uint32_t x = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if(voltAdc.unit == ADC_UNIT_1) {
|
||||
x += adc1_get_raw((adc1_channel_t) voltAdc.channel);
|
||||
} else if(voltAdc.unit == ADC_UNIT_2) {
|
||||
int v = 0;
|
||||
adc2_get_raw((adc2_channel_t) voltAdc.channel, ADC_WIDTH_BIT_12, &v);
|
||||
x += v;
|
||||
}
|
||||
}
|
||||
x = x / 10;
|
||||
uint32_t voltage = esp_adc_cal_raw_to_voltage(x, voltAdcChar);
|
||||
volts = voltage / 1000.0;
|
||||
} else {
|
||||
uint32_t x = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
x += analogRead(config->vccPin);
|
||||
}
|
||||
volts = x / 40950;
|
||||
}
|
||||
#else
|
||||
uint32_t x = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
x += analogRead(config->vccPin);
|
||||
}
|
||||
volts = x / 10240;
|
||||
#endif
|
||||
} else {
|
||||
#if defined(ESP8266)
|
||||
volts = ESP.getVcc() / 1024.0;
|
||||
#endif
|
||||
}
|
||||
if(config->vccResistorGnd > 0 && config->vccResistorVcc > 0) {
|
||||
volts *= ((double) (config->vccResistorGnd + config->vccResistorVcc) / config->vccResistorGnd);
|
||||
}
|
||||
|
||||
|
||||
float vccOffset = config->vccOffset / 100.0;
|
||||
float vccMultiplier = config->vccMultiplier / 1000.0;
|
||||
return vccOffset + (volts > 0.0 ? volts * vccMultiplier : 0.0);
|
||||
}
|
||||
|
||||
uint8_t HwTools::getTempSensorCount() {
|
||||
return sensorCount;
|
||||
}
|
||||
|
||||
TempSensorData* HwTools::getTempSensorData(uint8_t i) {
|
||||
if(i < sensorCount) {
|
||||
return tempSensors[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool HwTools::updateTemperatures() {
|
||||
if(config->tempSensorPin != 0xFF) {
|
||||
if(!tempSensorInit) {
|
||||
oneWire = new OneWire(config->tempSensorPin);
|
||||
sensorApi = new DallasTemperature(this->oneWire);
|
||||
sensorApi->begin();
|
||||
delay(100);
|
||||
tempSensorInit = true;
|
||||
|
||||
DeviceAddress addr;
|
||||
sensorApi->requestTemperatures();
|
||||
int c = sensorApi->getDeviceCount();
|
||||
if(this->tempSensors != NULL) {
|
||||
delete this->tempSensors;
|
||||
}
|
||||
this->tempSensors = new TempSensorData*[c];
|
||||
for(int i = 0; i < c; i++) {
|
||||
bool found = false;
|
||||
sensorApi->getAddress(addr, i);
|
||||
float t = sensorApi->getTempC(addr);
|
||||
for(int x = 0; x < sensorCount; x++) {
|
||||
TempSensorData *data = tempSensors[x];
|
||||
if(isSensorAddressEqual(data->address, addr)) {
|
||||
found = true;
|
||||
data->lastRead = t;
|
||||
if(t > -85) {
|
||||
data->changed = data->lastValidRead != t;
|
||||
data->lastValidRead = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!found) {
|
||||
TempSensorData *data = new TempSensorData();
|
||||
memcpy(data->address, addr, 8);
|
||||
data->lastRead = t;
|
||||
if(t > -85) {
|
||||
data->changed = data->lastValidRead != t;
|
||||
data->lastValidRead = t;
|
||||
}
|
||||
tempSensors[sensorCount++] = data;
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
} else {
|
||||
if(sensorCount > 0) {
|
||||
sensorApi->requestTemperatures();
|
||||
|
||||
for(int x = 0; x < sensorCount; x++) {
|
||||
TempSensorData *data = tempSensors[x];
|
||||
float t = sensorApi->getTempC(data->address);
|
||||
data->lastRead = t;
|
||||
if(t > -85) {
|
||||
data->changed = data->lastValidRead != t;
|
||||
data->lastValidRead = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HwTools::isSensorAddressEqual(uint8_t a[8], uint8_t b[8]) {
|
||||
for(int i = 0; i < 8; i++) {
|
||||
if(a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double HwTools::getTemperature() {
|
||||
|
||||
#if defined TEMP_SENSOR_PIN
|
||||
if(!tempSensorInit) {
|
||||
tempSensor->begin();
|
||||
delay(50);
|
||||
tempSensor->requestTemperatures();
|
||||
hasTempSensor = tempSensor->getTempCByIndex(0) != DEVICE_DISCONNECTED_C;
|
||||
tempSensorInit = true;
|
||||
uint8_t c = 0;
|
||||
double ret = 0;
|
||||
double analogTemp = getTemperatureAnalog();
|
||||
if(analogTemp != DEVICE_DISCONNECTED_C) {
|
||||
ret += analogTemp;
|
||||
c++;
|
||||
}
|
||||
|
||||
if(hasTempSensor) {
|
||||
tempSensor->requestTemperatures();
|
||||
return tempSensor->getTempCByIndex(0);
|
||||
} else {
|
||||
return DEVICE_DISCONNECTED_C;
|
||||
for(int x = 0; x < sensorCount; x++) {
|
||||
TempSensorData data = *tempSensors[x];
|
||||
TempSensorConfig* conf = amsConf->getTempSensorConfig(data.address);
|
||||
if((conf == NULL || conf->common) && data.lastValidRead > -85) {
|
||||
ret += data.lastValidRead;
|
||||
c++;
|
||||
}
|
||||
}
|
||||
return c == 0 ? DEVICE_DISCONNECTED_C : ret/c;
|
||||
}
|
||||
double HwTools::getTemperatureAnalog() {
|
||||
if(config->tempAnalogSensorPin != 0xFF) {
|
||||
float adcCalibrationFactor = 1.06587;
|
||||
int volts;
|
||||
#if defined(ESP8266)
|
||||
volts = (analogRead(config->tempAnalogSensorPin) / 1024.0) * 3.3;
|
||||
#elif defined(ESP32)
|
||||
volts = (analogRead(config->tempAnalogSensorPin) / 4095.0) * 3.3;
|
||||
#endif
|
||||
return ((volts * adcCalibrationFactor) - 0.4) / 0.0195;
|
||||
}
|
||||
#endif
|
||||
return DEVICE_DISCONNECTED_C;
|
||||
}
|
||||
|
||||
int HwTools::getWifiRssi() {
|
||||
int rssi = WiFi.RSSI();
|
||||
return isnan(rssi) ? -100.0 : rssi;
|
||||
}
|
||||
}
|
||||
|
||||
bool HwTools::ledOn(uint8_t color) {
|
||||
if(color == LED_INTERNAL) {
|
||||
return writeLedPin(color, config->ledInverted ? LOW : HIGH);
|
||||
} else {
|
||||
return writeLedPin(color, config->ledRgbInverted ? LOW : HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
bool HwTools::ledOff(uint8_t color) {
|
||||
if(color == LED_INTERNAL) {
|
||||
return writeLedPin(color, config->ledInverted ? HIGH : LOW);
|
||||
} else {
|
||||
return writeLedPin(color, config->ledRgbInverted ? HIGH : LOW);
|
||||
}
|
||||
}
|
||||
|
||||
bool HwTools::ledBlink(uint8_t color, uint8_t blink) {
|
||||
for(int i = 0; i < blink; i++) {
|
||||
if(!ledOn(color)) return false;
|
||||
delay(50);
|
||||
ledOff(color);
|
||||
if(i != blink)
|
||||
delay(50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
|
||||
switch(color) {
|
||||
case LED_INTERNAL: {
|
||||
if(config->ledPin != 0xFF) {
|
||||
digitalWrite(config->ledPin, state);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LED_RED: {
|
||||
if(config->ledPinRed != 0xFF) {
|
||||
digitalWrite(config->ledPinRed, state);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LED_GREEN: {
|
||||
if(config->ledPinGreen != 0xFF) {
|
||||
digitalWrite(config->ledPinGreen, state);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LED_BLUE: {
|
||||
if(config->ledPinBlue != 0xFF) {
|
||||
digitalWrite(config->ledPinBlue, state);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LED_YELLOW: {
|
||||
if(config->ledPinRed != 0xFF && config->ledPinGreen != 0xFF) {
|
||||
digitalWrite(config->ledPinRed, state);
|
||||
digitalWrite(config->ledPinGreen, state);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,37 +7,65 @@
|
||||
#include <ESP8266WiFi.h>
|
||||
#elif defined(ESP32)
|
||||
#include <WiFi.h>
|
||||
#include <driver/adc.h>
|
||||
#include <esp_adc_cal.h>
|
||||
#include <soc/adc_channel.h>
|
||||
#endif
|
||||
|
||||
#include <DallasTemperature.h>
|
||||
#include <OneWire.h>
|
||||
#include "AmsConfiguration.h"
|
||||
|
||||
#if HW_ROARFRED
|
||||
#define TEMP_SENSOR_PIN 5
|
||||
#elif defined(ARDUINO_LOLIN_D32)
|
||||
#define TEMP_SENSOR_PIN 14
|
||||
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
|
||||
#define TEMP_SENSOR_PIN D5
|
||||
#else
|
||||
#define TEMP_SENSOR_PIN 0xFFFFFFFF
|
||||
#endif
|
||||
#define LED_INTERNAL 0
|
||||
#define LED_RED 1
|
||||
#define LED_GREEN 2
|
||||
#define LED_BLUE 3
|
||||
#define LED_YELLOW 4
|
||||
|
||||
struct TempSensorData {
|
||||
uint8_t address[8];
|
||||
float lastRead;
|
||||
float lastValidRead;
|
||||
bool changed;
|
||||
};
|
||||
|
||||
struct AdcConfig {
|
||||
uint8_t unit;
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
class HwTools {
|
||||
public:
|
||||
void setup(GpioConfig*, AmsConfiguration*);
|
||||
double getVcc();
|
||||
uint8_t getTempSensorCount();
|
||||
TempSensorData* getTempSensorData(uint8_t);
|
||||
bool updateTemperatures();
|
||||
double getTemperature();
|
||||
double getTemperatureAnalog();
|
||||
double getTemperature(uint8_t address[8]);
|
||||
int getWifiRssi();
|
||||
bool ledOn(uint8_t color);
|
||||
bool ledOff(uint8_t color);
|
||||
bool ledBlink(uint8_t color, uint8_t blink);
|
||||
|
||||
HwTools() {
|
||||
oneWire = new OneWire(TEMP_SENSOR_PIN);
|
||||
tempSensor = new DallasTemperature(this->oneWire);
|
||||
};
|
||||
HwTools() {};
|
||||
private:
|
||||
bool tempSensorInit, hasTempSensor;
|
||||
OneWire *oneWire;
|
||||
DallasTemperature *tempSensor;
|
||||
AdcConfig voltAdc, tempAdc;
|
||||
#if defined(ESP32)
|
||||
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;
|
||||
#endif
|
||||
GpioConfig* config;
|
||||
AmsConfiguration* amsConf;
|
||||
bool tempSensorInit;
|
||||
OneWire *oneWire = NULL;
|
||||
DallasTemperature *sensorApi = NULL;
|
||||
uint8_t sensorCount = 0;
|
||||
TempSensorData** tempSensors = NULL;
|
||||
|
||||
bool writeLedPin(uint8_t color, uint8_t state);
|
||||
bool isSensorAddressEqual(uint8_t a[8], uint8_t b[8]);
|
||||
void getAdcChannel(uint8_t pin, AdcConfig&);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
108
src/IEC6205621.cpp
Normal file
108
src/IEC6205621.cpp
Normal file
@@ -0,0 +1,108 @@
|
||||
#include "IEC6205621.h"
|
||||
|
||||
IEC6205621::IEC6205621(String payload) {
|
||||
if(payload.length() < 16)
|
||||
return;
|
||||
|
||||
lastUpdateMillis = millis();
|
||||
listId = payload.substring(payload.startsWith("/") ? 1 : 0, payload.indexOf("\n"));
|
||||
if(listId.startsWith("ADN")) {
|
||||
meterType = AmsTypeAidon;
|
||||
listId = listId.substring(0,4);
|
||||
} else if(listId.startsWith("KFM")) {
|
||||
meterType = AmsTypeKaifa;
|
||||
listId = listId.substring(0,4);
|
||||
} else if(listId.startsWith("KMP")) {
|
||||
meterType = AmsTypeKamstrup;
|
||||
listId = listId.substring(0,4);
|
||||
} else if(listId.startsWith("ISk")) {
|
||||
meterType = AmsTypeIskra;
|
||||
listId = listId.substring(0,5);
|
||||
} else if(listId.startsWith("XMX")) {
|
||||
meterType = AmsTypeLandis;
|
||||
listId = listId.substring(0,6);
|
||||
} else if(listId.startsWith("Ene")) {
|
||||
meterType = AmsTypeSagemcom;
|
||||
listId = listId.substring(0,4);
|
||||
} else {
|
||||
meterType = AmsTypeUnknown;
|
||||
listId = listId.substring(0,4);
|
||||
}
|
||||
|
||||
meterId = extract(payload, "96.1.0");
|
||||
if(meterId.isEmpty()) {
|
||||
meterId = extract(payload, "0.0.5");
|
||||
}
|
||||
|
||||
meterModel = extract(payload, "96.1.1");
|
||||
if(meterModel.isEmpty()) {
|
||||
meterModel = extract(payload, "96.1.7");
|
||||
if(meterModel.isEmpty()) {
|
||||
meterModel = payload.substring(payload.indexOf(listId) + listId.length(), payload.indexOf("\n"));
|
||||
meterModel.trim();
|
||||
}
|
||||
}
|
||||
|
||||
String timestamp = extract(payload, "1.0.0");
|
||||
if(timestamp.length() > 10) {
|
||||
tmElements_t tm;
|
||||
tm.Year = (timestamp.substring(0,2).toInt() + 2000) - 1970;
|
||||
tm.Month = timestamp.substring(4,6).toInt();
|
||||
tm.Day = timestamp.substring(2,4).toInt();
|
||||
tm.Hour = timestamp.substring(6,8).toInt();
|
||||
tm.Minute = timestamp.substring(8,10).toInt();
|
||||
tm.Second = timestamp.substring(10,12).toInt();
|
||||
meterTimestamp = makeTime(tm); // TODO: Adjust for time zone
|
||||
}
|
||||
|
||||
activeImportPower = (uint16_t) (extractDouble(payload, "1.7.0") * 1000);
|
||||
activeExportPower = (uint16_t) (extractDouble(payload, "2.7.0") * 1000);
|
||||
reactiveImportPower = (uint16_t) (extractDouble(payload, "3.7.0") * 1000);
|
||||
reactiveExportPower = (uint16_t) (extractDouble(payload, "4.7.0") * 1000);
|
||||
|
||||
if(activeImportPower > 0)
|
||||
listType = 1;
|
||||
|
||||
l1voltage = extractDouble(payload, "32.7.0");
|
||||
l2voltage = extractDouble(payload, "52.7.0");
|
||||
l3voltage = extractDouble(payload, "72.7.0");
|
||||
|
||||
l1current = extractDouble(payload, "31.7.0");
|
||||
l2current = extractDouble(payload, "51.7.0");
|
||||
l3current = extractDouble(payload, "71.7.0");
|
||||
|
||||
if(l1voltage > 0 || l2voltage > 0 || l3voltage > 0)
|
||||
listType = 2;
|
||||
|
||||
activeImportCounter = extractDouble(payload, "1.8.0");
|
||||
activeExportCounter = extractDouble(payload, "2.8.0");
|
||||
reactiveImportCounter = extractDouble(payload, "3.8.0");
|
||||
reactiveExportCounter = extractDouble(payload, "4.8.0");
|
||||
|
||||
if(activeImportCounter > 0 || activeExportCounter > 0 || reactiveImportCounter > 0 || reactiveExportCounter > 0)
|
||||
listType = 3;
|
||||
|
||||
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
|
||||
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
|
||||
|
||||
if(threePhase) {
|
||||
if(l2current == 0 && l1current != 0 && l3current != 0) {
|
||||
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String IEC6205621::extract(String payload, String obis) {
|
||||
int a = payload.indexOf(String(":" + obis + "("));
|
||||
if(a > 0) {
|
||||
int b = payload.indexOf(")", a);
|
||||
if(b > a) {
|
||||
return payload.substring(a+obis.length()+2, b);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
double IEC6205621::extractDouble(String payload, String obis) {
|
||||
return extract(payload, obis).toDouble();
|
||||
}
|
||||
14
src/IEC6205621.h
Normal file
14
src/IEC6205621.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef _IEC62056_21_H
|
||||
#define _IEC62056_21_H
|
||||
|
||||
#include "AmsData.h"
|
||||
|
||||
class IEC6205621 : public AmsData {
|
||||
public:
|
||||
IEC6205621(String payload);
|
||||
|
||||
private:
|
||||
String extract(String payload, String obis);
|
||||
double extractDouble(String payload, String obis);
|
||||
};
|
||||
#endif
|
||||
526
src/IEC6205675.cpp
Normal file
526
src/IEC6205675.cpp
Normal file
@@ -0,0 +1,526 @@
|
||||
#include "IEC6205675.h"
|
||||
#include "lwip/def.h"
|
||||
#include "Timezone.h"
|
||||
|
||||
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, uint8_t distributionSystem, CosemDateTime packageTimestamp, HDLCConfig* hc) {
|
||||
double val;
|
||||
char str[64];
|
||||
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
Timezone tz(CEST, CET);
|
||||
|
||||
this->packageTimestamp = getTimestamp(packageTimestamp);
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
|
||||
if(val == NOVALUE) {
|
||||
CosemData* data = getCosemDataAt(1, ((char *) (d)));
|
||||
|
||||
// Kaifa special case...
|
||||
if(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);
|
||||
if(listId.startsWith("KFM_001")) {
|
||||
this->listId = listId;
|
||||
meterType = AmsTypeKaifa;
|
||||
|
||||
int idx = 0;
|
||||
data = getCosemDataAt(idx, ((char *) (d)));
|
||||
idx+=2;
|
||||
if(data->base.length == 0x0D || data->base.length == 0x12) {
|
||||
listType = data->base.length == 0x12 ? 3 : 2;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
meterId = String(str);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
meterModel = String(str);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveImportPower = ntohl(data->dlu.data);
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveExportPower = ntohl(data->dlu.data);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1current = ntohl(data->dlu.data) / 1000.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2current = ntohl(data->dlu.data) / 1000.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3current = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1voltage = ntohl(data->dlu.data) / 10.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l2voltage = ntohl(data->dlu.data) / 10.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l3voltage = ntohl(data->dlu.data) / 10.0;
|
||||
} else if(data->base.length == 0x09 || data->base.length == 0x0E) {
|
||||
listType = data->base.length == 0x0E ? 3 : 2;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
meterId = String(str);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
memcpy(str, data->oct.data, data->oct.length);
|
||||
str[data->oct.length] = 0x00;
|
||||
meterModel = String(str);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportPower = ntohl(data->dlu.data);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveImportPower = ntohl(data->dlu.data);
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveExportPower = ntohl(data->dlu.data);
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1current = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
l1voltage = ntohl(data->dlu.data) / 10.0;
|
||||
}
|
||||
|
||||
if(listType >= 2 && memcmp(meterModel.c_str(), "MA304T3", 7) == 0) {
|
||||
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
|
||||
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60 * (PI/180)), 2) + pow(l3voltage * sin(60 * (PI/180)),2));
|
||||
}
|
||||
|
||||
if(listType == 3) {
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
switch(data->base.type) {
|
||||
case CosemTypeOctetString: {
|
||||
if(data->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) data;
|
||||
time_t ts = getTimestamp(amst->dt);
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
activeExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveImportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
reactiveExportCounter = ntohl(data->dlu.data) / 1000.0;
|
||||
}
|
||||
|
||||
lastUpdateMillis = millis();
|
||||
}
|
||||
} else 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 = millis();
|
||||
}
|
||||
// Kaifa end
|
||||
} else {
|
||||
listType = 1;
|
||||
activeImportPower = val;
|
||||
|
||||
meterType = AmsTypeUnknown;
|
||||
CosemData* version = findObis(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), d);
|
||||
if(version != NULL && version->base.type == CosemTypeString) {
|
||||
if(memcmp(version->str.data, "AIDON", 5) == 0) {
|
||||
meterType = AmsTypeAidon;
|
||||
} else if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
|
||||
meterType = AmsTypeKamstrup;
|
||||
}
|
||||
} else {
|
||||
version = getCosemDataAt(1, ((char *) (d)));
|
||||
if(version->base.type == CosemTypeString) {
|
||||
if(memcmp(version->str.data, "Kamstrup", 8) == 0) {
|
||||
meterType = AmsTypeKamstrup;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try system title
|
||||
if(meterType == AmsTypeUnknown && hc != NULL) {
|
||||
if(memcmp(hc->system_title, "SAGY", 4)) {
|
||||
meterType = AmsTypeSagemcom;
|
||||
}
|
||||
}
|
||||
|
||||
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
}
|
||||
|
||||
uint8_t str_len = 0;
|
||||
str_len = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
listId = String(str);
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
activeExportPower = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
reactiveImportPower = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
reactiveExportPower = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L1, sizeof(AMS_OBIS_VOLTAGE_L1), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l1voltage = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L2, sizeof(AMS_OBIS_VOLTAGE_L2), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l2voltage = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L3, sizeof(AMS_OBIS_VOLTAGE_L3), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l3voltage = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_CURRENT_L1, sizeof(AMS_OBIS_CURRENT_L1), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l1current = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_CURRENT_L2, sizeof(AMS_OBIS_CURRENT_L2), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l2current = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_CURRENT_L3, sizeof(AMS_OBIS_CURRENT_L3), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 2;
|
||||
l3current = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_IMPORT_COUNT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 3;
|
||||
activeImportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_EXPORT_COUNT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 3;
|
||||
activeExportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_IMPORT_COUNT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 3;
|
||||
reactiveImportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_EXPORT_COUNT), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
listType = 3;
|
||||
reactiveExportCounter = val / 1000.0;
|
||||
}
|
||||
|
||||
str_len = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterModel = String(str);
|
||||
} else {
|
||||
str_len = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterModel = String(str);
|
||||
}
|
||||
}
|
||||
|
||||
str_len = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterId = String(str);
|
||||
} else {
|
||||
str_len = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
|
||||
if(str_len > 0) {
|
||||
meterId = String(str);
|
||||
}
|
||||
}
|
||||
|
||||
CosemData* meterTs = findObis(AMS_OBIS_METER_TIMESTAMP, sizeof(AMS_OBIS_METER_TIMESTAMP), ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
time_t ts = getTimestamp(amst->dt);
|
||||
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
|
||||
meterTimestamp = tz.toUTC(ts);
|
||||
} else {
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
powerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L1, sizeof(AMS_OBIS_POWER_FACTOR_L1), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
l1PowerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L2, sizeof(AMS_OBIS_POWER_FACTOR_L2), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
l2PowerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L3, sizeof(AMS_OBIS_POWER_FACTOR_L3), ((char *) (d)));
|
||||
if(val != NOVALUE) {
|
||||
l3PowerFactor = val;
|
||||
}
|
||||
|
||||
if(meterType == AmsTypeKamstrup) {
|
||||
if(listType >= 3) {
|
||||
activeImportCounter *= 10;
|
||||
activeExportCounter *= 10;
|
||||
reactiveImportCounter *= 10;
|
||||
reactiveExportCounter *= 10;
|
||||
}
|
||||
if(l1current != 0)
|
||||
l1current /= 100;
|
||||
if(l2current != 0)
|
||||
l2current /= 100;
|
||||
if(l3current != 0)
|
||||
l3current /= 100;
|
||||
if(powerFactor != 0)
|
||||
powerFactor /= 100;
|
||||
if(l1PowerFactor != 0)
|
||||
l1PowerFactor /= 100;
|
||||
if(l2PowerFactor != 0)
|
||||
l2PowerFactor /= 100;
|
||||
if(l3PowerFactor != 0)
|
||||
l3PowerFactor /= 100;
|
||||
} else if(meterType == AmsTypeSagemcom) {
|
||||
CosemData* meterTs = getCosemDataAt(1, ((char *) (d)));
|
||||
if(meterTs != NULL) {
|
||||
AmsOctetTimestamp* amst = (AmsOctetTimestamp*) meterTs;
|
||||
time_t ts = getTimestamp(amst->dt);
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
|
||||
CosemData* mid = getCosemDataAt(58, ((char *) (d))); // TODO: Get last item
|
||||
if(mid != NULL) {
|
||||
switch(mid->base.type) {
|
||||
case CosemTypeString:
|
||||
memcpy(&meterId, mid->str.data, mid->str.length);
|
||||
meterId[mid->str.length] = 0;
|
||||
break;
|
||||
case CosemTypeOctetString:
|
||||
memcpy(&meterId, mid->oct.data, mid->oct.length);
|
||||
meterId[mid->oct.length] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastUpdateMillis = millis();
|
||||
}
|
||||
|
||||
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
|
||||
if(!threePhase)
|
||||
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
|
||||
|
||||
// Special case for Norwegian IT/TT meters that does not report all values
|
||||
if(distributionSystem == 1) {
|
||||
if(threePhase) {
|
||||
if(l2current == 0.0 && l1current > 0.0 && l3current > 0.0) {
|
||||
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
|
||||
if(activeExportPower == 0.0) {
|
||||
l2current = max((float) 0.0, l2current);
|
||||
}
|
||||
}
|
||||
} else if(twoPhase && l1current > 0.0 && l2current > 0.0 && l3current > 0.0) {
|
||||
l2voltage = sqrt(pow(l1voltage - l3voltage * cos(60.0 * (PI/180.0)), 2) + pow(l3voltage * sin(60.0 * (PI/180.0)),2));
|
||||
threePhase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CosemData* IEC6205675::getCosemDataAt(uint8_t index, const char* ptr) {
|
||||
CosemData* item = (CosemData*) ptr;
|
||||
int i = 0;
|
||||
char* pos = (char*) ptr;
|
||||
do {
|
||||
item = (CosemData*) pos;
|
||||
if(i == index) return item;
|
||||
switch(item->base.type) {
|
||||
case CosemTypeArray:
|
||||
case CosemTypeStructure:
|
||||
pos += 2;
|
||||
break;
|
||||
case CosemTypeOctetString:
|
||||
case CosemTypeString:
|
||||
pos += 2 + item->base.length;
|
||||
break;
|
||||
case CosemTypeLongSigned:
|
||||
pos += 5;
|
||||
break;
|
||||
case CosemTypeLongUnsigned:
|
||||
pos += 3;
|
||||
break;
|
||||
case CosemTypeDLongUnsigned:
|
||||
pos += 5;
|
||||
break;
|
||||
case CosemTypeNull:
|
||||
return NULL;
|
||||
default:
|
||||
pos += 2;
|
||||
}
|
||||
i++;
|
||||
} while(item->base.type != HDLC_FLAG);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CosemData* IEC6205675::findObis(uint8_t* obis, int matchlength, const char* ptr) {
|
||||
CosemData* item = (CosemData*) ptr;
|
||||
int ret = 0;
|
||||
char* pos = (char*) ptr;
|
||||
do {
|
||||
item = (CosemData*) pos;
|
||||
if(ret == 1) return item;
|
||||
switch(item->base.type) {
|
||||
case CosemTypeArray:
|
||||
case CosemTypeStructure:
|
||||
pos += 2;
|
||||
break;
|
||||
case CosemTypeOctetString: {
|
||||
ret = 1;
|
||||
uint8_t* found = item->oct.data;
|
||||
int x = 6 - matchlength;
|
||||
for(int i = x; i < 6; i++) {
|
||||
if(found[i] != obis[i-x]) ret = 0;
|
||||
}
|
||||
} // Fallthrough
|
||||
case CosemTypeString: {
|
||||
pos += 2 + item->base.length;
|
||||
break;
|
||||
}
|
||||
case CosemTypeLongSigned:
|
||||
pos += 5;
|
||||
break;
|
||||
case CosemTypeLongUnsigned:
|
||||
pos += 3;
|
||||
break;
|
||||
case CosemTypeDLongUnsigned:
|
||||
pos += 5;
|
||||
break;
|
||||
case CosemTypeNull:
|
||||
return NULL;
|
||||
default:
|
||||
pos += 2;
|
||||
}
|
||||
} while(item->base.type != HDLC_FLAG);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t IEC6205675::getString(uint8_t* obis, int matchlength, const char* ptr, char* target) {
|
||||
CosemData* item = findObis(obis, matchlength, ptr);
|
||||
if(item != NULL) {
|
||||
switch(item->base.type) {
|
||||
case CosemTypeString:
|
||||
memcpy(target, item->str.data, item->str.length);
|
||||
target[item->str.length] = 0;
|
||||
return item->str.length;
|
||||
case CosemTypeOctetString:
|
||||
memcpy(target, item->oct.data, item->oct.length);
|
||||
target[item->oct.length] = 0;
|
||||
return item->oct.length;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double IEC6205675::getNumber(uint8_t* obis, int matchlength, const char* ptr) {
|
||||
CosemData* item = findObis(obis, matchlength, ptr);
|
||||
return getNumber(item);
|
||||
}
|
||||
|
||||
double IEC6205675::getNumber(CosemData* item) {
|
||||
if(item != NULL) {
|
||||
double ret = 0.0;
|
||||
char* pos = ((char*) item);
|
||||
switch(item->base.type) {
|
||||
case CosemTypeLongUnsigned: {
|
||||
uint16_t u16 = ntohs(item->lu.data);
|
||||
ret = u16;
|
||||
pos += 3;
|
||||
break;
|
||||
}
|
||||
case CosemTypeDLongUnsigned: {
|
||||
uint32_t u32 = ntohl(item->dlu.data);
|
||||
ret = u32;
|
||||
pos += 5;
|
||||
break;
|
||||
}
|
||||
case CosemTypeLongSigned: {
|
||||
uint16_t u16 = ntohs(item->lu.data); // ntohs only works for uint16 ?
|
||||
int16_t i16 = u16; // Cast to int16 before use?
|
||||
ret = i16; // Who knows, got to try it all...
|
||||
pos += 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(pos != NULL) {
|
||||
if(*pos++ == 0x02 && *pos++ == 0x02) {
|
||||
int8_t scale = *++pos;
|
||||
ret *= pow(10, scale);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
return NOVALUE;
|
||||
}
|
||||
|
||||
time_t IEC6205675::getTimestamp(uint8_t* obis, int matchlength, const char* ptr) {
|
||||
CosemData* item = findObis(obis, matchlength, ptr);
|
||||
if(item != NULL) {
|
||||
switch(item->base.type) {
|
||||
case CosemTypeOctetString: {
|
||||
if(item->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* ts = (AmsOctetTimestamp*) item;
|
||||
return getTimestamp(ts->dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
time_t IEC6205675::getTimestamp(CosemDateTime timestamp) {
|
||||
tmElements_t tm;
|
||||
uint16_t year = ntohs(timestamp.year);
|
||||
if(year < 1970) return 0;
|
||||
tm.Year = year - 1970;
|
||||
tm.Month = timestamp.month;
|
||||
tm.Day = timestamp.dayOfMonth;
|
||||
tm.Hour = timestamp.hour;
|
||||
tm.Minute = timestamp.minute;
|
||||
tm.Second = timestamp.second;
|
||||
|
||||
//Serial.printf("\nY: %d, M: %d, D: %d, h: %d, m: %d, s: %d, deviation: 0x%2X, status: 0x%1X\n", tm.Year, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, timestamp.deviation, timestamp.status);
|
||||
|
||||
time_t time = makeTime(tm);
|
||||
int16_t deviation = ntohs(timestamp.deviation);
|
||||
if(deviation >= -720 && deviation <= 720) {
|
||||
time -= deviation * 60;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
67
src/IEC6205675.h
Normal file
67
src/IEC6205675.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#ifndef _IEC62056_7_5_H
|
||||
#define _IEC62056_7_5_H
|
||||
|
||||
#include "AmsData.h"
|
||||
#include "ams/hdlc.h"
|
||||
|
||||
#define NOVALUE 0xFFFFFFFF
|
||||
|
||||
struct AmsOctetTimestamp {
|
||||
uint8_t type;
|
||||
CosemDateTime dt;
|
||||
} __attribute__((packed));
|
||||
|
||||
class IEC6205675 : public AmsData {
|
||||
public:
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, uint8_t distributionSystem, CosemDateTime packageTimestamp, HDLCConfig* hc);
|
||||
|
||||
private:
|
||||
CosemData* getCosemDataAt(uint8_t index, const char* ptr);
|
||||
CosemData* findObis(uint8_t* obis, int matchlength, const char* ptr);
|
||||
uint8_t getString(uint8_t* obis, int matchlength, const char* ptr, char* target);
|
||||
double getNumber(uint8_t* obis, int matchlength, const char* ptr);
|
||||
double getNumber(CosemData*);
|
||||
time_t getTimestamp(uint8_t* obis, int matchlength, const char* ptr);
|
||||
time_t getTimestamp(CosemDateTime timestamp);
|
||||
|
||||
uint8_t AMS_OBIS_VERSION[6] = { 1, 1, 0, 2, 129, 255 };
|
||||
uint8_t AMS_OBIS_METER_MODEL[4] = { 96, 1, 1, 255 };
|
||||
uint8_t AMS_OBIS_METER_MODEL_2[4] = { 96, 1, 7, 255 };
|
||||
uint8_t AMS_OBIS_METER_ID[4] = { 96, 1, 0, 255 };
|
||||
uint8_t AMS_OBIS_METER_ID_2[4] = { 0, 0, 5, 255 };
|
||||
uint8_t AMS_OBIS_METER_TIMESTAMP[4] = { 1, 0, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_IMPORT[4] = { 1, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_IMPORT_L1[4] = { 21, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_IMPORT_L2[4] = { 41, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_IMPORT_L3[4] = { 61, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_EXPORT[4] = { 2, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_EXPORT_L1[4] = { 22, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_EXPORT_L2[4] = { 42, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_EXPORT_L3[4] = { 62, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_IMPORT[4] = { 3, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_IMPORT_L1[4] = { 23, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_IMPORT_L2[4] = { 43, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_IMPORT_L3[4] = { 63, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_EXPORT[4] = { 4, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_EXPORT_L1[4] = { 24, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_EXPORT_L2[4] = { 44, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_EXPORT_L3[4] = { 64, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_CURRENT[4] = { 11, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_CURRENT_L1[4] = { 31, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_CURRENT_L2[4] = { 51, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_CURRENT_L3[4] = { 71, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_VOLTAGE[4] = { 12, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_VOLTAGE_L1[4] = { 32, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_VOLTAGE_L2[4] = { 52, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_VOLTAGE_L3[4] = { 72, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_IMPORT_COUNT[4] = { 1, 8, 0, 255 };
|
||||
uint8_t AMS_OBIS_ACTIVE_EXPORT_COUNT[4] = { 2, 8, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_IMPORT_COUNT[4] = { 3, 8, 0, 255 };
|
||||
uint8_t AMS_OBIS_REACTIVE_EXPORT_COUNT[4] = { 4, 8, 0, 255 };
|
||||
uint8_t AMS_OBIS_POWER_FACTOR[4] = { 13, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_POWER_FACTOR_L1[4] = { 33, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_POWER_FACTOR_L2[4] = { 53, 7, 0, 255 };
|
||||
uint8_t AMS_OBIS_POWER_FACTOR_L3[4] = { 73, 7, 0, 255 };
|
||||
|
||||
};
|
||||
#endif
|
||||
57
src/MbusAssembler.cpp
Normal file
57
src/MbusAssembler.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "Arduino.h"
|
||||
#include "MbusAssembler.h"
|
||||
#include "ams/hdlc.h"
|
||||
|
||||
MbusAssembler::MbusAssembler() {
|
||||
buf = (uint8_t *)malloc((size_t)1024); // TODO find out from first package ?
|
||||
}
|
||||
|
||||
uint8_t MbusAssembler::append(const uint8_t* d, int length) {
|
||||
MbusHeader* h = (MbusHeader*) d;
|
||||
uint8_t* ptr = (uint8_t*) &h[1];
|
||||
|
||||
uint8_t len = h->len1;
|
||||
|
||||
uint8_t control = *ptr;
|
||||
ptr++; len--;
|
||||
|
||||
uint8_t address = *ptr;
|
||||
ptr++; len--;
|
||||
|
||||
uint8_t ci = *ptr;
|
||||
ptr++; len--;
|
||||
|
||||
uint8_t stsap = *ptr;
|
||||
ptr++; len--;
|
||||
|
||||
uint8_t dtsap = *ptr;
|
||||
ptr++; len--;
|
||||
|
||||
uint8_t sequenceNumber = ci & 0x0F;
|
||||
if(sequenceNumber == 0) {
|
||||
memcpy(buf, d, length - 2); // Do not include FCS and MBUS_STOP
|
||||
buf[6] = 0x10; // Mark that this is a single, complete frame
|
||||
pos = length - 2;
|
||||
lastSequenceNumber = 0;
|
||||
return 0;
|
||||
} else if(pos + len > 1024 || sequenceNumber != (lastSequenceNumber + 1)) { // TODO return error
|
||||
pos = 0;
|
||||
lastSequenceNumber = -1;
|
||||
return -1;
|
||||
} else {
|
||||
if(len > length) return -1;
|
||||
memcpy(buf + pos, ptr, len);
|
||||
pos += len;
|
||||
lastSequenceNumber = sequenceNumber;
|
||||
return 0;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
uint16_t MbusAssembler::write(const uint8_t* d) {
|
||||
buf[1] = buf[2] = 0x00;
|
||||
buf[pos++] = mbusChecksum(buf+4, pos-4);
|
||||
buf[pos++] = MBUS_END;
|
||||
memcpy((uint8_t *) d, buf, pos);
|
||||
return pos;
|
||||
}
|
||||
18
src/MbusAssembler.h
Normal file
18
src/MbusAssembler.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef _MBUS_ASSEMBLER_H
|
||||
#define _MBUS_ASSEMBLER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
class MbusAssembler {
|
||||
public:
|
||||
MbusAssembler();
|
||||
uint8_t append(const uint8_t* d, int length);
|
||||
uint16_t write(const uint8_t* d);
|
||||
|
||||
private:
|
||||
uint16_t pos = 0;
|
||||
uint8_t *buf;
|
||||
uint8_t lastSequenceNumber = -1;
|
||||
};
|
||||
|
||||
#endif
|
||||
8
src/Uptime.cpp
Normal file
8
src/Uptime.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "Uptime.h"
|
||||
|
||||
uint64_t millis64() {
|
||||
uint32_t new_low32 = millis();
|
||||
if (new_low32 < _uptime_last_value) _uptime_rollovers++;
|
||||
_uptime_last_value = new_low32;
|
||||
return (uint64_t) _uptime_rollovers << 32 | _uptime_last_value;
|
||||
}
|
||||
10
src/Uptime.h
Normal file
10
src/Uptime.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#ifndef _UPTIME_H
|
||||
#define _UPTIME_H
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
static uint32_t _uptime_last_value = 0;
|
||||
static uint32_t _uptime_rollovers = 0;
|
||||
uint64_t millis64();
|
||||
|
||||
#endif
|
||||
12
src/ams/crc.cpp
Normal file
12
src/ams/crc.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "crc.h"
|
||||
|
||||
uint16_t crc16_x25(const uint8_t* p, int len)
|
||||
{
|
||||
uint16_t crc = UINT16_MAX;
|
||||
|
||||
while(len--)
|
||||
for (uint16_t i = 0, d = 0xff & *p++; i < 8; i++, d >>= 1)
|
||||
crc = ((crc & 1) ^ (d & 1)) ? (crc >> 1) ^ 0x8408 : (crc >> 1);
|
||||
|
||||
return (~crc << 8) | (~crc >> 8 & 0xff);
|
||||
}
|
||||
9
src/ams/crc.h
Normal file
9
src/ams/crc.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef _CRC_H
|
||||
#define _CRC_H
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <stdint.h>
|
||||
|
||||
uint16_t crc16_x25(const uint8_t* p, int len);
|
||||
|
||||
#endif
|
||||
289
src/ams/hdlc.cpp
Normal file
289
src/ams/hdlc.cpp
Normal file
@@ -0,0 +1,289 @@
|
||||
#include "Arduino.h"
|
||||
#include "hdlc.h"
|
||||
#include "crc.h"
|
||||
#include "lwip/def.h"
|
||||
#if defined(ESP8266)
|
||||
#include "bearssl/bearssl.h"
|
||||
#elif defined(ESP32)
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
|
||||
void mbus_hexdump(const uint8_t* buf, int len) {
|
||||
printf("\nDUMP (%db) [ ", len);
|
||||
for(const uint8_t* p = buf; p-buf < len; ++p)
|
||||
printf("%02X ", *p);
|
||||
printf("]\n");
|
||||
}
|
||||
|
||||
int HDLC_validate(const uint8_t* d, int length, HDLCConfig* config, CosemDateTime* timestamp) {
|
||||
int len;
|
||||
int headersize = 3;
|
||||
int footersize = 1;
|
||||
|
||||
uint8_t flag = *d;
|
||||
|
||||
uint8_t* ptr;
|
||||
if(flag == HDLC_FLAG) {
|
||||
if(length < 3)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
HDLCHeader* h = (HDLCHeader*) d;
|
||||
ptr = (uint8_t*) &h[1];
|
||||
|
||||
// Frame format type 3
|
||||
if((h->format & 0xF0) == 0xA0) {
|
||||
// Length field (11 lsb of format)
|
||||
len = (ntohs(h->format) & 0x7FF) + 2;
|
||||
if(len > length)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
HDLCFooter* f = (HDLCFooter*) (d + len - sizeof *f);
|
||||
footersize = sizeof *f;
|
||||
|
||||
// First and last byte should be MBUS_HAN_TAG
|
||||
if(h->flag != HDLC_FLAG || f->flag != HDLC_FLAG)
|
||||
return HDLC_BOUNDRY_FLAG_MISSING;
|
||||
|
||||
// Verify FCS
|
||||
if(ntohs(f->fcs) != crc16_x25(d + 1, len - sizeof *f - 1))
|
||||
return HDLC_FCS_ERROR;
|
||||
|
||||
// Skip destination address, LSB marks last byte
|
||||
while(((*ptr) & 0x01) == 0x00) {
|
||||
ptr++;
|
||||
headersize++;
|
||||
}
|
||||
headersize++;
|
||||
ptr++;
|
||||
|
||||
// Skip source address, LSB marks last byte
|
||||
while(((*ptr) & 0x01) == 0x00) {
|
||||
ptr++;
|
||||
headersize++;
|
||||
}
|
||||
headersize++;
|
||||
ptr++;
|
||||
|
||||
HDLC3CtrlHcs* t3 = (HDLC3CtrlHcs*) (ptr);
|
||||
headersize += 3;
|
||||
|
||||
// Verify HCS
|
||||
if(ntohs(t3->hcs) != crc16_x25(d + 1, ptr-d))
|
||||
return HDLC_HCS_ERROR;
|
||||
|
||||
ptr += sizeof *t3;
|
||||
|
||||
// Extract LLC
|
||||
HDLCLLC* llc = (HDLCLLC*) ptr;
|
||||
ptr += sizeof *llc;
|
||||
headersize += sizeof *llc;
|
||||
} else {
|
||||
return HDLC_UNKNOWN_DATA;
|
||||
}
|
||||
} else if(flag == MBUS_START) {
|
||||
// https://m-bus.com/documentation-wired/06-application-layer
|
||||
if(length < 4)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
MbusHeader* mh = (MbusHeader*) d;
|
||||
if(mh->flag1 != MBUS_START || mh->flag2 != MBUS_START)
|
||||
return MBUS_BOUNDRY_FLAG_MISSING;
|
||||
|
||||
// First two bytes is 1-byte length value repeated. Only used for last segment
|
||||
if(mh->len1 != mh->len2)
|
||||
return MBUS_FRAME_LENGTH_NOT_EQUAL;
|
||||
len = mh->len1;
|
||||
ptr = (uint8_t*) &mh[1];
|
||||
headersize = 4;
|
||||
footersize = 2;
|
||||
|
||||
if(len == 0x00)
|
||||
len = length - headersize - footersize;
|
||||
// Payload can max be 255 bytes, so I think the following case is only valid for austrian meters
|
||||
if(len < headersize)
|
||||
len += 256;
|
||||
|
||||
if((headersize + footersize + len) > length)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
MbusFooter* mf = (MbusFooter*) (d + len + headersize);
|
||||
if(mf->flag != MBUS_END)
|
||||
return MBUS_BOUNDRY_FLAG_MISSING;
|
||||
if(mbusChecksum(d + headersize, len) != mf->fcs)
|
||||
return MBUS_CHECKSUM_ERROR;
|
||||
|
||||
ptr += 2;
|
||||
|
||||
// Control information field
|
||||
uint8_t ci = *ptr;
|
||||
|
||||
// Bits 7 6 5 4 3 2 1 0
|
||||
// 0 0 0 Finished Sequence number
|
||||
uint8_t sequenceNumber = (ci & 0x0F);
|
||||
if((ci & 0x10) == 0x00) { // Not finished yet
|
||||
return MBUS_FRAME_INTERMEDIATE_SEGMENT;
|
||||
} else if(sequenceNumber > 0) { // This is the last frame of multiple, assembly needed
|
||||
return MBUS_FRAME_LAST_SEGMENT;
|
||||
}
|
||||
|
||||
// Skip CI, STSAP and DTSAP
|
||||
ptr += 3;
|
||||
headersize += 5; // And also control and address that we didn't skip earlier, needed these for checksum.
|
||||
} else {
|
||||
return HDLC_UNKNOWN_DATA;
|
||||
}
|
||||
|
||||
if(((*ptr) & 0xFF) == 0x0F) {
|
||||
// Unencrypted APDU
|
||||
HDLCADPU* adpu = (HDLCADPU*) (ptr);
|
||||
ptr += sizeof *adpu;
|
||||
|
||||
// ADPU timestamp
|
||||
CosemData* dateTime = (CosemData*) ptr;
|
||||
if(dateTime->base.type == CosemTypeOctetString) {
|
||||
if(dateTime->base.length == 0x0C) {
|
||||
memcpy(timestamp, ptr+1, dateTime->base.length+1);
|
||||
}
|
||||
ptr += 2 + dateTime->base.length;
|
||||
} else if(dateTime->base.type == CosemTypeNull) {
|
||||
timestamp = 0;
|
||||
ptr++;
|
||||
} else if(dateTime->base.type == CosemTypeDateTime) {
|
||||
memcpy(timestamp, ptr, dateTime->base.length);
|
||||
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
|
||||
memcpy(timestamp, ptr, 13);
|
||||
ptr += 13;
|
||||
} else {
|
||||
return HDLC_TIMESTAMP_UNKNOWN;
|
||||
}
|
||||
|
||||
return ptr-d;
|
||||
} else if(((*ptr) & 0xFF) == 0xDB) {
|
||||
if(length < headersize + 18)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
ptr++;
|
||||
// Encrypted APDU
|
||||
// http://www.weigu.lu/tutorials/sensors2bus/04_encryption/index.html
|
||||
if(config == NULL)
|
||||
return HDLC_ENCRYPTION_CONFIG_MISSING;
|
||||
|
||||
uint8_t systemTitleLength = *ptr;
|
||||
ptr++;
|
||||
memcpy(config->system_title, ptr, systemTitleLength);
|
||||
memcpy(config->initialization_vector, config->system_title, systemTitleLength);
|
||||
|
||||
headersize += 2 + systemTitleLength;
|
||||
ptr += systemTitleLength;
|
||||
if(((*ptr) & 0xFF) == 0x81) {
|
||||
ptr++;
|
||||
len = *ptr;
|
||||
// 1-byte payload length
|
||||
ptr++;
|
||||
headersize += 2;
|
||||
} else if(((*ptr) & 0xFF) == 0x82) {
|
||||
HDLCHeader* h = (HDLCHeader*) ptr;
|
||||
|
||||
// 2-byte payload length
|
||||
len = (ntohs(h->format) & 0xFFFF);
|
||||
|
||||
ptr += 3;
|
||||
headersize += 3;
|
||||
}
|
||||
if(len + headersize + footersize > length)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
//Serial.printf("\nL: %d : %d, %d : %d\n", length, len, headersize, footersize);
|
||||
|
||||
memcpy(config->additional_authenticated_data, ptr, 1);
|
||||
|
||||
// Security tag
|
||||
uint8_t sec = *ptr;
|
||||
ptr++;
|
||||
headersize++;
|
||||
|
||||
// Frame counter
|
||||
memcpy(config->initialization_vector + 8, ptr, 4);
|
||||
ptr += 4;
|
||||
headersize += 4;
|
||||
|
||||
// Authentication enabled
|
||||
uint8_t authkeylen = 0, aadlen = 0;
|
||||
if((sec & 0x10) == 0x10) {
|
||||
authkeylen = 12;
|
||||
aadlen = 17;
|
||||
footersize += authkeylen;
|
||||
memcpy(config->additional_authenticated_data + 1, config->authentication_key, 16);
|
||||
memcpy(config->authentication_tag, ptr + len - footersize - 2, authkeylen);
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
br_gcm_context gcmCtx;
|
||||
br_aes_ct_ctr_keys bc;
|
||||
br_aes_ct_ctr_init(&bc, config->encryption_key, 16);
|
||||
br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32);
|
||||
br_gcm_reset(&gcmCtx, config->initialization_vector, sizeof(config->initialization_vector));
|
||||
if(authkeylen > 0) {
|
||||
br_gcm_aad_inject(&gcmCtx, config->additional_authenticated_data, aadlen);
|
||||
}
|
||||
br_gcm_flip(&gcmCtx);
|
||||
br_gcm_run(&gcmCtx, 0, (void*) (ptr), len - authkeylen - 5); // 5 == security tag and frame counter
|
||||
if(authkeylen > 0 && br_gcm_check_tag_trunc(&gcmCtx, config->authentication_tag, authkeylen) != 1) {
|
||||
return HDLC_ENCRYPTION_AUTH_FAILED;
|
||||
}
|
||||
#elif defined(ESP32)
|
||||
uint8_t cipher_text[len - authkeylen - 5];
|
||||
memcpy(cipher_text, ptr, len - authkeylen - 5);
|
||||
|
||||
mbedtls_gcm_context m_ctx;
|
||||
mbedtls_gcm_init(&m_ctx);
|
||||
int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, config->encryption_key, 128);
|
||||
if (0 != success) {
|
||||
return HDLC_ENCRYPTION_KEY_FAILED;
|
||||
}
|
||||
success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), config->initialization_vector, sizeof(config->initialization_vector),
|
||||
config->additional_authenticated_data, aadlen, config->authentication_tag, authkeylen,
|
||||
cipher_text, (unsigned char*)(ptr));
|
||||
if (authkeylen > 0 && success == MBEDTLS_ERR_GCM_AUTH_FAILED) {
|
||||
return HDLC_ENCRYPTION_AUTH_FAILED;
|
||||
} else if(success == MBEDTLS_ERR_GCM_BAD_INPUT) {
|
||||
return HDLC_ENCRYPTION_DECRYPT_FAILED;
|
||||
}
|
||||
mbedtls_gcm_free(&m_ctx);
|
||||
#endif
|
||||
|
||||
HDLCADPU* adpu = (HDLCADPU*) (ptr);
|
||||
ptr += sizeof *adpu;
|
||||
|
||||
// ADPU timestamp
|
||||
CosemData* dateTime = (CosemData*) ptr;
|
||||
if(dateTime->base.type == CosemTypeOctetString) {
|
||||
if(dateTime->base.length == 0x0C) {
|
||||
memcpy(timestamp, ptr+1, dateTime->base.length);
|
||||
}
|
||||
ptr += 2 + dateTime->base.length;
|
||||
} else if(dateTime->base.type == CosemTypeNull) {
|
||||
timestamp = 0;
|
||||
ptr++;
|
||||
} else if(dateTime->base.type == CosemTypeDateTime) {
|
||||
memcpy(timestamp, ptr, dateTime->base.length);
|
||||
} else if(dateTime->base.type == 0x0C) { // Kamstrup bug...
|
||||
memcpy(timestamp, ptr, 0x0C);
|
||||
ptr += 13;
|
||||
} else {
|
||||
return HDLC_TIMESTAMP_UNKNOWN;
|
||||
}
|
||||
|
||||
return ptr-d;
|
||||
}
|
||||
|
||||
// Unknown payload
|
||||
return HDLC_UNKNOWN_DATA;
|
||||
}
|
||||
|
||||
uint8_t mbusChecksum(const uint8_t* p, int len) {
|
||||
uint8_t ret = 0;
|
||||
while(len--)
|
||||
ret += *p++;
|
||||
return ret;
|
||||
}
|
||||
143
src/ams/hdlc.h
Normal file
143
src/ams/hdlc.h
Normal file
@@ -0,0 +1,143 @@
|
||||
#ifndef _HDLC_H
|
||||
#define _HDLC_H
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <stdint.h>
|
||||
|
||||
#define HDLC_FLAG 0x7E
|
||||
#define HDLC_BOUNDRY_FLAG_MISSING -1
|
||||
#define HDLC_FCS_ERROR -2
|
||||
#define HDLC_HCS_ERROR -3
|
||||
#define HDLC_FRAME_INCOMPLETE -4
|
||||
#define HDLC_UNKNOWN_DATA -9
|
||||
#define HDLC_ENCRYPTION_CONFIG_MISSING -90
|
||||
#define HDLC_ENCRYPTION_AUTH_FAILED -91
|
||||
#define HDLC_ENCRYPTION_KEY_FAILED -92
|
||||
#define HDLC_ENCRYPTION_DECRYPT_FAILED -93
|
||||
#define HDLC_TIMESTAMP_UNKNOWN -99
|
||||
|
||||
#define MBUS_START 0x68
|
||||
#define MBUS_END 0x16
|
||||
#define MBUS_BOUNDRY_FLAG_MISSING -1
|
||||
#define MBUS_FRAME_LENGTH_NOT_EQUAL -40
|
||||
#define MBUS_FRAME_INTERMEDIATE_SEGMENT -41
|
||||
#define MBUS_FRAME_LAST_SEGMENT -42
|
||||
#define MBUS_CHECKSUM_ERROR -2
|
||||
|
||||
struct HDLCConfig {
|
||||
uint8_t encryption_key[32];
|
||||
uint8_t authentication_key[32];
|
||||
uint8_t system_title[8];
|
||||
uint8_t initialization_vector[12];
|
||||
uint8_t additional_authenticated_data[17];
|
||||
uint8_t authentication_tag[12];
|
||||
};
|
||||
|
||||
typedef struct HDLCHeader {
|
||||
uint8_t flag;
|
||||
uint16_t format;
|
||||
} __attribute__((packed)) HDLCHeader;
|
||||
|
||||
typedef struct HDLCFooter {
|
||||
uint16_t fcs;
|
||||
uint8_t flag;
|
||||
} __attribute__((packed)) HDLCFooter;
|
||||
|
||||
typedef struct HDLC3CtrlHcs {
|
||||
uint8_t control;
|
||||
uint16_t hcs;
|
||||
} __attribute__((packed)) HDLC3CtrlHcs;
|
||||
|
||||
typedef struct HDLCLLC {
|
||||
uint8_t dst;
|
||||
uint8_t src;
|
||||
uint8_t control;
|
||||
} __attribute__((packed)) HDLCLLC;
|
||||
|
||||
typedef struct HDLCADPU {
|
||||
uint8_t flag;
|
||||
uint32_t id;
|
||||
} __attribute__((packed)) HDLCADPU;
|
||||
|
||||
typedef struct MbusHeader {
|
||||
uint8_t flag1;
|
||||
uint8_t len1;
|
||||
uint8_t len2;
|
||||
uint8_t flag2;
|
||||
} __attribute__((packed)) MbusHeader;
|
||||
|
||||
typedef struct MbusFooter {
|
||||
uint8_t fcs;
|
||||
uint8_t flag;
|
||||
} __attribute__((packed)) MbusFooter;
|
||||
|
||||
|
||||
// Blue book, Table 2
|
||||
enum CosemType {
|
||||
CosemTypeNull = 0x00,
|
||||
CosemTypeArray = 0x01,
|
||||
CosemTypeStructure = 0x02,
|
||||
CosemTypeOctetString = 0x09,
|
||||
CosemTypeString = 0x0A,
|
||||
CosemTypeDLongUnsigned = 0x06,
|
||||
CosemTypeLongSigned = 0x10,
|
||||
CosemTypeLongUnsigned = 0x12,
|
||||
CosemTypeDateTime = 0x19
|
||||
};
|
||||
|
||||
struct CosemBasic {
|
||||
uint8_t type;
|
||||
uint8_t length;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CosemString {
|
||||
uint8_t type;
|
||||
uint8_t length;
|
||||
uint8_t data[];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CosemLongUnsigned {
|
||||
uint8_t type;
|
||||
uint16_t data;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CosemDLongUnsigned {
|
||||
uint8_t type;
|
||||
uint32_t data;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CosemLongSigned {
|
||||
uint8_t type;
|
||||
int16_t data;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct CosemDateTime {
|
||||
uint8_t type;
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t dayOfMonth;
|
||||
uint8_t dayOfWeek;
|
||||
uint8_t hour;
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
uint8_t hundredths;
|
||||
int16_t deviation;
|
||||
uint8_t status;
|
||||
} __attribute__((packed));
|
||||
|
||||
typedef union {
|
||||
struct CosemBasic base;
|
||||
struct CosemString str;
|
||||
struct CosemString oct;
|
||||
struct CosemLongUnsigned lu;
|
||||
struct CosemDLongUnsigned dlu;
|
||||
struct CosemLongSigned ls;
|
||||
struct CosemDateTime dt;
|
||||
} CosemData;
|
||||
|
||||
void mbus_hexdump(const uint8_t* buf, int len);
|
||||
int HDLC_validate(const uint8_t* d, int len, HDLCConfig* config, CosemDateTime* timestamp);
|
||||
|
||||
uint8_t mbusChecksum(const uint8_t* p, int len);
|
||||
|
||||
#endif
|
||||
76
src/entsoe/DnbCurrParser.cpp
Normal file
76
src/entsoe/DnbCurrParser.cpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#include "DnbCurrParser.h"
|
||||
#include "Arduino.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
float DnbCurrParser::getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
int DnbCurrParser::available() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int DnbCurrParser::read() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int DnbCurrParser::peek() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DnbCurrParser::flush() {
|
||||
|
||||
}
|
||||
|
||||
size_t DnbCurrParser::write(const uint8_t *buffer, size_t size) {
|
||||
for(int i = 0; i < size; i++) {
|
||||
write(buffer[i]);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t DnbCurrParser::write(uint8_t byte) {
|
||||
if(pos == 0) {
|
||||
if(byte == '<') {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
if(strncmp(buf, "<Series", 7) == 0) {
|
||||
for(int i = 0; i < pos; i++) {
|
||||
if(strncmp(buf+i, "UNIT_MULT=\"", 11) == 0) {
|
||||
pos = i + 11;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < 16; i++) {
|
||||
uint8_t b = buf[pos+i];
|
||||
if(b == '"') {
|
||||
buf[pos+i] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
scale = String(buf+pos).toInt();
|
||||
} else if(strncmp(buf, "<Obs", 4) == 0) {
|
||||
for(int i = 0; i < pos; i++) {
|
||||
if(strncmp(buf+i, "OBS_VALUE=\"", 11) == 0) {
|
||||
pos = i + 11;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < 16; i++) {
|
||||
uint8_t b = buf[pos+i];
|
||||
if(b == '"') {
|
||||
buf[pos+i] = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
value = String(buf+pos).toFloat() / pow(10, scale);
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
26
src/entsoe/DnbCurrParser.h
Normal file
26
src/entsoe/DnbCurrParser.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef _DNBCURRPARSER_H
|
||||
#define _DNBCURRPARSER_H
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
class DnbCurrParser: public Stream {
|
||||
public:
|
||||
float getValue();
|
||||
|
||||
int available();
|
||||
int read();
|
||||
int peek();
|
||||
void flush();
|
||||
size_t write(const uint8_t *buffer, size_t size);
|
||||
size_t write(uint8_t);
|
||||
|
||||
private:
|
||||
uint8_t scale = 0;
|
||||
float value = 1.0;
|
||||
|
||||
char buf[64];
|
||||
uint8_t pos = 0;
|
||||
uint8_t mode = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
102
src/entsoe/EntsoeA44Parser.cpp
Normal file
102
src/entsoe/EntsoeA44Parser.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "HardwareSerial.h"
|
||||
|
||||
EntsoeA44Parser::EntsoeA44Parser() {
|
||||
for(int i = 0; i < 24; i++) points[i] = ENTSOE_NO_VALUE;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
char* EntsoeA44Parser::getMeasurementUnit() {
|
||||
return measurementUnit;
|
||||
}
|
||||
|
||||
float EntsoeA44Parser::getPoint(uint8_t position) {
|
||||
return points[position];
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::available() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::read() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int EntsoeA44Parser::peek() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void EntsoeA44Parser::flush() {
|
||||
|
||||
}
|
||||
|
||||
size_t EntsoeA44Parser::write(const uint8_t *buffer, size_t size) {
|
||||
for(int i = 0; i < size; i++) {
|
||||
write(buffer[i]);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t EntsoeA44Parser::write(uint8_t byte) {
|
||||
if(docPos == DOCPOS_CURRENCY) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(currency, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
} else if(docPos == DOCPOS_MEASUREMENTUNIT) {
|
||||
buf[pos++] = byte;
|
||||
if(pos == 3) {
|
||||
buf[pos++] = '\0';
|
||||
memcpy(measurementUnit, buf, pos);
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
}
|
||||
} else if(docPos == DOCPOS_POSITION) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
pointNum = String(buf).toInt() - 1;
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(docPos == DOCPOS_AMOUNT) {
|
||||
if(byte == '<') {
|
||||
buf[pos] = '\0';
|
||||
points[pointNum] = String(buf).toFloat();
|
||||
docPos = DOCPOS_SEEK;
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else {
|
||||
if(pos == 0) {
|
||||
if(byte == '<') {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
} else if(byte == '>') {
|
||||
buf[pos++] = byte;
|
||||
buf[pos] = '\0';
|
||||
if(strcmp(buf, "<currency_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_CURRENCY;
|
||||
} else if(strcmp(buf, "<price_Measure_Unit.name>") == 0) {
|
||||
docPos = DOCPOS_MEASUREMENTUNIT;
|
||||
} else if(strcmp(buf, "<position>") == 0) {
|
||||
docPos = DOCPOS_POSITION;
|
||||
pointNum = 0xFF;
|
||||
} else if(strcmp(buf, "<price.amount>") == 0) {
|
||||
docPos = DOCPOS_AMOUNT;
|
||||
}
|
||||
pos = 0;
|
||||
} else {
|
||||
buf[pos++] = byte;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
40
src/entsoe/EntsoeA44Parser.h
Normal file
40
src/entsoe/EntsoeA44Parser.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef _ENTSOEA44PARSER_H
|
||||
#define _ENTSOEA44PARSER_H
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
#define DOCPOS_SEEK 0
|
||||
#define DOCPOS_CURRENCY 1
|
||||
#define DOCPOS_MEASUREMENTUNIT 2
|
||||
#define DOCPOS_POSITION 3
|
||||
#define DOCPOS_AMOUNT 4
|
||||
|
||||
#define ENTSOE_NO_VALUE -127
|
||||
|
||||
class EntsoeA44Parser: public Stream {
|
||||
public:
|
||||
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);
|
||||
|
||||
private:
|
||||
char currency[4];
|
||||
char measurementUnit[4];
|
||||
float points[24];
|
||||
|
||||
char buf[64];
|
||||
uint8_t pos = 0;
|
||||
uint8_t docPos = 0;
|
||||
uint8_t pointNum = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
302
src/entsoe/EntsoeApi.cpp
Normal file
302
src/entsoe/EntsoeApi.cpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#include "EntsoeApi.h"
|
||||
#include <EEPROM.h>
|
||||
#include "Uptime.h"
|
||||
#include "TimeLib.h"
|
||||
#include "DnbCurrParser.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
#include <esp_task_wdt.h>
|
||||
#endif
|
||||
|
||||
EntsoeApi::EntsoeApi(RemoteDebug* Debug) {
|
||||
debugger = Debug;
|
||||
|
||||
client.setInsecure();
|
||||
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
|
||||
// Entso-E uses CET/CEST
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
tz = new Timezone(CEST, CET);
|
||||
}
|
||||
|
||||
void EntsoeApi::setup(EntsoeConfig& config) {
|
||||
if(this->config == NULL) {
|
||||
this->config = new EntsoeConfig();
|
||||
}
|
||||
memcpy(this->config, &config, sizeof(config));
|
||||
lastCurrencyFetch = 0;
|
||||
}
|
||||
|
||||
char* EntsoeApi::getToken() {
|
||||
return this->config->token;
|
||||
}
|
||||
|
||||
char* EntsoeApi::getCurrency() {
|
||||
return this->config->currency;
|
||||
}
|
||||
|
||||
float EntsoeApi::getValueForHour(uint8_t hour) {
|
||||
time_t cur = time(nullptr);
|
||||
return getValueForHour(cur, hour);
|
||||
}
|
||||
|
||||
float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
|
||||
tmElements_t tm;
|
||||
if(tz != NULL)
|
||||
cur = tz->toLocal(cur);
|
||||
breakTime(cur, tm);
|
||||
int pos = tm.Hour + hour;
|
||||
if(pos >= 48)
|
||||
return ENTSOE_NO_VALUE;
|
||||
|
||||
double value = ENTSOE_NO_VALUE;
|
||||
double multiplier = config->multiplier / 1000.0;
|
||||
if(pos > 23) {
|
||||
if(tomorrow == NULL)
|
||||
return ENTSOE_NO_VALUE;
|
||||
value = tomorrow->getPoint(pos-24);
|
||||
if(strcmp(tomorrow->getMeasurementUnit(), "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return ENTSOE_NO_VALUE;
|
||||
}
|
||||
multiplier *= getCurrencyMultiplier(tomorrow->getCurrency(), config->currency);
|
||||
} else {
|
||||
if(today == NULL)
|
||||
return ENTSOE_NO_VALUE;
|
||||
value = today->getPoint(pos);
|
||||
if(strcmp(today->getMeasurementUnit(), "MWH") == 0) {
|
||||
multiplier *= 0.001;
|
||||
} else {
|
||||
return ENTSOE_NO_VALUE;
|
||||
}
|
||||
multiplier *= getCurrencyMultiplier(today->getCurrency(), config->currency);
|
||||
}
|
||||
return value * multiplier;
|
||||
}
|
||||
|
||||
bool EntsoeApi::loop() {
|
||||
if(strlen(getToken()) == 0)
|
||||
return false;
|
||||
|
||||
uint64_t now = millis64();
|
||||
if(now < 10000) return false; // Grace period
|
||||
|
||||
if(midnightMillis == 0) {
|
||||
time_t t = time(nullptr);
|
||||
if(t <= 0) return false; // NTP not ready
|
||||
|
||||
time_t epoch = tz->toLocal(t);
|
||||
|
||||
tmElements_t tm;
|
||||
breakTime(epoch, tm);
|
||||
if(tm.Year > 50) { // Make sure we are in 2021 or later (years after 1970)
|
||||
uint32_t curDayMillis = (((((tm.Hour * 60) + tm.Minute) * 60) + tm.Second) * 1000);
|
||||
|
||||
midnightMillis = now + (SECS_PER_DAY * 1000) - curDayMillis + 1000; // Adding 1s to ensure we have passed midnight
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Setting midnight millis %lu\n", midnightMillis);
|
||||
}
|
||||
} else if(now > midnightMillis) {
|
||||
time_t t = time(nullptr);
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Rotating price objects at %lu\n", t);
|
||||
delete today;
|
||||
today = tomorrow;
|
||||
tomorrow = NULL;
|
||||
midnightMillis = 0; // Force new midnight millis calculation
|
||||
} else {
|
||||
if(today == NULL && (lastTodayFetch == 0 || now - lastTodayFetch > 60000)) {
|
||||
lastTodayFetch = now;
|
||||
time_t e1 = time(nullptr) - (SECS_PER_DAY * 1);
|
||||
time_t e2 = e1 + SECS_PER_DAY;
|
||||
tmElements_t d1, d2;
|
||||
breakTime(e1, d1);
|
||||
breakTime(e2, d2);
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s",
|
||||
"https://transparency.entsoe.eu/api", getToken(),
|
||||
d1.Year+1970, d1.Month, d1.Day, 23, 00,
|
||||
d2.Year+1970, d2.Month, d2.Day, 23, 00,
|
||||
config->area, config->area);
|
||||
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
#elif defined(ESP8266)
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for today\n");
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
|
||||
EntsoeA44Parser* a44 = new EntsoeA44Parser();
|
||||
if(retrieve(url, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
|
||||
today = a44;
|
||||
return true;
|
||||
} else if(a44 != NULL) {
|
||||
delete a44;
|
||||
today = NULL;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(tomorrow == NULL
|
||||
&& midnightMillis - now < 39600000 // Fetch 11hrs before midnight (13:00 CE(S)T)
|
||||
&& (lastTomorrowFetch == 0 || now - lastTomorrowFetch > 300000) // Retry every 5min
|
||||
) {
|
||||
lastTomorrowFetch = now;
|
||||
time_t e1 = time(nullptr);
|
||||
time_t e2 = e1 + SECS_PER_DAY;
|
||||
tmElements_t d1, d2;
|
||||
breakTime(e1, d1);
|
||||
breakTime(e2, d2);
|
||||
|
||||
char url[256];
|
||||
snprintf(url, sizeof(url), "%s?securityToken=%s&documentType=A44&periodStart=%04d%02d%02d%02d%02d&periodEnd=%04d%02d%02d%02d%02d&in_Domain=%s&out_Domain=%s",
|
||||
"https://transparency.entsoe.eu/api", getToken(),
|
||||
d1.Year+1970, d1.Month, d1.Day, 23, 00,
|
||||
d2.Year+1970, d2.Month, d2.Day, 23, 00,
|
||||
config->area, config->area);
|
||||
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
#elif defined(ESP8266)
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Fetching prices for tomorrow\n");
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
|
||||
EntsoeA44Parser* a44 = new EntsoeA44Parser();
|
||||
if(retrieve(url, a44) && a44->getPoint(0) != ENTSOE_NO_VALUE) {
|
||||
tomorrow = a44;
|
||||
return true;
|
||||
} else if(a44 != NULL) {
|
||||
delete a44;
|
||||
tomorrow = NULL;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EntsoeApi::retrieve(const char* url, Stream* doc) {
|
||||
#if defined(ESP8266)
|
||||
// https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/bearssl-client-secure-class.html#mfln-or-maximum-fragment-length-negotiation-saving-ram
|
||||
/* Rumor has it that a client cannot request a lower max_fragment_length, so I guess thats why the following does not work.
|
||||
And there is currently not enough heap space to go around in this project to do a full HTTPS request on ESP8266
|
||||
|
||||
int bufSize = 512;
|
||||
while(!client.probeMaxFragmentLength("transparency.entsoe.eu", 443, bufSize) && bufSize <= 4096) {
|
||||
bufSize += 512;
|
||||
}
|
||||
if(client.probeMaxFragmentLength("transparency.entsoe.eu", 443, bufSize)) {
|
||||
printD("Negotiated MFLN size");
|
||||
printD(String(bufSize));
|
||||
client.setBufferSizes(bufSize, bufSize);
|
||||
}
|
||||
*/
|
||||
#endif
|
||||
|
||||
if(https.begin(client, url)) {
|
||||
printD("Connection established");
|
||||
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
#elif defined(ESP8266)
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
|
||||
/*
|
||||
#if defined(ESP8266)
|
||||
if(!client.getMFLNStatus()) {
|
||||
printE("Negotiated MFLN was not respected");
|
||||
https.end();
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
*/
|
||||
|
||||
int status = https.GET();
|
||||
|
||||
#if defined(ESP32)
|
||||
esp_task_wdt_reset();
|
||||
#elif defined(ESP8266)
|
||||
ESP.wdtFeed();
|
||||
#endif
|
||||
|
||||
if(status == HTTP_CODE_OK) {
|
||||
printD("Receiving data");
|
||||
https.writeToStream(doc);
|
||||
https.end();
|
||||
return true;
|
||||
} else {
|
||||
printE("Communication error: ");
|
||||
printE(https.errorToString(status));
|
||||
printD(https.getString());
|
||||
|
||||
#if defined(ESP8266)
|
||||
char buf[64];
|
||||
client.getLastSSLError(buf,64);
|
||||
printE(buf);
|
||||
#endif
|
||||
|
||||
https.end();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
#if defined(ESP8266)
|
||||
char buf[64];
|
||||
client.getLastSSLError(buf,64);
|
||||
printE(buf);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
client.stop();
|
||||
}
|
||||
|
||||
float EntsoeApi::getCurrencyMultiplier(const char* from, const char* to) {
|
||||
if(strcmp(from, to) == 0)
|
||||
return 1.00;
|
||||
|
||||
uint64_t now = millis64();
|
||||
if(lastCurrencyFetch == 0 || now - lastCurrencyFetch > (SECS_PER_HOUR * 1000)) {
|
||||
char url[256];
|
||||
DnbCurrParser p;
|
||||
|
||||
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", from);
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", from);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
|
||||
if(retrieve(url, &p)) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
|
||||
currencyMultiplier = p.getValue();
|
||||
if(strncmp(to, "NOK", 3) != 0) {
|
||||
snprintf(url, sizeof(url), "https://data.norges-bank.no/api/data/EXR/M.%s.NOK.SP?lastNObservations=1", to);
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EntsoeApi) Retrieving %s to NOK conversion\n", to);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) url: %s\n", url);
|
||||
if(retrieve(url, &p)) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) got exchange rate %.4f\n", p.getValue());
|
||||
currencyMultiplier /= p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EntsoeApi) Resulting currency multiplier: %.4f\n", currencyMultiplier);
|
||||
lastCurrencyFetch = now;
|
||||
}
|
||||
return currencyMultiplier;
|
||||
}
|
||||
|
||||
void EntsoeApi::printD(String fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void EntsoeApi::printE(String fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(EntsoeApi)" + fmt + "\n").c_str(), args);
|
||||
va_end(args);
|
||||
}
|
||||
55
src/entsoe/EntsoeApi.h
Normal file
55
src/entsoe/EntsoeApi.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef _ENTSOEAPI_H
|
||||
#define _ENTSOEAPI_H
|
||||
|
||||
#include "TimeLib.h"
|
||||
#include "Timezone.h"
|
||||
#include "RemoteDebug.h"
|
||||
#include "EntsoeA44Parser.h"
|
||||
#include "AmsConfiguration.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
|
||||
#include <HTTPClient.h>
|
||||
#else
|
||||
#warning "Unsupported board type"
|
||||
#endif
|
||||
|
||||
#define ENTSOE_DEFAULT_MULTIPLIER 1.00
|
||||
#define SSL_BUF_SIZE 512
|
||||
|
||||
class EntsoeApi {
|
||||
public:
|
||||
EntsoeApi(RemoteDebug*);
|
||||
void setup(EntsoeConfig&);
|
||||
bool loop();
|
||||
|
||||
char* getToken();
|
||||
char* getCurrency();
|
||||
float getValueForHour(uint8_t);
|
||||
float getValueForHour(time_t, uint8_t);
|
||||
|
||||
private:
|
||||
RemoteDebug* debugger;
|
||||
EntsoeConfig* config = NULL;
|
||||
WiFiClientSecure client;
|
||||
HTTPClient https;
|
||||
|
||||
uint64_t midnightMillis = 0;
|
||||
uint64_t lastTodayFetch = 0;
|
||||
uint64_t lastTomorrowFetch = 0;
|
||||
uint64_t lastCurrencyFetch = 0;
|
||||
EntsoeA44Parser* today = NULL;
|
||||
EntsoeA44Parser* tomorrow = NULL;
|
||||
|
||||
Timezone* tz = NULL;
|
||||
|
||||
float currencyMultiplier = ENTSOE_DEFAULT_MULTIPLIER;
|
||||
|
||||
bool retrieve(const char* url, Stream* doc);
|
||||
float getCurrencyMultiplier(const char* from, const char* to);
|
||||
|
||||
void printD(String fmt, ...);
|
||||
void printE(String fmt, ...);
|
||||
};
|
||||
#endif
|
||||
23
src/hexutils.cpp
Normal file
23
src/hexutils.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "hexutils.h"
|
||||
|
||||
String toHex(uint8_t* in) {
|
||||
return toHex(in, sizeof(in)*2);
|
||||
}
|
||||
|
||||
String toHex(uint8_t* in, uint8_t size) {
|
||||
String hex;
|
||||
for(int i = 0; i < size; i++) {
|
||||
if(in[i] < 0x10) {
|
||||
hex += '0';
|
||||
}
|
||||
hex += String(in[i], HEX);
|
||||
}
|
||||
hex.toUpperCase();
|
||||
return hex;
|
||||
}
|
||||
|
||||
void fromHex(uint8_t *out, String in, uint8_t size) {
|
||||
for(int i = 0; i < size*2; i += 2) {
|
||||
out[i/2] = strtol(in.substring(i, i+2).c_str(), 0, 16);
|
||||
}
|
||||
}
|
||||
11
src/hexutils.h
Normal file
11
src/hexutils.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#ifndef _HEXUTILS_H
|
||||
#define _HEXUTILS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "Arduino.h"
|
||||
|
||||
String toHex(uint8_t* in);
|
||||
String toHex(uint8_t* in, uint8_t size);
|
||||
void fromHex(uint8_t *out, String in, uint8_t size);
|
||||
|
||||
#endif
|
||||
26
src/mqtt/AmsMqttHandler.h
Normal file
26
src/mqtt/AmsMqttHandler.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#ifndef _AMSMQTTHANDLER_H
|
||||
#define _AMSMQTTHANDLER_H
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <MQTT.h>
|
||||
#include "AmsData.h"
|
||||
#include "AmsConfiguration.h"
|
||||
#include "HwTools.h"
|
||||
#include "entsoe/EntsoeApi.h"
|
||||
|
||||
class AmsMqttHandler {
|
||||
public:
|
||||
AmsMqttHandler(MQTTClient* mqtt) {
|
||||
this->mqtt = mqtt;
|
||||
};
|
||||
|
||||
virtual bool publish(AmsData* data, AmsData* previousState);
|
||||
virtual bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
virtual bool publishPrices(EntsoeApi* eapi);
|
||||
virtual bool publishSystem(HwTools*);
|
||||
|
||||
protected:
|
||||
MQTTClient* mqtt;
|
||||
};
|
||||
|
||||
#endif
|
||||
81
src/mqtt/DomoticzMqttHandler.cpp
Normal file
81
src/mqtt/DomoticzMqttHandler.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "DomoticzMqttHandler.h"
|
||||
#include "web/root/domoticz_json.h"
|
||||
|
||||
bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState) {
|
||||
bool ret = false;
|
||||
if (config.elidx > 0) {
|
||||
if(data->getActiveImportCounter() > 1.0) {
|
||||
energy = data->getActiveImportCounter();
|
||||
}
|
||||
if(energy > 0.0) {
|
||||
char val[16];
|
||||
snprintf(val, 16, "%.1f;%.1f", (data->getActiveImportPower()/1.0), energy*1000.0);
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
|
||||
config.elidx,
|
||||
val
|
||||
);
|
||||
ret = mqtt->publish("domoticz/in", json);
|
||||
}
|
||||
}
|
||||
|
||||
if(data->getListType() == 1)
|
||||
return ret;
|
||||
|
||||
if (config.vl1idx > 0){
|
||||
char val[16];
|
||||
snprintf(val, 16, "%.2f", data->getL1Voltage());
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
|
||||
config.vl1idx,
|
||||
val
|
||||
);
|
||||
ret |= mqtt->publish("domoticz/in", json);
|
||||
}
|
||||
|
||||
if (config.vl2idx > 0){
|
||||
char val[16];
|
||||
snprintf(val, 16, "%.2f", data->getL2Voltage());
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
|
||||
config.vl2idx,
|
||||
val
|
||||
);
|
||||
ret |= mqtt->publish("domoticz/in", json);
|
||||
}
|
||||
|
||||
if (config.vl3idx > 0){
|
||||
char val[16];
|
||||
snprintf(val, 16, "%.2f", data->getL3Voltage());
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
|
||||
config.vl3idx,
|
||||
val
|
||||
);
|
||||
ret |= mqtt->publish("domoticz/in", json);
|
||||
}
|
||||
|
||||
if (config.cl1idx > 0){
|
||||
char val[16];
|
||||
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
|
||||
config.cl1idx,
|
||||
val
|
||||
);
|
||||
ret |= mqtt->publish("domoticz/in", json);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool DomoticzMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DomoticzMqttHandler::publishPrices(EntsoeApi* eapi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DomoticzMqttHandler::publishSystem(HwTools* hw) {
|
||||
return false;
|
||||
}
|
||||
21
src/mqtt/DomoticzMqttHandler.h
Normal file
21
src/mqtt/DomoticzMqttHandler.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef _DOMOTICZMQTTHANDLER_H
|
||||
#define _DOMOTICZMQTTHANDLER_H
|
||||
|
||||
#include "AmsMqttHandler.h"
|
||||
#include "AmsConfiguration.h"
|
||||
|
||||
class DomoticzMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
DomoticzMqttHandler(MQTTClient* mqtt, DomoticzConfig config) : AmsMqttHandler(mqtt) {
|
||||
this->config = config;
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(EntsoeApi*);
|
||||
bool publishSystem(HwTools*);
|
||||
|
||||
private:
|
||||
DomoticzConfig config;
|
||||
int energy = 0.0;
|
||||
};
|
||||
#endif
|
||||
271
src/mqtt/JsonMqttHandler.cpp
Normal file
271
src/mqtt/JsonMqttHandler.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
#include "JsonMqttHandler.h"
|
||||
#include "hexutils.h"
|
||||
#include "Uptime.h"
|
||||
#include "web/root/json1_json.h"
|
||||
#include "web/root/json2_json.h"
|
||||
#include "web/root/json3_json.h"
|
||||
#include "web/root/json3pf_json.h"
|
||||
#include "web/root/jsonsys_json.h"
|
||||
#include "web/root/jsonprices_json.h"
|
||||
|
||||
bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
|
||||
if(data->getListType() == 1) {
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), JSON1_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
clientId.c_str(),
|
||||
(uint32_t) (millis64()/1000),
|
||||
data->getPackageTimestamp(),
|
||||
hw->getVcc(),
|
||||
hw->getWifiRssi(),
|
||||
hw->getTemperature(),
|
||||
data->getActiveImportPower()
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
} else if(data->getListType() == 2) {
|
||||
char json[384];
|
||||
snprintf_P(json, sizeof(json), JSON2_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
clientId.c_str(),
|
||||
(uint32_t) (millis64()/1000),
|
||||
data->getPackageTimestamp(),
|
||||
hw->getVcc(),
|
||||
hw->getWifiRssi(),
|
||||
hw->getTemperature(),
|
||||
data->getListId().c_str(),
|
||||
data->getMeterId().c_str(),
|
||||
data->getMeterModel().c_str(),
|
||||
data->getActiveImportPower(),
|
||||
data->getReactiveImportPower(),
|
||||
data->getActiveExportPower(),
|
||||
data->getReactiveExportPower(),
|
||||
data->getL1Current(),
|
||||
data->getL2Current(),
|
||||
data->getL3Current(),
|
||||
data->getL1Voltage(),
|
||||
data->getL2Voltage(),
|
||||
data->getL3Voltage()
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
} else if(data->getListType() == 3) {
|
||||
if(data->getPowerFactor() == 0) {
|
||||
char json[512];
|
||||
snprintf_P(json, sizeof(json), JSON3_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
clientId.c_str(),
|
||||
(uint32_t) (millis64()/1000),
|
||||
data->getPackageTimestamp(),
|
||||
hw->getVcc(),
|
||||
hw->getWifiRssi(),
|
||||
hw->getTemperature(),
|
||||
data->getListId().c_str(),
|
||||
data->getMeterId().c_str(),
|
||||
data->getMeterModel().c_str(),
|
||||
data->getActiveImportPower(),
|
||||
data->getReactiveImportPower(),
|
||||
data->getActiveExportPower(),
|
||||
data->getReactiveExportPower(),
|
||||
data->getL1Current(),
|
||||
data->getL2Current(),
|
||||
data->getL3Current(),
|
||||
data->getL1Voltage(),
|
||||
data->getL2Voltage(),
|
||||
data->getL3Voltage(),
|
||||
data->getActiveImportCounter(),
|
||||
data->getActiveExportCounter(),
|
||||
data->getReactiveImportCounter(),
|
||||
data->getReactiveExportCounter(),
|
||||
data->getMeterTimestamp()
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
} else {
|
||||
char json[768];
|
||||
snprintf_P(json, sizeof(json), JSON3PF_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
clientId.c_str(),
|
||||
(uint32_t) (millis64()/1000),
|
||||
data->getPackageTimestamp(),
|
||||
hw->getVcc(),
|
||||
hw->getWifiRssi(),
|
||||
hw->getTemperature(),
|
||||
data->getListId().c_str(),
|
||||
data->getMeterId().c_str(),
|
||||
data->getMeterModel().c_str(),
|
||||
data->getActiveImportPower(),
|
||||
data->getReactiveImportPower(),
|
||||
data->getActiveExportPower(),
|
||||
data->getReactiveExportPower(),
|
||||
data->getL1Current(),
|
||||
data->getL2Current(),
|
||||
data->getL3Current(),
|
||||
data->getL1Voltage(),
|
||||
data->getL2Voltage(),
|
||||
data->getL3Voltage(),
|
||||
data->getPowerFactor(),
|
||||
data->getL1PowerFactor(),
|
||||
data->getL2PowerFactor(),
|
||||
data->getL3PowerFactor(),
|
||||
data->getActiveImportCounter(),
|
||||
data->getActiveExportCounter(),
|
||||
data->getReactiveImportCounter(),
|
||||
data->getReactiveExportCounter(),
|
||||
data->getMeterTimestamp()
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
|
||||
int count = hw->getTempSensorCount();
|
||||
if(count == 0)
|
||||
return false;
|
||||
|
||||
int size = 32 + (count * 26);
|
||||
|
||||
char buf[size];
|
||||
snprintf(buf, 24, "{\"temperatures\":{");
|
||||
|
||||
for(int i = 0; i < count; i++) {
|
||||
TempSensorData* data = hw->getTempSensorData(i);
|
||||
if(data != NULL) {
|
||||
char* pos = buf+strlen(buf);
|
||||
snprintf(pos, 26, "\"%s\":%.2f,",
|
||||
toHex(data->address, 8).c_str(),
|
||||
data->lastRead
|
||||
);
|
||||
data->changed = false;
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
char* pos = buf+strlen(buf);
|
||||
snprintf(count == 0 ? pos : pos-1, 8, "}}");
|
||||
return mqtt->publish(topic, buf);
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
if(strlen(eapi->getToken()) == 0)
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
|
||||
float min1hr, min3hr, min6hr;
|
||||
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
|
||||
float min = INT16_MAX, max = INT16_MIN;
|
||||
float values[24] = {0};
|
||||
for(uint8_t i = 0; i < 24; i++) {
|
||||
float val = eapi->getValueForHour(now, i);
|
||||
values[i] = val;
|
||||
|
||||
if(val == ENTSOE_NO_VALUE) break;
|
||||
|
||||
if(val < min) min = val;
|
||||
if(val > max) max = val;
|
||||
|
||||
if(min1hrIdx == -1 || min1hr > val) {
|
||||
min1hr = val;
|
||||
min1hrIdx = i;
|
||||
}
|
||||
|
||||
if(i >= 2) {
|
||||
i -= 2;
|
||||
float val1 = values[i++];
|
||||
float val2 = values[i++];
|
||||
float val3 = val;
|
||||
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
|
||||
float val3hr = val1+val2+val3;
|
||||
if(min3hrIdx == -1 || min3hr > val3hr) {
|
||||
min3hr = val3hr;
|
||||
min3hrIdx = i-2;
|
||||
}
|
||||
}
|
||||
|
||||
if(i >= 5) {
|
||||
i -= 5;
|
||||
float val1 = values[i++];
|
||||
float val2 = values[i++];
|
||||
float val3 = values[i++];
|
||||
float val4 = values[i++];
|
||||
float val5 = values[i++];
|
||||
float val6 = val;
|
||||
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
|
||||
float val6hr = val1+val2+val3+val4+val5+val6;
|
||||
if(min6hrIdx == -1 || min6hr > val6hr) {
|
||||
min6hr = val6hr;
|
||||
min6hrIdx = i-5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
char ts1hr[21];
|
||||
if(min1hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
|
||||
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts3hr[21];
|
||||
if(min3hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
|
||||
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts6hr[21];
|
||||
if(min6hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
|
||||
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
char json[384];
|
||||
snprintf_P(json, sizeof(json), JSONPRICES_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
values[0],
|
||||
values[1],
|
||||
values[2],
|
||||
values[3],
|
||||
values[4],
|
||||
values[5],
|
||||
values[6],
|
||||
values[7],
|
||||
values[8],
|
||||
values[9],
|
||||
values[10],
|
||||
values[11],
|
||||
min == INT16_MAX ? 0.0 : min,
|
||||
max == INT16_MIN ? 0.0 : max,
|
||||
ts1hr,
|
||||
ts3hr,
|
||||
ts6hr
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishSystem(HwTools* hw) {
|
||||
if(init || topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
|
||||
char json[192];
|
||||
snprintf_P(json, sizeof(json), JSONSYS_JSON,
|
||||
WiFi.macAddress().c_str(),
|
||||
clientId.c_str(),
|
||||
(uint32_t) (millis64()/1000),
|
||||
hw->getVcc(),
|
||||
hw->getWifiRssi(),
|
||||
hw->getTemperature()
|
||||
);
|
||||
init = mqtt->publish(topic, json);
|
||||
return init;
|
||||
}
|
||||
24
src/mqtt/JsonMqttHandler.h
Normal file
24
src/mqtt/JsonMqttHandler.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef _JSONMQTTHANDLER_H
|
||||
#define _JSONMQTTHANDLER_H
|
||||
|
||||
#include "AmsMqttHandler.h"
|
||||
|
||||
class JsonMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
JsonMqttHandler(MQTTClient* mqtt, const char* clientId, const char* topic, HwTools* hw) : AmsMqttHandler(mqtt) {
|
||||
this->clientId = clientId;
|
||||
this->topic = String(topic);
|
||||
this->hw = hw;
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(EntsoeApi*);
|
||||
bool publishSystem(HwTools*);
|
||||
|
||||
private:
|
||||
String clientId;
|
||||
String topic;
|
||||
HwTools* hw;
|
||||
bool init = false;
|
||||
};
|
||||
#endif
|
||||
219
src/mqtt/RawMqttHandler.cpp
Normal file
219
src/mqtt/RawMqttHandler.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "RawMqttHandler.h"
|
||||
#include "hexutils.h"
|
||||
#include "Uptime.h"
|
||||
|
||||
bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
|
||||
if(data->getPackageTimestamp() > 0) {
|
||||
mqtt->publish(topic + "/meter/dlms/timestamp", String(data->getPackageTimestamp()));
|
||||
}
|
||||
switch(data->getListType()) {
|
||||
case 3:
|
||||
// ID and type belongs to List 2, but I see no need to send that every 10s
|
||||
mqtt->publish(topic + "/meter/id", data->getMeterId(), true, 0);
|
||||
mqtt->publish(topic + "/meter/type", data->getMeterModel(), true, 0);
|
||||
mqtt->publish(topic + "/meter/clock", String(data->getMeterTimestamp()));
|
||||
mqtt->publish(topic + "/meter/import/reactive/accumulated", String(data->getReactiveImportCounter(), 3), true, 0);
|
||||
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 3), true, 0);
|
||||
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 3), true, 0);
|
||||
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 3), true, 0);
|
||||
if(full || meterState->getPowerFactor() != data->getPowerFactor()) {
|
||||
mqtt->publish(topic + "/meter/powerfactor", String(data->getPowerFactor(), 2));
|
||||
}
|
||||
if(full || meterState->getL1PowerFactor() != data->getL1PowerFactor()) {
|
||||
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 2));
|
||||
}
|
||||
if(full || meterState->getL2PowerFactor() != data->getL2PowerFactor()) {
|
||||
mqtt->publish(topic + "/meter/l2/powerfactor", String(data->getL2PowerFactor(), 2));
|
||||
}
|
||||
if(full || meterState->getL3PowerFactor() != data->getL3PowerFactor()) {
|
||||
mqtt->publish(topic + "/meter/l3/powerfactor", String(data->getL3PowerFactor(), 2));
|
||||
}
|
||||
case 2:
|
||||
// Only send data if changed. ID and Type is sent on the 10s interval only if changed
|
||||
if(full || meterState->getMeterId() != data->getMeterId()) {
|
||||
mqtt->publish(topic + "/meter/id", data->getMeterId());
|
||||
}
|
||||
if(full || meterState->getMeterModel() != data->getMeterModel()) {
|
||||
mqtt->publish(topic + "/meter/type", data->getMeterModel());
|
||||
}
|
||||
if(full || meterState->getL1Current() != data->getL1Current()) {
|
||||
mqtt->publish(topic + "/meter/l1/current", String(data->getL1Current(), 2));
|
||||
}
|
||||
if(full || meterState->getL1Voltage() != data->getL1Voltage()) {
|
||||
mqtt->publish(topic + "/meter/l1/voltage", String(data->getL1Voltage(), 2));
|
||||
}
|
||||
if(full || meterState->getL2Current() != data->getL2Current()) {
|
||||
mqtt->publish(topic + "/meter/l2/current", String(data->getL2Current(), 2));
|
||||
}
|
||||
if(full || meterState->getL2Voltage() != data->getL2Voltage()) {
|
||||
mqtt->publish(topic + "/meter/l2/voltage", String(data->getL2Voltage(), 2));
|
||||
}
|
||||
if(full || meterState->getL3Current() != data->getL3Current()) {
|
||||
mqtt->publish(topic + "/meter/l3/current", String(data->getL3Current(), 2));
|
||||
}
|
||||
if(full || meterState->getL3Voltage() != data->getL3Voltage()) {
|
||||
mqtt->publish(topic + "/meter/l3/voltage", String(data->getL3Voltage(), 2));
|
||||
}
|
||||
if(full || meterState->getReactiveExportPower() != data->getReactiveExportPower()) {
|
||||
mqtt->publish(topic + "/meter/export/reactive", String(data->getReactiveExportPower()));
|
||||
}
|
||||
if(full || meterState->getActiveExportPower() != data->getActiveExportPower()) {
|
||||
mqtt->publish(topic + "/meter/export/active", String(data->getActiveExportPower()));
|
||||
}
|
||||
if(full || meterState->getReactiveImportPower() != data->getReactiveImportPower()) {
|
||||
mqtt->publish(topic + "/meter/import/reactive", String(data->getReactiveImportPower()));
|
||||
}
|
||||
case 1:
|
||||
if(full || meterState->getActiveImportPower() != data->getActiveImportPower()) {
|
||||
mqtt->publish(topic + "/meter/import/active", String(data->getActiveImportPower()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
|
||||
uint8_t c = hw->getTempSensorCount();
|
||||
for(int i = 0; i < c; i++) {
|
||||
TempSensorData* data = hw->getTempSensorData(i);
|
||||
if(data != NULL && data->lastValidRead > -85) {
|
||||
if(data->changed || full) {
|
||||
mqtt->publish(topic + "/temperature/" + toHex(data->address), String(data->lastValidRead, 2));
|
||||
data->changed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return c > 0;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishPrices(EntsoeApi* eapi) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
if(strcmp(eapi->getToken(), "") == 0)
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
|
||||
float min1hr, min3hr, min6hr;
|
||||
int8_t min1hrIdx = -1, min3hrIdx = -1, min6hrIdx = -1;
|
||||
float min = INT16_MAX, max = INT16_MIN;
|
||||
float values[34] = {0};
|
||||
for(uint8_t i = 0; i < 34; i++) {
|
||||
float val = eapi->getValueForHour(now, i);
|
||||
values[i] = val;
|
||||
|
||||
if(i > 23) continue;
|
||||
if(val == ENTSOE_NO_VALUE) break;
|
||||
|
||||
if(val < min) min = val;
|
||||
if(val > max) max = val;
|
||||
|
||||
if(min1hrIdx == -1 || min1hr > val) {
|
||||
min1hr = val;
|
||||
min1hrIdx = i;
|
||||
}
|
||||
|
||||
if(i >= 2) {
|
||||
i -= 2;
|
||||
float val1 = values[i++];
|
||||
float val2 = values[i++];
|
||||
float val3 = val;
|
||||
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE) continue;
|
||||
float val3hr = val1+val2+val3;
|
||||
if(min3hrIdx == -1 || min3hr > val3hr) {
|
||||
min3hr = val3hr;
|
||||
min3hrIdx = i-2;
|
||||
}
|
||||
}
|
||||
|
||||
if(i >= 5) {
|
||||
i -= 5;
|
||||
float val1 = values[i++];
|
||||
float val2 = values[i++];
|
||||
float val3 = values[i++];
|
||||
float val4 = values[i++];
|
||||
float val5 = values[i++];
|
||||
float val6 = val;
|
||||
if(val1 == ENTSOE_NO_VALUE || val2 == ENTSOE_NO_VALUE || val3 == ENTSOE_NO_VALUE || val4 == ENTSOE_NO_VALUE || val5 == ENTSOE_NO_VALUE || val6 == ENTSOE_NO_VALUE) continue;
|
||||
float val6hr = val1+val2+val3+val4+val5+val6;
|
||||
if(min6hrIdx == -1 || min6hr > val6hr) {
|
||||
min6hr = val6hr;
|
||||
min6hrIdx = i-5;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
char ts1hr[21];
|
||||
if(min1hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min1hrIdx);
|
||||
//Serial.printf("1hr: %d %lu\n", min1hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts1hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts3hr[21];
|
||||
if(min3hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min3hrIdx);
|
||||
//Serial.printf("3hr: %d %lu\n", min3hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts3hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
char ts6hr[21];
|
||||
if(min6hrIdx > -1) {
|
||||
time_t ts = now + (SECS_PER_HOUR * min6hrIdx);
|
||||
//Serial.printf("6hr: %d %lu\n", min6hrIdx, ts);
|
||||
tmElements_t tm;
|
||||
breakTime(ts, tm);
|
||||
sprintf(ts6hr, "%04d-%02d-%02dT%02d:00:00Z", tm.Year+1970, tm.Month, tm.Day, tm.Hour);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 34; i++) {
|
||||
float val = values[i];
|
||||
if(val == ENTSOE_NO_VALUE) {
|
||||
mqtt->publish(topic + "/price/" + String(i), "", true, 0);
|
||||
break;
|
||||
} else {
|
||||
mqtt->publish(topic + "/price/" + String(i), String(val, 4), true, 0);
|
||||
}
|
||||
mqtt->loop();
|
||||
delay(10);
|
||||
}
|
||||
if(min != INT16_MAX) {
|
||||
mqtt->publish(topic + "/price/min", String(min, 4), true, 0);
|
||||
}
|
||||
if(max != INT16_MIN) {
|
||||
mqtt->publish(topic + "/price/max", String(max, 4), true, 0);
|
||||
}
|
||||
|
||||
if(min1hrIdx != -1) {
|
||||
mqtt->publish(topic + "/price/cheapest/1hr", String(ts1hr), true, 0);
|
||||
}
|
||||
if(min3hrIdx != -1) {
|
||||
mqtt->publish(topic + "/price/cheapest/3hr", String(ts3hr), true, 0);
|
||||
}
|
||||
if(min6hrIdx != -1) {
|
||||
mqtt->publish(topic + "/price/cheapest/6hr", String(ts6hr), true, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RawMqttHandler::publishSystem(HwTools* hw) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
|
||||
mqtt->publish(topic + "/id", WiFi.macAddress(), true, 0);
|
||||
mqtt->publish(topic + "/uptime", String((unsigned long) millis64()/1000));
|
||||
float vcc = hw->getVcc();
|
||||
if(vcc > 0) {
|
||||
mqtt->publish(topic + "/vcc", String(vcc, 2));
|
||||
}
|
||||
mqtt->publish(topic + "/rssi", String(hw->getWifiRssi()));
|
||||
if(hw->getTemperature() > -85) {
|
||||
mqtt->publish(topic + "/temperature", String(hw->getTemperature(), 2));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
21
src/mqtt/RawMqttHandler.h
Normal file
21
src/mqtt/RawMqttHandler.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef _RAWMQTTHANDLER_H
|
||||
#define _RAWMQTTHANDLER_H
|
||||
|
||||
#include "AmsMqttHandler.h"
|
||||
|
||||
class RawMqttHandler : public AmsMqttHandler {
|
||||
public:
|
||||
RawMqttHandler(MQTTClient* mqtt, const char* topic, bool full) : AmsMqttHandler(mqtt) {
|
||||
this->topic = String(topic);
|
||||
this->full = full;
|
||||
};
|
||||
bool publish(AmsData* data, AmsData* previousState);
|
||||
bool publishTemperatures(AmsConfiguration*, HwTools*);
|
||||
bool publishPrices(EntsoeApi*);
|
||||
bool publishSystem(HwTools*);
|
||||
|
||||
private:
|
||||
String topic;
|
||||
bool full;
|
||||
};
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,42 +3,57 @@
|
||||
|
||||
#define BOOTSTRAP_URL "https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <MQTT.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "AmsConfiguration.h"
|
||||
#include "HwTools.h"
|
||||
#include "AmsData.h"
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
#include "AmsDataStorage.h"
|
||||
#include "Uptime.h"
|
||||
#include "RemoteDebug.h"
|
||||
#include "entsoe/EntsoeApi.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <HTTPClient.h>
|
||||
#else
|
||||
#warning "Unsupported board type"
|
||||
#endif
|
||||
|
||||
#include "LittleFS.h"
|
||||
|
||||
class AmsWebServer {
|
||||
public:
|
||||
void setup(AmsConfiguration* config, Stream* debugger, MQTTClient* mqtt);
|
||||
AmsWebServer(RemoteDebug* Debug, HwTools* hw);
|
||||
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*);
|
||||
void loop();
|
||||
|
||||
void setData(AmsData& data);
|
||||
void setMqtt(MQTTClient* mqtt);
|
||||
void setTimezone(Timezone* tz);
|
||||
void setMqttEnabled(bool);
|
||||
void setEntsoeApi(EntsoeApi* eapi);
|
||||
|
||||
private:
|
||||
int maxPwr;
|
||||
HwTools hw;
|
||||
AmsConfiguration* config;
|
||||
AmsData data;
|
||||
Stream* debugger;
|
||||
MQTTClient* mqtt;
|
||||
RemoteDebug* debugger;
|
||||
bool mqttEnabled = false;
|
||||
int maxPwr = 0;
|
||||
HwTools* hw;
|
||||
Timezone* tz;
|
||||
EntsoeApi* eapi = NULL;
|
||||
AmsConfiguration* config;
|
||||
GpioConfig* gpioConfig;
|
||||
MeterConfig* meterConfig;
|
||||
WebConfig webConfig;
|
||||
AmsData* meterState;
|
||||
AmsDataStorage* ds;
|
||||
MQTTClient* mqtt = NULL;
|
||||
bool uploading = false;
|
||||
File file;
|
||||
bool performRestart = false;
|
||||
|
||||
#if defined(ESP8266)
|
||||
ESP8266WebServer server;
|
||||
@@ -49,21 +64,63 @@ private:
|
||||
bool checkSecurity(byte level);
|
||||
|
||||
void indexHtml();
|
||||
void applicationJs();
|
||||
void temperature();
|
||||
void temperaturePost();
|
||||
void temperatureJson();
|
||||
void price();
|
||||
void configMeterHtml();
|
||||
void configWifiHtml();
|
||||
void configMqttHtml();
|
||||
void configWebHtml();
|
||||
void configDomoticzHtml();
|
||||
void configEntsoeHtml();
|
||||
void configNtpHtml();
|
||||
void configGpioHtml();
|
||||
void configDebugHtml();
|
||||
void bootCss();
|
||||
void gaugemeterJs();
|
||||
void githubSvg();
|
||||
void dataJson();
|
||||
void dayplotJson();
|
||||
void monthplotJson();
|
||||
void energyPriceJson();
|
||||
|
||||
void handleSetup();
|
||||
void handleSave();
|
||||
|
||||
size_t print(const char* text);
|
||||
size_t println(const char* text);
|
||||
size_t print(const Printable& data);
|
||||
size_t println(const Printable& data);
|
||||
String getSerialSelectOptions(int selected);
|
||||
void firmwareHtml();
|
||||
void firmwareUpload();
|
||||
void firmwareDownload();
|
||||
void restartHtml();
|
||||
void restartPost();
|
||||
void restartWaitHtml();
|
||||
void isAliveCheck();
|
||||
|
||||
void uploadHtml(const char* label, const char* action, const char* menu);
|
||||
void deleteHtml(const char* label, const char* action, const char* menu);
|
||||
void uploadFile(const char* path);
|
||||
void deleteFile(const char* path);
|
||||
void uploadPost();
|
||||
void mqttCa();
|
||||
void mqttCaUpload();
|
||||
void mqttCaDelete();
|
||||
void mqttCert();
|
||||
void mqttCertUpload();
|
||||
void mqttCertDelete();
|
||||
void mqttKey();
|
||||
void mqttKeyUpload();
|
||||
void mqttKeyDelete();
|
||||
|
||||
void factoryResetHtml();
|
||||
void factoryResetPost();
|
||||
|
||||
void notFound();
|
||||
|
||||
void printD(String fmt, ...);
|
||||
void printI(String fmt, ...);
|
||||
void printW(String fmt, ...);
|
||||
void printE(String fmt, ...);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user