mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-03-11 04:57:28 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise 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 and concise 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]
|
||||
- Temperature sensors [e.g. 3xDS18B20]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
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]
|
||||
14
README.md
14
README.md
@@ -1,14 +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 a web interface available on runtime, showing meter data in real time.
|
||||
|
||||
<img src="webui.jpg">
|
||||
<img src="webui.png">
|
||||
|
||||
## Setting up your device
|
||||
Go to the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki) for information on how to get your own device!
|
||||
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.
|
||||
@@ -19,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.
|
||||
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/Aidon-RJ12.pdf
Normal file
BIN
doc/Aidon-RJ12.pdf
Normal file
Binary file not shown.
18
doc/Aidon_OBIS.txt
Normal file
18
doc/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/Aidon_data.xml
Normal file
119
doc/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>
|
||||
32
doc/Kamstrup_encrypted_OBIS.txt
Normal file
32
doc/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/Kamstrup_encrypted_data.xml
Normal file
112
doc/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/Slimme_meter_15_a727fce1f1.pdf
Normal file
BIN
doc/Slimme_meter_15_a727fce1f1.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
|
||||
@@ -1,4 +1,31 @@
|
||||
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
|
||||
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
|
||||
|
||||
@@ -23,4 +23,12 @@ T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
|
||||
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 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
|
||||
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
|
||||
104
frames/austria.raw
Normal file
104
frames/austria.raw
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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
|
||||
|
||||
|
||||
Some complete frames
|
||||
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 4F D7 32 A2 4E 08 32 D8 38 62 C0
|
||||
91 7E 0F C3 BF 47 83 9A 1C 8F 81 D8 BC DB 8D C8 06 D6 8C B3 F2 7A 64 FF F5 AE F8 74 31 7F F0 D8 D8 30 57 57
|
||||
D7 23 C1 5A 50 23 A2 56 C5 4E 1B A3 C1 FC 75 65 75 31 4F EF D3 71 C3 E9 B4 1E CD 61 3E BF A7 27 26 A7 48 B4
|
||||
64 E3 75 B5 4A A3 57 B1 C1 8C E2 25 8F D9 14 C6 6F 9B 6B EE EF 7E 0B 3E 1C 7E 53 7F D4 A6 9D 5F 3E 5E 0B 4A
|
||||
61 BA 45 8F A4 0E D5 2D 88 F3 51 76 1D 90 78 8E 0F 29 43 D4 DF 9E 05 88 26 1F C9 4A 1D F2 C2 95 84 57 A8 95
|
||||
19 EF 45 7B E8 17 CE 59 B1 78 1D 0D 82 E4 58 3F 1A 76 D2 01 CF 65 75 3C 53 97 78 C0 8A 8A 31 94 E5 15 01 81
|
||||
EB 58 E6 95 34 3D C9 46 AF FC 57 EE 5A 6D 5E 6F 6A 21 15 D1 6B 7D 4F E2 A1 83 C4 3A 81 CA 1E C9 D0 73 84 E1
|
||||
60 E5 0E 80 BC D5 58 2D B9 1A 16
|
||||
|
||||
// 19b
|
||||
68 0D 0D 68 53 FF 11 01 67 CD 6B CB 69 13 53 FF 98 34 16
|
||||
|
||||
// 263b
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05
|
||||
E6 D9 FD 81 F8 20 00 69 D1 50 55 28 2C E9 97 46
|
||||
82 61 19 3E 23 78 8A E6 E2 42 D1 D6 44 BA 2C 3C
|
||||
55 0E 59 47 02 DC 8D D4 10 91 67 6B 76 9B F0 2F
|
||||
42 BF D9 D2 FE A2 B3 AA 11 B1 BF 7B 8B B3 36 FE
|
||||
7E B0 22 D7 60 10 48 1B 77 AA C2 DC 99 8D C2 C4
|
||||
5D 78 83 53 92 E8 66 44 CC 32 43 A9 E8 22 B2 0E
|
||||
DF D8 39 B3 21 5B E6 A8 F1 83 5E 85 5A A3 5D 2B
|
||||
92 ED 59 D7 24 2C CC 26 AB A6 0A FE 78 B0 E9 D3
|
||||
7C 6D B8 32 0F 36 C0 A0 9B A2 56 73 08 56 EE 9B
|
||||
AD 7C CC F3 6B EC 13 63 55 2A 28 0E 7A 9B D9 2A
|
||||
62 08 D5 9C AD E8 43 6D 7A CA 8B DD BF DB 3F E1
|
||||
88 3F 9D B9 7C 19 D3 68 8C 57 AB 82 46 4B 75 B8
|
||||
F3 9E 2C 22 06 2A 93 78 56 56 76 51 65 11 C7 12
|
||||
78 AB F2 97 97 51 2A 16 70 56 30 C9 12 00 08 BC
|
||||
80 55 6E 44 51 A1 93 CD CF BA 9A DA CA 48 19 74
|
||||
E4 70 1E AD 63 99 16
|
||||
|
||||
68 0D 0D 68 53 FF 11 01 67 21 2B 32 52 74 00 40 41 90 16
|
||||
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53
|
||||
41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 51 A8 0C 89 6B 68 23 FE 94 57 3B AB 39 56 9F 26 E5 D9 A7 10 1C F3 E4
|
||||
0E 2E C6 8D 3F 0A FE 8B 54 ED AC AE 84 36 86 72 B6 AA 0D B3 94 88 C0 37 4C 75 09 53 6E 3F 44 E1 A9 28 F8 28
|
||||
7A D4 E0 65 0A FA 46 A9 08 A6 3A EE C6 20 B5 7C E8 F8 C1 92 40 84 54 2E F4 99 A9 04 86 42 9E 2F E3 D5 28 92
|
||||
99 80 EE 10 A4 96 7F BC 72 63 33 32 4E 1C FF 71 1C 4B 66 CE 48 9B 46 FF A5 36 F2 E6 FE 84 E8 38 56 65 2E 59
|
||||
79 4B 2A A1 84 B4 63 53 25 EA 02 F1 9B 50 A2 CA FB DE 22 BB E8 24 A5 70 52 F4 64 F5 93 D7 16 9D A1 90 6C F3
|
||||
04 C6 26 95 6E 60 3E C6 4A F6 BA BB AC 01 FE 23 74 26 3F 7E F1 05 BD 76 2A 7C 34 FA FC EF 1F 40 46 6E F1 6F
|
||||
DA 36 75 00 E6 1A BF C2 B5 7F 34 D7 37 4C A0 6C CC A7 33 EF 9C 4A B0 E7 50 67 0A FB 85 18 25 31 67 DD 16
|
||||
|
||||
68 0D 0D 68 53 FF 11 01 67 CD 91 DA 34 7A CE 84 AD B0 16
|
||||
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9
|
||||
FD 81 F8 20 00 69 D1 52 7F 86 D1 DD 40 FD F7 2C 67 32 4F 5D 69 56 B0 8F FB 04 A9 E6 03 C6 A7 6B 7F 86 E7 BF
|
||||
2D E6 44 09 42 BF C8 B3 92 D1 EB CA EF B2 78 56 14 F9 32 6B 8F A4 8E 44 DB 86 B8 D1 83 82 67 BC DF 3D 85 DE
|
||||
42 9F FA 88 69 F4 06 C8 CB 4C A8 FF E6 7A 44 D3 29 97 90 CA 1B 22 F7 D8 8D 6E F7 69 34 1C 7F 6A B6 AA 95 96
|
||||
D0 48 95 02 0E 76 C0 BE 31 94 A1 72 66 30 E2 72 D9 82 30 1E 9A 81 DC 23 D7 AA 10 E5 91 19 AA 5B 97 F4 51 21
|
||||
17 97 E6 2E 25 D2 7C 8D E0 D8 10 19 ED B6 2A B7 29 EA 4B B1 35 F9 89 65 1A 4D 45 BE C5 3F E9 86 24 14 A3 5B
|
||||
20 AD 9F 20 A5 57 9F DE 82 AD 58 4D 65 35 4D 4E 74 D2 0F EA DE 26 4F 48 AB 3D E0 91 0D 9C 1D E6 C8 2B 4F C2
|
||||
3B 53 21 2A 26 82 B2 7D 9F 93 83 91 9F 4C 0B 47 3A 48 60 7B A3 12 6D 0B 9A 40 77 88 16
|
||||
|
||||
68 0D 0D 68 53 FF 11 01 67 C7 91 CB B9 7B 23 AC 8D 7E 16
|
||||
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69
|
||||
D1 53 4F 0C 66 DD DE 97 31 C0 8B E5 2B CE C3 99 51 17 DE FE AB 9E 2F 1B 76 06 71 8D 2E 9D BF 54 6E E5 B3 7B
|
||||
CE 05 34 58 22 6D 53 8F 95 6A 0A 6F EA 88 87 18 CD 29 B4 B1 99 8E 03 05 EB 1B 61 9B 36 09 99 E9 42 D4 D5 BC
|
||||
E7 57 11 1E 95 FE 9B 76 CF C2 1A 52 EE 70 2B 1F 6A 52 CA 90 85 FF AB D1 F0 E5 20 90 82 3E 5E 2E 25 60 D0 FB
|
||||
74 A1 7C A2 01 C2 AC 40 4A C6 0F 82 8C 0C CE 18 B8 18 1D EF 94 CC 54 16 D8 64 1F 60 B3 34 B8 0E 0C 10 56 11
|
||||
89 D6 1E 26 91 1F 85 C4 BE 55 69 96 DA D0 D6 9E 69 4A 8F 10 BF 37 97 68 55 3E 92 B1 F1 76 21 BF 34 03 54 DB
|
||||
F1 2C 23 9F B7 79 02 E8 37 DD AF 15 79 70 C4 95 C9 28 90 4E 6F BC FA 52 E7 FE 27 B5 F3 6E C4 C3 C1 37 CF C9
|
||||
7C E8 B1 10 7F 6B 4D 49 88 CD 2B 60 22 51 D7 5C 5F E6 AA 0B E1 2B 16
|
||||
|
||||
68 0D 0D 68 53 FF 11 01 67 C5 B1 63 A0 24 39 F5 57 ED 16
|
||||
|
||||
|
||||
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 54 5B D2 4F 76
|
||||
37 23 43 D5 D1 C1 02 A8 E8 91 41 6D 6C F7 E5 10 1E D5 6A 1B 73 8C 54 B1 87 32 FB 85 A6 F1 80 CE 40 B9 48 5F
|
||||
54 7F 1F 6A 18 F2 54 9E ED D9 18 33 DC 52 E2 14 E1 BF 65 FE 7B 58 9B 37 89 93 8D 98 AF 63 FB 90 FA A1 02 8B
|
||||
80 32 BF 7D 9D 61 42 56 F0 2E 7A D1 1B B0 88 09 DC 21 F6 6E 0E 5D D3 D9 B8 66 69 96 2D 60 CC 55 2C F1 E3 A1
|
||||
5B 30 56 AB 57 FE 5D 6A 4F E4 40 CF A1 64 22 C7 4E 60 DD DA 8E 08 17 2F 49 F2 C8 0D F5 A0 ED 06 27 E0 A5 F9
|
||||
1C B2 7A 97 5E 59 0A CC C8 A9 25 AB 1F 6A B4 AD DE 8B FF AF 9A 4F 7F 44 D3 00 18 34 57 E3 15 A2 DB 4E D0 2A
|
||||
20 54 61 60 47 50 B9 6C 8E D0 D4 7C 27 36 0C 89 0B 82 DD 14 2D BE A1 9C D9 DF 31 F9 BF 46 A4 14 26 4D 86 04
|
||||
28 54 44 F3 49 9C 12 CF 02 15 F9 4D B7 AB 4A B2 16 68 0D 0D 68 53 FF 11 01 67 70 74 A7 30 84 47 41 BE 50 16
|
||||
68 01 01 68 53 FF 00 01 67 DB 08 53 41 47 59 05 E6 D9 FD 81 F8 20 00 69 D1 55 59 04 1D D6 A4 96 28 33 5D A5
|
||||
A5 8F 22 2F 26 3C 77 F2 94 3C D7 ED 65 24 AA 5D 96 5E 95 71 E3 1C 99 83 40 31 A1 3F 2E BD 1F 32 A7 CE 5F 32
|
||||
59 05 55 AC C9 C0 9D 2A 59 AF 09 3A 81 61 BC DB 4D 60 CF 8D 65 BF 6E 06 E5 73 59 FE 1D 1C D3 1C 08 EC 5C 8E
|
||||
57 AA 3E E6 06 6D 71 45 CE AB DC 5C 25 AC 15 09 BC A2 BD E8 12 C2 92 C9 9E D1 38 A4 02 59 38 98 38 63 3B 45
|
||||
B4 1A 20 4D 34 05 74 46 50 BE A5 87 D1 7A 5F 98 91 9A DA E9 FA 1E AA 72 10 58 3C 0A 5D 46 81 4E 57 B2 98 DF
|
||||
@@ -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,10 +0,0 @@
|
||||
name=HanReader
|
||||
version=1.0.1
|
||||
author=roarfred
|
||||
maintainer=roarfred <not@important.com>
|
||||
sentence=HAN support
|
||||
paragraph=HAN support
|
||||
category=Sensors
|
||||
url=https://github.com/roarfred/AmsToMqttBridge
|
||||
architectures=*
|
||||
depends=Timezone
|
||||
@@ -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,14 +0,0 @@
|
||||
#include "Crc16.h"
|
||||
|
||||
Crc16Class::Crc16Class() { }
|
||||
|
||||
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) ^ crc16_ccitt_table_reverse[index]);
|
||||
}
|
||||
fcs ^= 0xffff;
|
||||
return fcs;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#ifndef _CRC16_h
|
||||
#define _CRC16_h
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
const int crc16_ccitt_table_reverse [256] PROGMEM = {
|
||||
0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF,
|
||||
0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7,
|
||||
0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E,
|
||||
0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876,
|
||||
0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD,
|
||||
0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5,
|
||||
0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C,
|
||||
0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974,
|
||||
0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB,
|
||||
0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3,
|
||||
0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A,
|
||||
0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72,
|
||||
0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9,
|
||||
0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1,
|
||||
0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738,
|
||||
0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70,
|
||||
0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7,
|
||||
0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF,
|
||||
0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036,
|
||||
0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E,
|
||||
0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5,
|
||||
0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD,
|
||||
0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134,
|
||||
0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C,
|
||||
0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3,
|
||||
0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB,
|
||||
0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232,
|
||||
0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A,
|
||||
0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1,
|
||||
0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9,
|
||||
0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330,
|
||||
0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78
|
||||
};
|
||||
|
||||
|
||||
#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);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,224 +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, Print* debugger)
|
||||
{
|
||||
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) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Preparing for next frame\n");
|
||||
}
|
||||
Clear();
|
||||
}
|
||||
|
||||
// Check if we're about to run into a buffer overflow
|
||||
if (position >= DLMS_READER_BUFFER_SIZE) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Buffer overflow\n");
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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)) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Incorrect frame format %02X\n", frameFormatType);
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Destination address length incorrect\n");
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Source address length incorrect\n");
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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)) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Header checksum is incorrect %02X\n", headerChecksum);
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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)) {
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Frame checksum is incorrect %02X\n", checksum);
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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
|
||||
{
|
||||
if(debugger != NULL) {
|
||||
debugger->printf("Received incorrect end marker %02X\n", data);
|
||||
debugPrint(buffer, 0, position, debugger);
|
||||
}
|
||||
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::getBytesRead() {
|
||||
return dataLength - (destinationAddressLength + sourceAddressLength + 7);
|
||||
}
|
||||
|
||||
byte* DlmsReader::getBuffer() {
|
||||
return buffer + (3 + destinationAddressLength + sourceAddressLength + 2 + 1);
|
||||
}
|
||||
|
||||
byte* DlmsReader::getFullBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
int DlmsReader::getFullBufferLength() {
|
||||
return dataLength;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
void DlmsReader::debugPrint(byte *buffer, int start, int length, Print* debugger) {
|
||||
for (int i = start; i < start + length; i++) {
|
||||
if (buffer[i] < 0x10)
|
||||
debugger->print("0");
|
||||
debugger->print(buffer[i], HEX);
|
||||
debugger->print(" ");
|
||||
if ((i - start + 1) % 16 == 0)
|
||||
debugger->println("");
|
||||
else if ((i - start + 1) % 4 == 0)
|
||||
debugger->print(" ");
|
||||
|
||||
yield(); // Let other get some resources too
|
||||
}
|
||||
debugger->println("");
|
||||
}
|
||||
@@ -1,47 +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, Print* Debug);
|
||||
int GetRawData(byte *buffer, int start, int length);
|
||||
int getBytesRead();
|
||||
byte* getBuffer();
|
||||
byte* getFullBuffer();
|
||||
int getFullBufferLength();
|
||||
|
||||
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();
|
||||
void debugPrint(byte *buffer, int start, int length, Print* debugger);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,424 +0,0 @@
|
||||
#include "HanReader.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
|
||||
HanReader::HanReader() {
|
||||
// Central European Time (Frankfurt, Paris)
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
|
||||
localZone = new Timezone(CEST, CET);
|
||||
}
|
||||
|
||||
void HanReader::setup(Stream *hanPort, RemoteDebug *debug)
|
||||
{
|
||||
han = hanPort;
|
||||
bytesRead = 0;
|
||||
debugger = debug;
|
||||
|
||||
if (debug) debug->println("MBUS serial setup complete");
|
||||
}
|
||||
|
||||
void HanReader::setup(Stream *hanPort){
|
||||
setup(hanPort, NULL);
|
||||
}
|
||||
|
||||
void HanReader::setEncryptionKey(uint8_t* encryption_key) {
|
||||
this->encryption_key = encryption_key;
|
||||
}
|
||||
|
||||
void HanReader::setAuthenticationKey(uint8_t* authentication_key) {
|
||||
this->authentication_key = authentication_key;
|
||||
}
|
||||
|
||||
|
||||
bool HanReader::read(byte data) {
|
||||
if (reader.Read(data, debugger->isActive(RemoteDebug::DEBUG) ? debugger : NULL)) {
|
||||
bytesRead = reader.getBytesRead();
|
||||
buffer = reader.getBuffer();
|
||||
if (debugger->isActive(RemoteDebug::INFO)) {
|
||||
printI("Got valid DLMS data (%d bytes)", bytesRead);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
byte* full = reader.getFullBuffer();
|
||||
int size = reader.getFullBufferLength();
|
||||
printI("Full DLMS frame (%d bytes)", size);
|
||||
debugPrint(full, 0, size);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Data should start with E6 E7 00 0F
|
||||
and continue with four bytes for the InvokeId
|
||||
*/
|
||||
if (bytesRead < 9) {
|
||||
printW("Invalid HAN data: Less than 9 bytes received");
|
||||
return false;
|
||||
}
|
||||
else if (
|
||||
buffer[0] != 0xE6
|
||||
|| buffer[1] != 0xE7
|
||||
|| buffer[2] != 0x00
|
||||
)
|
||||
{
|
||||
printW("Invalid HAN data: Start should be E6 E7 00");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Have not found any documentation supporting this, but 0x0F for all norwegian meters.
|
||||
// Danish meters with encryption has 0xDB, so lets assume this has something to do with that.
|
||||
switch(buffer[3]) {
|
||||
case 0x0F:
|
||||
dataHeader = 8;
|
||||
break;
|
||||
case 0xDB:
|
||||
printI("Decrypting frame");
|
||||
if(!decryptFrame()) return false;
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("Data after decryption:");
|
||||
debugPrint(buffer, 0, bytesRead);
|
||||
}
|
||||
dataHeader = 26;
|
||||
break;
|
||||
}
|
||||
|
||||
listSize = getInt(0, buffer, 0, bytesRead);
|
||||
printI("HAN data is valid, listSize: %d", listSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t headersize = 3;
|
||||
const size_t footersize = 0;
|
||||
|
||||
bool HanReader::decryptFrame() {
|
||||
uint8_t system_title[8];
|
||||
memcpy(system_title, buffer + headersize + 2, 8);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("System title:");
|
||||
debugPrint(system_title, 0, 8);
|
||||
}
|
||||
|
||||
uint8_t initialization_vector[12];
|
||||
memcpy(initialization_vector, system_title, 8);
|
||||
memcpy(initialization_vector + 8, buffer + headersize + 14, 4);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("Initialization vector:");
|
||||
debugPrint(initialization_vector, 0, 12);
|
||||
}
|
||||
|
||||
uint8_t additional_authenticated_data[17];
|
||||
memcpy(additional_authenticated_data, buffer + headersize + 13, 1);
|
||||
memcpy(additional_authenticated_data + 1, authentication_key, 16);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("Additional authenticated data:");
|
||||
debugPrint(additional_authenticated_data, 0, 17);
|
||||
}
|
||||
|
||||
uint8_t authentication_tag[12];
|
||||
memcpy(authentication_tag, buffer + headersize + bytesRead - headersize - footersize - 12, 12);
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("Authentication tag:");
|
||||
debugPrint(authentication_tag, 0, 12);
|
||||
}
|
||||
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
printD("Encryption key:");
|
||||
debugPrint(encryption_key, 0, 16);
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
br_gcm_context gcmCtx;
|
||||
br_aes_ct_ctr_keys bc;
|
||||
br_aes_ct_ctr_init(&bc, encryption_key, 16);
|
||||
br_gcm_init(&gcmCtx, &bc.vtable, br_ghash_ctmul32);
|
||||
br_gcm_reset(&gcmCtx, initialization_vector, sizeof(initialization_vector));
|
||||
br_gcm_aad_inject(&gcmCtx, additional_authenticated_data, sizeof(additional_authenticated_data));
|
||||
br_gcm_flip(&gcmCtx);
|
||||
br_gcm_run(&gcmCtx, 0, buffer + headersize + 18, bytesRead - headersize - footersize - 18 - 12);
|
||||
if(br_gcm_check_tag_trunc(&gcmCtx, authentication_tag, 12) != 1) {
|
||||
printE("authdecrypt failed");
|
||||
return false;
|
||||
}
|
||||
#elif defined(ESP32)
|
||||
uint8_t cipher_text[bytesRead - headersize - footersize - 18 - 12];
|
||||
memcpy(cipher_text, buffer + headersize + 18, bytesRead - headersize - footersize - 12 - 18);
|
||||
|
||||
mbedtls_gcm_context m_ctx;
|
||||
mbedtls_gcm_init(&m_ctx);
|
||||
int success = mbedtls_gcm_setkey(&m_ctx, MBEDTLS_CIPHER_ID_AES, encryption_key, 128);
|
||||
if (0 != success ) {
|
||||
printE("Setkey failed: " + String(success));
|
||||
return false;
|
||||
}
|
||||
success = mbedtls_gcm_auth_decrypt(&m_ctx, sizeof(cipher_text), initialization_vector, sizeof(initialization_vector),
|
||||
additional_authenticated_data, sizeof(additional_authenticated_data), authentication_tag, sizeof(authentication_tag),
|
||||
cipher_text, buffer + headersize + 18);
|
||||
if (0 != success) {
|
||||
printE("authdecrypt failed: " + String(success));
|
||||
return false;
|
||||
}
|
||||
mbedtls_gcm_free(&m_ctx);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void HanReader::debugPrint(byte *buffer, int start, int length) {
|
||||
for (int i = start; i < start + length; i++) {
|
||||
if (buffer[i] < 0x10)
|
||||
debugger->print("0");
|
||||
debugger->print(buffer[i], HEX);
|
||||
debugger->print(" ");
|
||||
if ((i - start + 1) % 16 == 0)
|
||||
debugger->println("");
|
||||
else if ((i - start + 1) % 4 == 0)
|
||||
debugger->print(" ");
|
||||
|
||||
yield(); // Let other get some resources too
|
||||
}
|
||||
debugger->println("");
|
||||
}
|
||||
|
||||
bool HanReader::read() {
|
||||
while(han->available()) {
|
||||
if(read(han->read())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int HanReader::getListSize() {
|
||||
return listSize;
|
||||
}
|
||||
|
||||
time_t HanReader::getPackageTime(bool respectTimezone, bool respectDsc) {
|
||||
int packageTimePosition = dataHeader
|
||||
+ (compensateFor09HeaderBug ? 1 : 0);
|
||||
|
||||
return getTime(buffer, packageTimePosition, bytesRead, respectTimezone, respectDsc);
|
||||
}
|
||||
|
||||
time_t HanReader::getTime(uint8_t objectId, bool respectTimezone, bool respectDsc) {
|
||||
return getTime(objectId, respectTimezone, respectDsc, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
int32_t HanReader::getInt(uint8_t objectId) {
|
||||
return getInt(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
uint32_t HanReader::getUint(uint8_t objectId) {
|
||||
return getUint32(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
String HanReader::getString(uint8_t objectId) {
|
||||
return getString(objectId, buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
int HanReader::getBuffer(byte* buf) {
|
||||
for (int i = 0; i < bytesRead; i++) {
|
||||
buf[i] = buffer[i];
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
int HanReader::findValuePosition(uint8_t 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 {
|
||||
printW("Unknown data type found: 0x%s", String(buffer[i], HEX).c_str());
|
||||
return 0; // unknown data type found
|
||||
}
|
||||
}
|
||||
|
||||
printD("Passed the end of the data. Length was: %d", length);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
time_t HanReader::getTime(uint8_t dataPosition, bool respectTimezone, bool respectDsc, 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, respectTimezone, respectDsc);
|
||||
}
|
||||
|
||||
time_t HanReader::getTime(byte *buffer, int start, int length, bool respectTimezone, bool respectDsc) {
|
||||
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];
|
||||
// 4: Day of week
|
||||
int hour = buffer[pos + 5];
|
||||
int minute = buffer[pos + 6];
|
||||
int second = buffer[pos + 7];
|
||||
// 8: Hundredths
|
||||
int16_t tzMinutes = buffer[pos + 9] << 8 | buffer[pos + 10];
|
||||
bool dsc = (buffer[pos + 11] & 0x80) == 0x80;
|
||||
|
||||
tmElements_t tm;
|
||||
tm.Year = year - 1970;
|
||||
tm.Month = month;
|
||||
tm.Day = day;
|
||||
tm.Hour = hour;
|
||||
tm.Minute = minute;
|
||||
tm.Second = second;
|
||||
|
||||
time_t time = makeTime(tm);
|
||||
if(respectTimezone && (tzMinutes | 0x8000) != 0x8000 && tzMinutes <= 720 && tzMinutes >= -720) {
|
||||
time -= tzMinutes * 60;
|
||||
if(respectDsc && dsc)
|
||||
time += 3600;
|
||||
} else {
|
||||
if(respectDsc && dsc)
|
||||
time += 3600;
|
||||
time = localZone->toUTC(time);
|
||||
}
|
||||
return time;
|
||||
} else if(dataLength == 0) {
|
||||
return (time_t)0L;
|
||||
} else {
|
||||
printW("Unknown time length: %d", dataLength);
|
||||
// Date format not supported
|
||||
return (time_t)0L;
|
||||
}
|
||||
}
|
||||
|
||||
int HanReader::getInt(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
|
||||
if (valuePosition > 0) {
|
||||
switch (buffer[valuePosition++]) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
case 0x16:
|
||||
return getUint8(dataPosition, buffer, start, length);
|
||||
case 0x0F:
|
||||
return getInt8(dataPosition, buffer, start, length);
|
||||
case 0x12:
|
||||
return getUint16(dataPosition, buffer, start, length);
|
||||
case 0x10:
|
||||
return getInt16(dataPosition, buffer, start, length);
|
||||
case 0x06:
|
||||
return getUint32(dataPosition, buffer, start, length);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int8_t HanReader::getInt8(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0 && buffer[valuePosition++] == 0x0F) {
|
||||
return buffer[valuePosition];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int16_t HanReader::getInt16(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0 && buffer[valuePosition++] == 0x10) {
|
||||
return buffer[valuePosition] << 8 | buffer[valuePosition+1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t HanReader::getUint8(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0) {
|
||||
switch(buffer[valuePosition++]) {
|
||||
case 0x01:
|
||||
case 0x02:
|
||||
case 0x16:
|
||||
return buffer[valuePosition];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t HanReader::getUint16(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0 && buffer[valuePosition++] == 0x12) {
|
||||
return buffer[valuePosition] << 8 | buffer[valuePosition+1];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t HanReader::getUint32(uint8_t dataPosition, byte *buffer, int start, int length) {
|
||||
int valuePosition = findValuePosition(dataPosition, buffer, start, length);
|
||||
if (valuePosition > 0) {
|
||||
if(buffer[valuePosition++] != 0x06)
|
||||
return 0;
|
||||
uint32_t value = 0;
|
||||
for (int i = valuePosition; i < valuePosition + 4; i++) {
|
||||
value = value << 8 | buffer[i];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String HanReader::getString(uint8_t 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("");
|
||||
}
|
||||
|
||||
void HanReader::printD(String fmt, int arg) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
|
||||
}
|
||||
|
||||
void HanReader::printI(String fmt, int arg) {
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
|
||||
}
|
||||
|
||||
void HanReader::printW(String fmt, int arg) {
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
|
||||
}
|
||||
|
||||
void HanReader::printW(String fmt, const char* arg) {
|
||||
if(debugger->isActive(RemoteDebug::WARNING)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
|
||||
}
|
||||
|
||||
void HanReader::printE(String fmt, int arg) {
|
||||
if(debugger->isActive(RemoteDebug::ERROR)) debugger->printf(String("(HanReader)" + fmt + "\n").c_str(), arg);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#ifndef _HANREADER_h
|
||||
#define _HANREADER_h
|
||||
|
||||
#if defined(ARDUINO) && ARDUINO >= 100
|
||||
#include "Arduino.h"
|
||||
#else
|
||||
#include "WProgram.h"
|
||||
#endif
|
||||
|
||||
|
||||
#include "DlmsReader.h"
|
||||
#include <Timezone.h>
|
||||
#include "RemoteDebug.h"
|
||||
|
||||
class HanReader
|
||||
{
|
||||
public:
|
||||
uint dataHeader = 8;
|
||||
bool compensateFor09HeaderBug = false;
|
||||
|
||||
HanReader();
|
||||
void setup(Stream *hanPort);
|
||||
void setup(Stream *hanPort, RemoteDebug *debug);
|
||||
bool read();
|
||||
bool read(byte data);
|
||||
int getListSize();
|
||||
time_t getPackageTime(bool respectTimezone, bool respectDsc);
|
||||
int32_t getInt(uint8_t objectId); // Use this for uint8, int8, uint16, int16
|
||||
uint32_t getUint(uint8_t objectId); // Only for uint32
|
||||
String getString(uint8_t objectId);
|
||||
time_t getTime(uint8_t objectId, bool respectTimezone, bool respectDsc);
|
||||
int getBuffer(byte* buf);
|
||||
|
||||
void setEncryptionKey(uint8_t* encryption_key);
|
||||
void setAuthenticationKey(uint8_t* authentication_key);
|
||||
|
||||
private:
|
||||
RemoteDebug* debugger;
|
||||
Stream *han;
|
||||
byte* buffer;
|
||||
int bytesRead;
|
||||
DlmsReader reader;
|
||||
int listSize;
|
||||
Timezone *localZone;
|
||||
uint8_t* encryption_key;
|
||||
uint8_t* authentication_key;
|
||||
|
||||
int findValuePosition(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
|
||||
time_t getTime(uint8_t dataPosition, bool respectTimezone, bool respectDsc, byte *buffer, int start, int length);
|
||||
time_t getTime(byte *buffer, int start, int length, bool respectTimezone, bool respectDsc);
|
||||
int getInt(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
int8_t getInt8(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
uint8_t getUint8(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
int16_t getInt16(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
uint16_t getUint16(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
uint32_t getUint32(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
String getString(uint8_t dataPosition, byte *buffer, int start, int length);
|
||||
|
||||
time_t toUnixTime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
|
||||
|
||||
bool decryptFrame();
|
||||
|
||||
void debugPrint(byte *buffer, int start, int length);
|
||||
|
||||
void printD(String fmt, int arg=0);
|
||||
void printI(String fmt, int arg=0);
|
||||
void printW(String fmt, int arg=0);
|
||||
void printW(String fmt, const char* arg);
|
||||
void printE(String fmt, int arg=0);
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
@@ -1,57 +0,0 @@
|
||||
#ifndef _KAIFA_h
|
||||
#define _KAIFA_h
|
||||
|
||||
enum class Kaifa {
|
||||
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,127 +0,0 @@
|
||||
// Kamstrup.h
|
||||
|
||||
#ifndef _KAMSTRUP_h
|
||||
#define _KAMSTRUP_h
|
||||
|
||||
enum class Kamstrup
|
||||
{
|
||||
List1PhaseShort = 0x11,
|
||||
List1PhaseLong = 0x1B,
|
||||
List3PhaseShort = 0x19,
|
||||
List3PhaseLong = 0x23,
|
||||
List3PhaseITShort = 0x17,
|
||||
List3PhaseITLong = 0x21
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
enum class Kamstrup_List3PhaseIT
|
||||
{
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
MeterID_OBIS,
|
||||
MeterID,
|
||||
MeterType_OBIS,
|
||||
MeterType,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
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
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
#ifndef _OMNIPOWER_h
|
||||
#define _OMNIPOWER_h
|
||||
|
||||
enum class Omnipower {
|
||||
DLMS = 0x41
|
||||
};
|
||||
|
||||
enum class Omnipower_DLMS {
|
||||
ListSize,
|
||||
ListVersionIdentifier,
|
||||
CumulativeActiveImportEnergy_OBIS,
|
||||
CumulativeActiveImportEnergy,
|
||||
CumulativeActiveExportEnergy_OBIS,
|
||||
CumulativeActiveExportEnergy,
|
||||
CumulativeReactiveImportEnergy_OBIS,
|
||||
CumulativeReactiveImportEnergy,
|
||||
CumulativeReactiveExportEnergy_OBIS,
|
||||
CumulativeReactiveExportEnergy,
|
||||
MeterNumber_OBIS,
|
||||
MeterNumber,
|
||||
ActiveImportPower_OBIS,
|
||||
ActiveImportPower,
|
||||
ActiveExportPower_OBIS,
|
||||
ActiveExportPower,
|
||||
ReactiveImportPower_OBIS,
|
||||
ReactiveImportPower,
|
||||
ReactiveExportPower_OBIS,
|
||||
ReactiveExportPower,
|
||||
MeterClock_OBIS,
|
||||
MeterClock,
|
||||
VoltageL1_OBIS,
|
||||
VoltageL1,
|
||||
VoltageL2_OBIS,
|
||||
VoltageL2,
|
||||
VoltageL3_OBIS,
|
||||
VoltageL3,
|
||||
CurrentL1_OBIS,
|
||||
CurrentL1,
|
||||
CurrentL2_OBIS,
|
||||
CurrentL2,
|
||||
CurrentL3_OBIS,
|
||||
CurrentL3,
|
||||
ActiveImportPowerL1_OBIS,
|
||||
ActiveImportPowerL1,
|
||||
ActiveImportPowerL2_OBIS,
|
||||
ActiveImportPowerL2,
|
||||
ActiveImportPowerL3_OBIS,
|
||||
ActiveImportPowerL3,
|
||||
PowerFactorL1_OBIS,
|
||||
PowerFactorL1,
|
||||
PowerFactorL2_OBIS,
|
||||
PowerFactorL2,
|
||||
PowerFactorL3_OBIS,
|
||||
PowerFactorL3,
|
||||
PowerFactor_OBIS,
|
||||
PowerFactor,
|
||||
ActiveExportPowerL1_OBIS,
|
||||
ActiveExportPowerL1,
|
||||
ActiveExportPowerL2_OBIS,
|
||||
ActiveExportPowerL2,
|
||||
ActiveExportPowerL3_OBIS,
|
||||
ActiveExportPowerL3,
|
||||
CumulativeActiveExportEnergyL1_OBIS,
|
||||
CumulativeActiveExportEnergyL1,
|
||||
CumulativeActiveExportEnergyL2_OBIS,
|
||||
CumulativeActiveExportEnergyL2,
|
||||
CumulativeActiveExportEnergyL3_OBIS,
|
||||
CumulativeActiveExportEnergyL3,
|
||||
CumulativeActiveImportEnergyL1_OBIS,
|
||||
CumulativeActiveImportEnergyL1,
|
||||
CumulativeActiveImportEnergyL2_OBIS,
|
||||
CumulativeActiveImportEnergyL2,
|
||||
CumulativeActiveImportEnergyL3_OBIS,
|
||||
CumulativeActiveImportEnergyL3
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -2,24 +2,31 @@
|
||||
extra_configs = platformio-user.ini
|
||||
|
||||
[common]
|
||||
lib_deps = file://lib/HanReader, file://lib/Timezone, MQTT@2.5.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.9.0, RemoteDebug@3.0.5, Time@1.6.0
|
||||
lib_deps = file://lib/Timezone, 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.0
|
||||
lib_ignore = OneWire
|
||||
|
||||
[env:esp8266]
|
||||
platform = espressif8266@2.6.2
|
||||
platform = espressif8266@3.2.0
|
||||
board = esp12e
|
||||
board_build.ldscript = eagle.flash.4m2m.ld
|
||||
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
|
||||
|
||||
|
||||
[env:esp32]
|
||||
platform = espressif32@2.1.0
|
||||
platform = https://github.com/platformio/platform-espressif32.git#feature/arduino-upstream
|
||||
board = esp32dev
|
||||
board_build.partitions = no_ota.csv
|
||||
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
|
||||
|
||||
@@ -182,7 +182,12 @@ bool AmsConfiguration::getMeterConfig(MeterConfig& config) {
|
||||
bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
|
||||
MeterConfig existing;
|
||||
if(getMeterConfig(existing)) {
|
||||
meterChanged |= config.type != existing.type;
|
||||
meterChanged |= config.baud != existing.baud;
|
||||
meterChanged |= config.parity != existing.parity;
|
||||
meterChanged |= config.invert != existing.invert;
|
||||
meterChanged |= config.distributionSystem != existing.distributionSystem;
|
||||
meterChanged |= config.mainFuse != existing.mainFuse;
|
||||
meterChanged |= config.productionCapacity != existing.productionCapacity;
|
||||
meterChanged |= strcmp((char*) config.encryptionKey, (char*) existing.encryptionKey);
|
||||
meterChanged |= strcmp((char*) config.authenticationKey, (char*) existing.authenticationKey);
|
||||
} else {
|
||||
@@ -196,13 +201,14 @@ bool AmsConfiguration::setMeterConfig(MeterConfig& config) {
|
||||
}
|
||||
|
||||
void AmsConfiguration::clearMeter(MeterConfig& config) {
|
||||
config.type = 0;
|
||||
config.baud = 2400;
|
||||
config.parity = 11; // 8E1
|
||||
config.invert = false;
|
||||
config.distributionSystem = 0;
|
||||
config.mainFuse = 0;
|
||||
config.productionCapacity = 0;
|
||||
memset(config.encryptionKey, 0, 16);
|
||||
memset(config.authenticationKey, 0, 16);
|
||||
config.substituteMissing = false;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::isMeterChanged() {
|
||||
@@ -382,6 +388,8 @@ void AmsConfiguration::clearGpio(GpioConfig& config) {
|
||||
config.vccOffset = 0;
|
||||
config.vccMultiplier = 1000;
|
||||
config.vccBootLimit = 0;
|
||||
config.vccResistorGnd = 0;
|
||||
config.vccResistorVcc = 0;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::getNtpConfig(NtpConfig& config) {
|
||||
@@ -521,16 +529,6 @@ bool AmsConfiguration::hasConfig() {
|
||||
EEPROM.end();
|
||||
}
|
||||
switch(configVersion) {
|
||||
case 82:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(loadConfig82(EEPROM_CONFIG_ADDRESS+1)) {
|
||||
configVersion = EEPROM_CHECK_SUM;
|
||||
return true;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 83:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(loadConfig83(EEPROM_CONFIG_ADDRESS+1)) {
|
||||
@@ -544,20 +542,50 @@ bool AmsConfiguration::hasConfig() {
|
||||
case 86:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(relocateConfig86()) {
|
||||
configVersion = EEPROM_CHECK_SUM;
|
||||
return true;
|
||||
configVersion = 87;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
case 87:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(relocateConfig87()) {
|
||||
configVersion = 88;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
case 88:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(relocateConfig88()) {
|
||||
configVersion = 89;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
case 89:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(relocateConfig89()) {
|
||||
configVersion = 90;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
case 90:
|
||||
configVersion = -1; // Prevent loop
|
||||
if(relocateConfig90()) {
|
||||
configVersion = 91;
|
||||
} else {
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case EEPROM_CHECK_SUM:
|
||||
return true;
|
||||
default:
|
||||
configVersion = 0;
|
||||
return false;
|
||||
}
|
||||
return configVersion == EEPROM_CHECK_SUM;
|
||||
}
|
||||
|
||||
int AmsConfiguration::getConfigVersion() {
|
||||
@@ -602,107 +630,6 @@ void AmsConfiguration::saveTempSensors() {
|
||||
}
|
||||
}
|
||||
|
||||
bool AmsConfiguration::loadConfig82(int address) {
|
||||
ConfigObject82 c;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(address, c);
|
||||
|
||||
EntsoeConfig entsoe;
|
||||
clearEntsoe(entsoe);
|
||||
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
|
||||
|
||||
NtpConfig ntp;
|
||||
clearNtp(ntp);
|
||||
EEPROM.put(CONFIG_NTP_START, ntp);
|
||||
|
||||
DomoticzConfig domo {
|
||||
c.domoELIDX,
|
||||
c.domoVL1IDX,
|
||||
c.domoVL2IDX,
|
||||
c.domoVL3IDX,
|
||||
c.domoCL1IDX
|
||||
};
|
||||
EEPROM.put(CONFIG_DOMOTICZ_START, domo);
|
||||
|
||||
GpioConfig gpio {
|
||||
c.hanPin,
|
||||
c.apPin,
|
||||
c.ledPin,
|
||||
c.ledInverted,
|
||||
c.ledPinRed,
|
||||
c.ledPinGreen,
|
||||
c.ledPinBlue,
|
||||
c.ledRgbInverted,
|
||||
c.tempSensorPin,
|
||||
0xFF,
|
||||
c.vccPin,
|
||||
0,
|
||||
c.vccMultiplier,
|
||||
c.vccBootLimit
|
||||
};
|
||||
EEPROM.put(CONFIG_GPIO_START, gpio);
|
||||
|
||||
DebugConfig debug {
|
||||
c.debugTelnet,
|
||||
c.debugSerial,
|
||||
c.debugLevel
|
||||
};
|
||||
EEPROM.put(CONFIG_DEBUG_START, debug);
|
||||
|
||||
MeterConfig meter {
|
||||
c.meterType,
|
||||
c.distributionSystem,
|
||||
c.mainFuse,
|
||||
c.productionCapacity,
|
||||
{0},
|
||||
{0},
|
||||
c.substituteMissing
|
||||
};
|
||||
EEPROM.put(CONFIG_METER_START, meter);
|
||||
|
||||
WebConfig web {
|
||||
c.authSecurity
|
||||
};
|
||||
strcpy(web.username, c.authUser);
|
||||
strcpy(web.password, c.authPassword);
|
||||
EEPROM.put(CONFIG_WEB_START, web);
|
||||
|
||||
MqttConfig mqtt;
|
||||
strcpy(mqtt.host, c.mqttHost);
|
||||
mqtt.port = c.mqttPort;
|
||||
strcpy(mqtt.clientId, c.mqttClientId);
|
||||
strcpy(mqtt.publishTopic, c.mqttPublishTopic);
|
||||
strcpy(mqtt.subscribeTopic, c.mqttSubscribeTopic);
|
||||
strcpy(mqtt.username, c.mqttUser);
|
||||
strcpy(mqtt.password, c.mqttPassword);
|
||||
mqtt.payloadFormat = c.mqttPayloadFormat;
|
||||
mqtt.ssl = c.mqttSsl;
|
||||
EEPROM.put(CONFIG_MQTT_START, mqtt);
|
||||
|
||||
WiFiConfig wifi;
|
||||
strcpy(wifi.ssid, c.wifiSsid);
|
||||
strcpy(wifi.psk, c.wifiPassword);
|
||||
strcpy(wifi.ip, c.wifiIp);
|
||||
strcpy(wifi.gateway, c.wifiGw);
|
||||
strcpy(wifi.subnet, c.wifiSubnet);
|
||||
strcpy(wifi.dns1, c.wifiDns1);
|
||||
strcpy(wifi.dns2, c.wifiDns2);
|
||||
strcpy(wifi.hostname, c.wifiHostname);
|
||||
wifi.mdns = true;
|
||||
EEPROM.put(CONFIG_WIFI_START, wifi);
|
||||
|
||||
SystemConfig sys {
|
||||
c.boardType
|
||||
};
|
||||
EEPROM.put(CONFIG_SYSTEM_START, sys);
|
||||
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::loadConfig83(int address) {
|
||||
ConfigObject83 c;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
@@ -743,7 +670,9 @@ bool AmsConfiguration::loadConfig83(int address) {
|
||||
c.vccPin,
|
||||
c.vccOffset,
|
||||
c.vccMultiplier,
|
||||
c.vccBootLimit
|
||||
c.vccBootLimit,
|
||||
0,
|
||||
0
|
||||
};
|
||||
EEPROM.put(CONFIG_GPIO_START, gpio);
|
||||
|
||||
@@ -755,13 +684,14 @@ bool AmsConfiguration::loadConfig83(int address) {
|
||||
EEPROM.put(CONFIG_DEBUG_START, debug);
|
||||
|
||||
MeterConfig meter {
|
||||
c.meterType,
|
||||
2400,
|
||||
c.meterType == 3 || c.meterType == 4 ? 3 : 11,
|
||||
false,
|
||||
c.distributionSystem,
|
||||
c.mainFuse,
|
||||
c.productionCapacity,
|
||||
{0},
|
||||
{0},
|
||||
c.substituteMissing
|
||||
{0}
|
||||
};
|
||||
memcpy(meter.encryptionKey, c.meterEncryptionKey, 16);
|
||||
memcpy(meter.authenticationKey, c.meterAuthenticationKey, 16);
|
||||
@@ -825,6 +755,96 @@ bool AmsConfiguration::relocateConfig86() {
|
||||
mqtt.payloadFormat = mqtt86.payloadFormat;
|
||||
mqtt.ssl = mqtt86.ssl;
|
||||
EEPROM.put(CONFIG_MQTT_START, mqtt);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 87);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::relocateConfig87() {
|
||||
MeterConfig87 meter87;
|
||||
MeterConfig meter;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_METER_START_87, meter87);
|
||||
if(meter87.type < 5) {
|
||||
meter.baud = 2400;
|
||||
meter.parity = meter87.type == 3 || meter87.type == 4 ? 3 : 11;
|
||||
meter.invert = false;
|
||||
} else {
|
||||
meter.baud = 115200;
|
||||
meter.parity = 3;
|
||||
meter.invert = meter87.type == 6;
|
||||
}
|
||||
meter.distributionSystem = meter87.distributionSystem;
|
||||
meter.mainFuse = meter87.mainFuse;
|
||||
meter.productionCapacity = meter87.productionCapacity;
|
||||
EEPROM.put(CONFIG_METER_START, meter);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 88);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::relocateConfig88() {
|
||||
GpioConfig88 gpio88;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_GPIO_START_88, gpio88);
|
||||
|
||||
GpioConfig gpio {
|
||||
gpio88.hanPin,
|
||||
gpio88.apPin,
|
||||
gpio88.ledPin,
|
||||
gpio88.ledInverted,
|
||||
gpio88.ledPinRed,
|
||||
gpio88.ledPinGreen,
|
||||
gpio88.ledPinBlue,
|
||||
gpio88.ledRgbInverted,
|
||||
gpio88.tempSensorPin,
|
||||
gpio88.tempAnalogSensorPin,
|
||||
gpio88.vccPin,
|
||||
gpio88.vccOffset,
|
||||
gpio88.vccMultiplier,
|
||||
gpio88.vccBootLimit,
|
||||
0,
|
||||
0
|
||||
};
|
||||
EEPROM.put(CONFIG_GPIO_START, gpio);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 89);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::relocateConfig89() {
|
||||
EntsoeConfig89 entose89;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_ENTSOE_START_89, entose89);
|
||||
|
||||
uint32_t multiplier = entose89.multiplier;
|
||||
|
||||
EntsoeConfig entsoe = {
|
||||
0x0,
|
||||
0x0,
|
||||
0x0,
|
||||
multiplier
|
||||
};
|
||||
strcpy(entsoe.token, entose89.token);
|
||||
strcpy(entsoe.area, entose89.area);
|
||||
strcpy(entsoe.currency, entose89.currency);
|
||||
|
||||
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 90);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AmsConfiguration::relocateConfig90() {
|
||||
EntsoeConfig entsoe;
|
||||
EEPROM.begin(EEPROM_SIZE);
|
||||
EEPROM.get(CONFIG_ENTSOE_START_90, entsoe);
|
||||
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
|
||||
EEPROM.put(EEPROM_CONFIG_ADDRESS, 91);
|
||||
bool ret = EEPROM.commit();
|
||||
EEPROM.end();
|
||||
return ret;
|
||||
@@ -986,11 +1006,12 @@ void AmsConfiguration::print(Print* debugger)
|
||||
MeterConfig meter;
|
||||
if(getMeterConfig(meter)) {
|
||||
debugger->println("--Meter configuration--");
|
||||
debugger->printf("Type: %i\r\n", meter.type);
|
||||
debugger->printf("Baud: %i\r\n", meter.baud);
|
||||
debugger->printf("Parity: %i\r\n", meter.parity);
|
||||
debugger->printf("Invert serial: %s\r\n", meter.invert ? "Yes" : "No");
|
||||
debugger->printf("Distribution system: %i\r\n", meter.distributionSystem);
|
||||
debugger->printf("Main fuse: %i\r\n", meter.mainFuse);
|
||||
debugger->printf("Production Capacity: %i\r\n", meter.productionCapacity);
|
||||
debugger->printf("Substitute missing: %s\r\n", meter.substituteMissing ? "Yes" : "No");
|
||||
debugger->println("");
|
||||
delay(10);
|
||||
Serial.flush();
|
||||
@@ -1010,9 +1031,17 @@ void AmsConfiguration::print(Print* debugger)
|
||||
debugger->printf("Temperature pin: %i\r\n", gpio.tempSensorPin);
|
||||
debugger->printf("Temp analog pin: %i\r\n", gpio.tempAnalogSensorPin);
|
||||
debugger->printf("Vcc pin: %i\r\n", gpio.vccPin);
|
||||
debugger->printf("Vcc multiplier: %f\r\n", gpio.vccMultiplier / 1000.0);
|
||||
debugger->printf("Vcc offset: %f\r\n", gpio.vccOffset / 100.0);
|
||||
debugger->printf("Vcc boot limit: %f\r\n", gpio.vccBootLimit / 10.0);
|
||||
if(gpio.vccMultiplier > 0) {
|
||||
debugger->printf("Vcc multiplier: %f\r\n", gpio.vccMultiplier / 1000.0);
|
||||
}
|
||||
if(gpio.vccOffset > 0) {
|
||||
debugger->printf("Vcc offset: %f\r\n", gpio.vccOffset / 100.0);
|
||||
}
|
||||
if(gpio.vccBootLimit > 0) {
|
||||
debugger->printf("Vcc boot limit: %f\r\n", gpio.vccBootLimit / 10.0);
|
||||
}
|
||||
debugger->printf("GND resistor: %i\r\n", gpio.vccResistorGnd);
|
||||
debugger->printf("Vcc resistor: %i\r\n", gpio.vccResistorVcc);
|
||||
debugger->println("");
|
||||
delay(10);
|
||||
Serial.flush();
|
||||
|
||||
@@ -3,23 +3,27 @@
|
||||
#include <EEPROM.h>
|
||||
#include "Arduino.h"
|
||||
|
||||
#define EEPROM_SIZE 1024 * 3
|
||||
#define EEPROM_CHECK_SUM 87 // Used to check if config is stored. Change if structure changes
|
||||
#define EEPROM_SIZE 1024*3
|
||||
#define EEPROM_CHECK_SUM 91 // 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_WIFI_START 16
|
||||
#define CONFIG_METER_START 224
|
||||
#define CONFIG_GPIO_START 266
|
||||
#define CONFIG_ENTSOE_START 290
|
||||
#define CONFIG_WEB_START 648
|
||||
#define CONFIG_METER_START 784
|
||||
#define CONFIG_DEBUG_START 824
|
||||
#define CONFIG_GPIO_START 832
|
||||
#define CONFIG_DOMOTICZ_START 856
|
||||
#define CONFIG_NTP_START 872
|
||||
#define CONFIG_ENTSOE_START 944
|
||||
#define CONFIG_MQTT_START 1004
|
||||
|
||||
#define CONFIG_MQTT_START_86 224
|
||||
#define CONFIG_METER_START_87 784
|
||||
#define CONFIG_GPIO_START_88 832
|
||||
#define CONFIG_ENTSOE_START_89 944
|
||||
#define CONFIG_ENTSOE_START_90 286
|
||||
|
||||
|
||||
struct SystemConfig {
|
||||
@@ -69,6 +73,17 @@ struct WebConfig {
|
||||
}; // 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;
|
||||
@@ -99,6 +114,25 @@ struct GpioConfig {
|
||||
int16_t vccOffset;
|
||||
uint16_t vccMultiplier;
|
||||
uint8_t vccBootLimit;
|
||||
uint16_t vccResistorGnd;
|
||||
uint16_t vccResistorVcc;
|
||||
}; // 20
|
||||
|
||||
struct GpioConfig88 {
|
||||
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;
|
||||
}; // 16
|
||||
|
||||
struct DomoticzConfig {
|
||||
@@ -117,13 +151,20 @@ struct NtpConfig {
|
||||
char server[64];
|
||||
}; // 70
|
||||
|
||||
struct EntsoeConfig {
|
||||
struct EntsoeConfig89 {
|
||||
char token[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint16_t multiplier;
|
||||
}; // 60
|
||||
|
||||
struct EntsoeConfig {
|
||||
char token[37];
|
||||
char area[17];
|
||||
char currency[4];
|
||||
uint32_t multiplier;
|
||||
}; // 62
|
||||
|
||||
struct ConfigObject83 {
|
||||
uint8_t boardType;
|
||||
char wifiSsid[32];
|
||||
@@ -190,60 +231,6 @@ struct ConfigObject83 {
|
||||
uint8_t tempAnalogSensorPin;
|
||||
};
|
||||
|
||||
struct ConfigObject82 {
|
||||
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;
|
||||
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;
|
||||
uint16_t vccMultiplier;
|
||||
uint8_t vccBootLimit;
|
||||
|
||||
uint16_t domoELIDX;
|
||||
uint16_t domoVL1IDX;
|
||||
uint16_t domoVL2IDX;
|
||||
uint16_t domoVL3IDX;
|
||||
uint16_t domoCL1IDX;
|
||||
};
|
||||
|
||||
struct TempSensorConfig {
|
||||
uint8_t address[8];
|
||||
char name[16];
|
||||
@@ -334,9 +321,12 @@ private:
|
||||
uint8_t tempSensorCount = 0;
|
||||
TempSensorConfig** tempSensors;
|
||||
|
||||
bool loadConfig82(int address);
|
||||
bool loadConfig83(int address);
|
||||
bool relocateConfig86();
|
||||
bool relocateConfig87();
|
||||
bool relocateConfig88(); // dev 1.6
|
||||
bool relocateConfig89(); // dev 1.6
|
||||
bool relocateConfig90(); // 2.0.0
|
||||
|
||||
int readString(int pAddress, char* pString[]);
|
||||
int readInt(int pAddress, int *pValue);
|
||||
|
||||
333
src/AmsData.cpp
333
src/AmsData.cpp
@@ -1,327 +1,43 @@
|
||||
#include "AmsData.h"
|
||||
#include "Kaifa.h"
|
||||
#include "Aidon.h"
|
||||
#include "Kamstrup.h"
|
||||
#include "Omnipower.h"
|
||||
|
||||
AmsData::AmsData() {}
|
||||
|
||||
AmsData::AmsData(uint8_t meterType, bool substituteMissing, HanReader& hanReader) {
|
||||
lastUpdateMillis = millis();
|
||||
packageTimestamp = hanReader.getPackageTime(true, true);
|
||||
|
||||
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;
|
||||
case METER_TYPE_OMNIPOWER:
|
||||
extractFromOmnipower(hanReader, listSize);
|
||||
break;
|
||||
}
|
||||
threePhase = l1voltage > 0 && l2voltage > 0 && l3voltage > 0;
|
||||
twoPhase = (l1voltage > 0 && l2voltage > 0) || (l2voltage > 0 && l3voltage > 0) || (l3voltage > 0 && l1voltage > 0);
|
||||
|
||||
if(threePhase) {
|
||||
if(substituteMissing) {
|
||||
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromKaifa(HanReader& hanReader, uint8_t listSize) {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Kaifa::List1:
|
||||
listType = 1;
|
||||
break;
|
||||
case (uint8_t)Kaifa::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (uint8_t)Kaifa::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (uint8_t)Kaifa::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (uint8_t)Kaifa::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
if(listSize == (uint8_t)Kaifa::List1) {
|
||||
activeImportPower = hanReader.getInt((int)Kaifa_List1::ActivePowerImported);
|
||||
} else {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Kaifa::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock, false, false);
|
||||
activeImportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveImportEnergy)) / 1000;
|
||||
activeExportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeActiveExportEnergy)) / 1000;
|
||||
reactiveImportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeReactiveImportEnergy)) / 1000;
|
||||
reactiveExportCounter = ((float) hanReader.getUint((int)Kaifa_List3Phase::CumulativeReactiveExportEnergy)) / 1000;
|
||||
case (uint8_t)Kaifa::List3PhaseShort:
|
||||
listId = hanReader.getString( (int)Kaifa_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kaifa_List3Phase::MeterID);
|
||||
meterModel = hanReader.getString( (int)Kaifa_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getUint( (int)Kaifa_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getUint( (int)Kaifa_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getUint( (int)Kaifa_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getUint( (int)Kaifa_List3Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL1)) / 1000;
|
||||
l2current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL2)) / 1000;
|
||||
l3current = ((float) hanReader.getInt( (int)Kaifa_List3Phase::CurrentL3)) / 1000;
|
||||
l1voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL1)) / 10;
|
||||
l2voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL2)) / 10;
|
||||
l3voltage = ((float) hanReader.getInt( (int)Kaifa_List3Phase::VoltageL3)) / 10;
|
||||
break;
|
||||
case (uint8_t)Kaifa::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock, false, false);
|
||||
activeImportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveImportEnergy));
|
||||
activeExportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeActiveExportEnergy));
|
||||
reactiveImportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeReactiveImportEnergy));
|
||||
reactiveExportCounter = ((float) hanReader.getUint((int)Kaifa_List1Phase::CumulativeReactiveExportEnergy));
|
||||
case (uint8_t)Kaifa::List1PhaseShort:
|
||||
listId = hanReader.getString( (int)Kaifa_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (int)Kaifa_List1Phase::MeterID);
|
||||
meterModel = hanReader.getString( (int)Kaifa_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getUint( (int)Kaifa_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getUint( (int)Kaifa_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getUint( (int)Kaifa_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getUint( (int)Kaifa_List1Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt( (int)Kaifa_List1Phase::CurrentL1)) / 1000;
|
||||
l1voltage = ((float) hanReader.getInt( (int)Kaifa_List1Phase::VoltageL1)) / 10;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromAidon(HanReader& hanReader, uint8_t listSize) {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Aidon::List1:
|
||||
listType = 1;
|
||||
break;
|
||||
case (uint8_t)Aidon::List3PhaseITShort:
|
||||
case (uint8_t)Aidon::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (uint8_t)Aidon::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (uint8_t)Aidon::List3PhaseITLong:
|
||||
case (uint8_t)Aidon::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (uint8_t)Aidon::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
if(listSize == (uint8_t)Aidon::List1) {
|
||||
activeImportPower = hanReader.getUint((uint8_t)Aidon_List1::ActiveImportPower);
|
||||
} else {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Aidon::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List3Phase::Timestamp, false, false);
|
||||
activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Aidon::List3PhaseShort:
|
||||
listId = hanReader.getString( (uint8_t)Aidon_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Aidon_List3Phase::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Aidon_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List3Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL1)) / 10;
|
||||
l2current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL2)) / 10;
|
||||
l3current = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::CurrentL3)) / 10;
|
||||
l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL1)) / 10;
|
||||
l2voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL2)) / 10;
|
||||
l3voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3Phase::VoltageL3)) / 10;
|
||||
break;
|
||||
case (uint8_t)Aidon::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List1Phase::Timestamp, false, false);
|
||||
activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List1Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Aidon::List1PhaseShort:
|
||||
listId = hanReader.getString( (uint8_t)Aidon_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Aidon_List1Phase::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Aidon_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List1Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List1Phase::CurrentL1)) / 10;
|
||||
l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List1Phase::VoltageL1)) / 10;
|
||||
break;
|
||||
case (uint8_t)Aidon::List3PhaseITLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Aidon_List3PhaseIT::Timestamp, false, false);
|
||||
activeImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Aidon::List3PhaseITShort:
|
||||
listId = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Aidon_List3PhaseIT::MeterType);
|
||||
activeImportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getUint( (uint8_t)Aidon_List3PhaseIT::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::CurrentL1)) / 10;
|
||||
l2current = 0;
|
||||
l3current = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::CurrentL3)) / 10;
|
||||
l1voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL1)) / 10;
|
||||
l2voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL2)) / 10;
|
||||
l3voltage = ((float) hanReader.getInt( (uint8_t)Aidon_List3PhaseIT::VoltageL3)) / 10;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromKamstrup(HanReader& hanReader, uint8_t listSize) {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Kamstrup::List3PhaseITShort:
|
||||
case (uint8_t)Kamstrup::List3PhaseShort:
|
||||
threePhase = true;
|
||||
case (uint8_t)Kamstrup::List1PhaseShort:
|
||||
listType = 2;
|
||||
break;
|
||||
case (uint8_t)Kamstrup::List3PhaseITLong:
|
||||
case (uint8_t)Kamstrup::List3PhaseLong:
|
||||
threePhase = true;
|
||||
case (uint8_t)Kamstrup::List1PhaseLong:
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
switch(listSize) {
|
||||
case (uint8_t)Kamstrup::List1PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List1Phase::MeterClock, true, true);
|
||||
activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Kamstrup::List1PhaseShort:
|
||||
listId = hanReader.getString( (uint8_t)Kamstrup_List1Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Kamstrup_List1Phase::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Kamstrup_List1Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List1Phase::CurrentL1)) / 100;
|
||||
l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List1Phase::VoltageL1);
|
||||
break;
|
||||
case (uint8_t)Kamstrup::List3PhaseLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List3Phase::MeterClock, true, true);
|
||||
activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Kamstrup::List3PhaseShort:
|
||||
listId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL1)) / 100;
|
||||
l2current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL2)) / 100;
|
||||
l3current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL3)) / 100;
|
||||
l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL1);
|
||||
l2voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL2);
|
||||
l3voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL3);
|
||||
break;
|
||||
case (uint8_t)Kamstrup::List3PhaseITLong:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Kamstrup_List3Phase::MeterClock, true, true);
|
||||
activeImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CumulativeReactiveExportEnergy)) / 100;
|
||||
case (uint8_t)Kamstrup::List3PhaseITShort:
|
||||
listId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::ListVersionIdentifier);
|
||||
meterId = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterID);
|
||||
meterModel = hanReader.getString( (uint8_t)Kamstrup_List3Phase::MeterType);
|
||||
activeImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL1)) / 100;
|
||||
l2current = 0;
|
||||
l3current = ((float) hanReader.getInt((uint8_t)Kamstrup_List3Phase::CurrentL3)) / 100;
|
||||
l1voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL1);
|
||||
l2voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL2);
|
||||
l3voltage = hanReader.getInt( (uint8_t)Kamstrup_List3Phase::VoltageL3);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AmsData::extractFromOmnipower(HanReader& hanReader, uint8_t listSize) {
|
||||
switch(listSize) {
|
||||
case (uint8_t)Kamstrup::List3PhaseITShort:
|
||||
case (uint8_t)Kamstrup::List3PhaseShort:
|
||||
case (uint8_t)Kamstrup::List1PhaseShort:
|
||||
case (uint8_t)Kamstrup::List3PhaseITLong:
|
||||
case (uint8_t)Kamstrup::List3PhaseLong:
|
||||
case (uint8_t)Kamstrup::List1PhaseLong:
|
||||
extractFromKamstrup(hanReader, listSize);
|
||||
break;
|
||||
case (uint8_t)Omnipower::DLMS:
|
||||
meterTimestamp = hanReader.getTime( (uint8_t)Omnipower_DLMS::MeterClock, true, true);
|
||||
activeImportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeActiveImportEnergy)) / 100;
|
||||
activeExportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeActiveExportEnergy)) / 100;
|
||||
reactiveImportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeReactiveImportEnergy)) / 100;
|
||||
reactiveExportCounter = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CumulativeReactiveExportEnergy)) / 100;
|
||||
listId = hanReader.getString( (uint8_t)Omnipower_DLMS::ListVersionIdentifier);
|
||||
activeImportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ActiveImportPower);
|
||||
reactiveImportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ReactiveImportPower);
|
||||
activeExportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ActiveExportPower);
|
||||
reactiveExportPower = hanReader.getInt( (uint8_t)Omnipower_DLMS::ReactiveExportPower);
|
||||
l1current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL1)) / 100;
|
||||
l2current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL2)) / 100;
|
||||
l3current = ((float) hanReader.getInt((uint8_t)Omnipower_DLMS::CurrentL3)) / 100;
|
||||
l1voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL1);
|
||||
l2voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL2);
|
||||
l3voltage = hanReader.getInt( (uint8_t)Omnipower_DLMS::VoltageL3);
|
||||
listType = 3;
|
||||
break;
|
||||
}
|
||||
threePhase = l3voltage != 0;
|
||||
}
|
||||
|
||||
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)
|
||||
activeImportCounter += (((float) ms) * other.getActiveImportPower()) / 3600000000;
|
||||
activeImportCounter += (((float) ms) * other.getActiveImportPower()) / 3600000000.0;
|
||||
|
||||
if(other.getListType() > 1) {
|
||||
ms = this->lastUpdateMillis > other.getLastUpdateMillis() ? 0 : other.getLastUpdateMillis() - this->lastList2;
|
||||
if(other.getActiveExportPower() > 0)
|
||||
activeExportCounter += (((float) ms*2) * other.getActiveExportPower()) / 3600000000;
|
||||
activeExportCounter += (((float) ms) * other.getActiveExportPower()) / 3600000000.0;
|
||||
if(other.getReactiveImportPower() > 0)
|
||||
reactiveImportCounter += (((float) ms*2) * other.getReactiveImportPower()) / 3600000000;
|
||||
reactiveImportCounter += (((float) ms) * other.getReactiveImportPower()) / 3600000000.0;
|
||||
if(other.getReactiveExportPower() > 0)
|
||||
reactiveExportCounter += (((float) ms*2) * other.getReactiveExportPower()) / 3600000000;
|
||||
reactiveExportCounter += (((float) ms) * other.getReactiveExportPower()) / 3600000000.0;
|
||||
}
|
||||
counterEstimated = true;
|
||||
}
|
||||
} else {
|
||||
//Serial.printf("\nDeviation: %.4f\n", other.getActiveImportCounter() - activeImportCounter);
|
||||
}
|
||||
|
||||
this->lastUpdateMillis = other.getLastUpdateMillis();
|
||||
if(other.getListType() > 1) {
|
||||
this->lastList2 = this->lastUpdateMillis;
|
||||
}
|
||||
this->packageTimestamp = other.getPackageTimestamp();
|
||||
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();
|
||||
@@ -331,6 +47,7 @@ void AmsData::apply(AmsData& other) {
|
||||
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();
|
||||
@@ -368,6 +85,10 @@ String AmsData::getMeterId() {
|
||||
return this->meterId;
|
||||
}
|
||||
|
||||
uint8_t AmsData::getMeterType() {
|
||||
return this->meterType;
|
||||
}
|
||||
|
||||
String AmsData::getMeterModel() {
|
||||
return this->meterModel;
|
||||
}
|
||||
@@ -416,6 +137,22 @@ float AmsData::getL3Current() {
|
||||
return this->l3current;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <Timezone.h>
|
||||
#include "HanReader.h"
|
||||
|
||||
#define METER_TYPE_KAIFA 1
|
||||
#define METER_TYPE_AIDON 2
|
||||
#define METER_TYPE_KAMSTRUP 3
|
||||
#define METER_TYPE_OMNIPOWER 4
|
||||
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(uint8_t meterType, bool substituteMissing, HanReader& hanReader);
|
||||
|
||||
void apply(AmsData& other);
|
||||
|
||||
@@ -25,6 +30,7 @@ public:
|
||||
|
||||
String getListId();
|
||||
String getMeterId();
|
||||
uint8_t getMeterType();
|
||||
String getMeterModel();
|
||||
|
||||
time_t getMeterTimestamp();
|
||||
@@ -42,6 +48,11 @@ public:
|
||||
float getL2Current();
|
||||
float getL3Current();
|
||||
|
||||
float getPowerFactor();
|
||||
float getL1PowerFactor();
|
||||
float getL2PowerFactor();
|
||||
float getL3PowerFactor();
|
||||
|
||||
float getActiveImportCounter();
|
||||
float getReactiveImportCounter();
|
||||
float getActiveExportCounter();
|
||||
@@ -50,21 +61,18 @@ public:
|
||||
bool isThreePhase();
|
||||
bool isTwoPhase();
|
||||
|
||||
private:
|
||||
protected:
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
uint8_t listType = 0;
|
||||
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;
|
||||
|
||||
void extractFromKaifa(HanReader& hanReader, uint8_t listSize);
|
||||
void extractFromAidon(HanReader& hanReader, uint8_t listSize);
|
||||
void extractFromKamstrup(HanReader& hanReader, uint8_t listSize);
|
||||
void extractFromOmnipower(HanReader& hanReader, uint8_t listSize);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
315
src/AmsDataStorage.cpp
Normal file
315
src/AmsDataStorage.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
#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::DEBUG)) {
|
||||
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-day.lastMeterReadTime < 3595) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
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(now > EPOCH_2021_01_01) {
|
||||
tmElements_t last;
|
||||
|
||||
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::DEBUG)) {
|
||||
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::DEBUG)) {
|
||||
debugger->printf("(AmsDataStorage) Clearing day: %d\n", i);
|
||||
}
|
||||
setDay(i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(tm.Hour == 0 && now-month.lastMeterReadTime > 86300) {
|
||||
Serial.printf("\n%d %d %d %d\n", month.version, month.lastMeterReadTime, month.activeImport, month.activeExport);
|
||||
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 = 0; 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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(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) return;
|
||||
day.points[hour] = val / 10;
|
||||
}
|
||||
|
||||
int16_t AmsDataStorage::getHour(uint8_t hour) {
|
||||
if(hour < 0) return 0;
|
||||
return day.points[hour] * 10;
|
||||
}
|
||||
|
||||
void AmsDataStorage::setDay(uint8_t day, int32_t val) {
|
||||
if(day < 1) return;
|
||||
month.points[day-1] = val / 10;
|
||||
}
|
||||
|
||||
int32_t AmsDataStorage::getDay(uint8_t day) {
|
||||
if(day < 1) 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 = 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
|
||||
@@ -7,4 +7,7 @@
|
||||
#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,11 +1,11 @@
|
||||
#ifndef _AMSTOMQTTBRIDGE_H
|
||||
#define _AMSTOMQTTBRIDGE_H
|
||||
|
||||
#define WIFI_CONNECTION_TIMEOUT 30000;
|
||||
#define WIFI_CONNECTION_TIMEOUT 25000;
|
||||
|
||||
#define INVALID_BUTTON_PIN 0xFFFFFFFF
|
||||
|
||||
#define EPOCH_2021_01_01 1609459200
|
||||
#define MAX_PEM_SIZE 4096
|
||||
|
||||
#include <SoftwareSerial.h>
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include "SPIFFS.h"
|
||||
#include "Update.h"
|
||||
#endif
|
||||
|
||||
#include "LittleFS.h"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,44 +10,39 @@
|
||||
* @author Gunnar Skjold (@gskjold)
|
||||
* Maintainer of current code
|
||||
* https://github.com/gskjold/AmsToMqttBridge
|
||||
*
|
||||
* @author Roar Fredriksen (@roarfred)
|
||||
* The original developer for this project
|
||||
* https://github.com/roarfred/AmsToMqttBridge
|
||||
*/
|
||||
#if defined(ESP8266)
|
||||
ADC_MODE(ADC_VCC);
|
||||
#endif
|
||||
|
||||
#include "AmsToMqttBridge.h"
|
||||
#include "AmsStorage.h"
|
||||
#include "AmsDataStorage.h"
|
||||
#include <MQTT.h>
|
||||
#include <DNSServer.h>
|
||||
#include <lwip/apps/sntp.h>
|
||||
|
||||
#if defined(ESP8266)
|
||||
ADC_MODE(ADC_VCC);
|
||||
#endif
|
||||
|
||||
#include "HwTools.h"
|
||||
#include "entsoe/EntsoeApi.h"
|
||||
|
||||
#include "web/AmsWebServer.h"
|
||||
#include "AmsConfiguration.h"
|
||||
#include "HanReader.h"
|
||||
|
||||
#include "mqtt/AmsMqttHandler.h"
|
||||
#include "mqtt/JsonMqttHandler.h"
|
||||
#include "mqtt/RawMqttHandler.h"
|
||||
#include "mqtt/DomoticzMqttHandler.h"
|
||||
|
||||
#include "Aidon.h"
|
||||
#include "Kaifa.h"
|
||||
#include "Kamstrup.h"
|
||||
#include "Omnipower.h"
|
||||
|
||||
#include "Uptime.h"
|
||||
|
||||
#define WEBSOCKET_DISABLED true
|
||||
#include "RemoteDebug.h"
|
||||
|
||||
#define BUF_SIZE (1024)
|
||||
#include "ams/hdlc.h"
|
||||
|
||||
#include "IEC6205621.h"
|
||||
#include "IEC6205675.h"
|
||||
|
||||
HwTools hw;
|
||||
|
||||
DNSServer* dnsServer = NULL;
|
||||
@@ -62,21 +57,26 @@ Timezone* tz;
|
||||
|
||||
AmsWebServer ws(&Debug, &hw);
|
||||
|
||||
MQTTClient mqtt(512);
|
||||
MQTTClient *mqtt = NULL;
|
||||
WiFiClient *mqttClient = new WiFiClient();
|
||||
WiFiClientSecure *mqttSecureClient = NULL;
|
||||
AmsMqttHandler* mqttHandler = NULL;
|
||||
|
||||
HanReader hanReader;
|
||||
|
||||
Stream *hanSerial;
|
||||
SoftwareSerial *swSerial = NULL;
|
||||
HDLCConfig* hc = NULL;
|
||||
|
||||
GpioConfig gpioConfig;
|
||||
MeterConfig meterConfig;
|
||||
bool mqttEnabled = false;
|
||||
uint8_t payloadFormat = 0;
|
||||
String topic = "ams";
|
||||
AmsData meterState;
|
||||
bool ntpEnabled = false;
|
||||
|
||||
AmsDataStorage ds(&Debug);
|
||||
|
||||
uint8_t wifiReconnectCount = 0;
|
||||
|
||||
void setup() {
|
||||
WiFiConfig wifi;
|
||||
Serial.begin(115200);
|
||||
@@ -140,17 +140,34 @@ void setup() {
|
||||
|
||||
bool shared = false;
|
||||
config.getMeterConfig(meterConfig);
|
||||
Serial.flush();
|
||||
Serial.end();
|
||||
if(gpioConfig.hanPin == 3) {
|
||||
shared = true;
|
||||
switch(meterConfig.type) {
|
||||
case METER_TYPE_KAMSTRUP:
|
||||
case METER_TYPE_OMNIPOWER:
|
||||
Serial.begin(2400, SERIAL_8N1);
|
||||
#if defined(ESP8266)
|
||||
SerialConfig serialConfig;
|
||||
#elif defined(ESP32)
|
||||
uint32_t serialConfig;
|
||||
#endif
|
||||
switch(meterConfig.parity) {
|
||||
case 2:
|
||||
serialConfig = SERIAL_7N1;
|
||||
break;
|
||||
case 3:
|
||||
serialConfig = SERIAL_8N1;
|
||||
break;
|
||||
case 10:
|
||||
serialConfig = SERIAL_7E1;
|
||||
break;
|
||||
default:
|
||||
Serial.begin(2400, SERIAL_8E1);
|
||||
serialConfig = SERIAL_8E1;
|
||||
break;
|
||||
}
|
||||
#if defined(ESP32)
|
||||
Serial.begin(meterConfig.baud, serialConfig, -1, -1, meterConfig.invert);
|
||||
#else
|
||||
Serial.begin(meterConfig.baud, serialConfig, SERIAL_FULL, 1, meterConfig.invert);
|
||||
#endif
|
||||
}
|
||||
|
||||
if(!shared) {
|
||||
@@ -174,10 +191,10 @@ void setup() {
|
||||
}
|
||||
|
||||
float vccBootLimit = gpioConfig.vccBootLimit == 0 ? 0 : gpioConfig.vccBootLimit / 10.0;
|
||||
if(vccBootLimit > 0 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
|
||||
if(vccBootLimit > 2.5 && vccBootLimit < 3.3 && (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH)) { // Skip if user is holding AP button while booting (HIGH = button is released)
|
||||
if (vcc < vccBootLimit) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugI("Voltage is too low, sleeping");
|
||||
Debug.printf("(setup) Voltage is too low (%.2f < %.2f), sleeping\n", vcc, vccBootLimit);
|
||||
Serial.flush();
|
||||
}
|
||||
ESP.deepSleep(10000000); //Deep sleep to allow output cap to charge up
|
||||
@@ -188,58 +205,63 @@ void setup() {
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
|
||||
bool spiffs = false;
|
||||
bool hasFs = false;
|
||||
#if defined(ESP32)
|
||||
debugD("ESP32 SPIFFS");
|
||||
spiffs = SPIFFS.begin(true);
|
||||
debugD("ESP32 LittleFS");
|
||||
hasFs = LittleFS.begin(true);
|
||||
debugD(" size: %d", LittleFS.totalBytes());
|
||||
#else
|
||||
debugD("ESP8266 SPIFFS");
|
||||
spiffs = SPIFFS.begin();
|
||||
debugD("ESP8266 LittleFS");
|
||||
hasFs = LittleFS.begin();
|
||||
#endif
|
||||
delay(1);
|
||||
|
||||
if(spiffs) {
|
||||
if(hasFs) {
|
||||
bool flashed = false;
|
||||
if(SPIFFS.exists(FILE_FIRMWARE)) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI("Found firmware");
|
||||
#if defined(ESP8266)
|
||||
WiFi.setSleepMode(WIFI_LIGHT_SLEEP);
|
||||
WiFi.forceSleepBegin();
|
||||
#endif
|
||||
int i = 0;
|
||||
while(hw.getVcc() > 1.0 && hw.getVcc() < 3.2 && i < 3) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI(" vcc not optimal, light sleep 10s");
|
||||
#if defined(ESP8266)
|
||||
delay(10000);
|
||||
#elif defined(ESP32)
|
||||
esp_sleep_enable_timer_wakeup(10000000);
|
||||
esp_light_sleep_start();
|
||||
#endif
|
||||
i++;
|
||||
}
|
||||
if(LittleFS.exists(FILE_FIRMWARE)) {
|
||||
if (gpioConfig.apPin == 0xFF || digitalRead(gpioConfig.apPin) == HIGH) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI("Found firmware");
|
||||
#if defined(ESP8266)
|
||||
WiFi.setSleepMode(WIFI_LIGHT_SLEEP);
|
||||
WiFi.forceSleepBegin();
|
||||
#endif
|
||||
int i = 0;
|
||||
while(hw.getVcc() > 1.0 && hw.getVcc() < 3.2 && i < 3) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI(" vcc not optimal, light sleep 10s");
|
||||
#if defined(ESP8266)
|
||||
delay(10000);
|
||||
#elif defined(ESP32)
|
||||
esp_sleep_enable_timer_wakeup(10000000);
|
||||
esp_light_sleep_start();
|
||||
#endif
|
||||
i++;
|
||||
}
|
||||
|
||||
debugI(" flashing");
|
||||
File firmwareFile = SPIFFS.open(FILE_FIRMWARE, "r");
|
||||
debugD(" firmware size: %d", firmwareFile.size());
|
||||
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||
debugD(" available: %d", maxSketchSpace);
|
||||
if (!Update.begin(maxSketchSpace, U_FLASH)) {
|
||||
if(Debug.isActive(RemoteDebug::ERROR)) {
|
||||
debugE("Unable to start firmware update");
|
||||
Update.printError(Serial);
|
||||
debugI(" flashing");
|
||||
File firmwareFile = LittleFS.open(FILE_FIRMWARE, "r");
|
||||
debugD(" firmware size: %d", firmwareFile.size());
|
||||
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||
debugD(" available: %d", maxSketchSpace);
|
||||
if (!Update.begin(maxSketchSpace, U_FLASH)) {
|
||||
if(Debug.isActive(RemoteDebug::ERROR)) {
|
||||
debugE("Unable to start firmware update");
|
||||
Update.printError(Serial);
|
||||
}
|
||||
} else {
|
||||
while (firmwareFile.available()) {
|
||||
uint8_t ibuffer[128];
|
||||
firmwareFile.read((uint8_t *)ibuffer, 128);
|
||||
Update.write(ibuffer, sizeof(ibuffer));
|
||||
}
|
||||
flashed = Update.end(true);
|
||||
}
|
||||
firmwareFile.close();
|
||||
} else {
|
||||
while (firmwareFile.available()) {
|
||||
uint8_t ibuffer[128];
|
||||
firmwareFile.read((uint8_t *)ibuffer, 128);
|
||||
Update.write(ibuffer, sizeof(ibuffer));
|
||||
}
|
||||
flashed = Update.end(true);
|
||||
debugW("AP button pressed, skipping firmware update and deleting firmware file.");
|
||||
}
|
||||
firmwareFile.close();
|
||||
SPIFFS.remove(FILE_FIRMWARE);
|
||||
LittleFS.remove(FILE_FIRMWARE);
|
||||
}
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
if(flashed) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugI("Firmware update complete, restarting");
|
||||
@@ -254,6 +276,7 @@ void setup() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
LittleFS.end();
|
||||
delay(1);
|
||||
|
||||
if(config.hasConfig()) {
|
||||
@@ -269,7 +292,10 @@ void setup() {
|
||||
TimeChangeRule dst = {"DST", Last, Sun, Mar, 2, (ntp.offset + ntp.summerOffset) / 6};
|
||||
tz = new Timezone(dst, std);
|
||||
ws.setTimezone(tz);
|
||||
ds.setTimezone(tz);
|
||||
}
|
||||
|
||||
ds.load();
|
||||
} else {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugI("No configuration, booting AP");
|
||||
@@ -277,7 +303,7 @@ void setup() {
|
||||
swapWifiMode();
|
||||
}
|
||||
|
||||
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &mqtt);
|
||||
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds);
|
||||
}
|
||||
|
||||
int buttonTimer = 0;
|
||||
@@ -288,7 +314,6 @@ bool longPressActive = false;
|
||||
bool wifiConnected = false;
|
||||
|
||||
unsigned long lastTemperatureRead = 0;
|
||||
unsigned long lastSuccessfulRead = 0;
|
||||
unsigned long lastErrorBlink = 0;
|
||||
int lastError = 0;
|
||||
|
||||
@@ -323,7 +348,7 @@ void loop() {
|
||||
hw.updateTemperatures();
|
||||
lastTemperatureRead = now;
|
||||
|
||||
if(mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt.connected() && !topic.isEmpty()) {
|
||||
if(mqtt != NULL && mqttHandler != NULL && WiFi.getMode() != WIFI_AP && WiFi.status() == WL_CONNECTED && mqtt->connected() && !topic.isEmpty()) {
|
||||
mqttHandler->publishTemperatures(&config, &hw);
|
||||
}
|
||||
debugD("Used %d ms to update temperature", millis()-start);
|
||||
@@ -334,8 +359,9 @@ void loop() {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
wifiConnected = false;
|
||||
Debug.stop();
|
||||
WiFi_connect();
|
||||
//WiFi_connect();
|
||||
} else {
|
||||
wifiReconnectCount = 0;
|
||||
if(!wifiConnected) {
|
||||
wifiConnected = true;
|
||||
|
||||
@@ -400,17 +426,16 @@ void loop() {
|
||||
}
|
||||
|
||||
if (mqttEnabled || config.isMqttChanged()) {
|
||||
mqtt.loop();
|
||||
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
|
||||
if(!mqtt.connected() || config.isMqttChanged()) {
|
||||
if(mqtt == NULL || !mqtt->connected() || config.isMqttChanged()) {
|
||||
MQTT_connect();
|
||||
}
|
||||
} else if(mqtt.connected()) {
|
||||
mqtt.disconnect();
|
||||
} else if(mqtt != NULL && mqtt->connected()) {
|
||||
mqttClient->stop();
|
||||
mqtt->disconnect();
|
||||
}
|
||||
|
||||
if(eapi != NULL && ntpEnabled) {
|
||||
if(eapi->loop() && mqttHandler != NULL && mqtt.connected()) {
|
||||
if(eapi->loop() && mqtt != NULL && mqttHandler != NULL && mqtt->connected()) {
|
||||
mqttHandler->publishPrices(eapi);
|
||||
}
|
||||
}
|
||||
@@ -430,7 +455,11 @@ void loop() {
|
||||
}
|
||||
config.ackEntsoeChange();
|
||||
}
|
||||
|
||||
ws.loop();
|
||||
}
|
||||
if(mqtt != NULL) { // Run loop regardless, to let MQTT do its work.
|
||||
mqtt->loop();
|
||||
delay(10); // Needed to preserve power. After adding this, the voltage is super smooth on a HAN powered device
|
||||
}
|
||||
} else {
|
||||
if(dnsServer != NULL) {
|
||||
@@ -442,26 +471,29 @@ void loop() {
|
||||
hw.ledBlink(LED_INTERNAL, 1);
|
||||
}
|
||||
}
|
||||
ws.loop();
|
||||
}
|
||||
|
||||
if(config.isMeterChanged()) {
|
||||
config.getMeterConfig(meterConfig);
|
||||
setupHanPort(gpioConfig.hanPin, meterConfig.type);
|
||||
setupHanPort(gpioConfig.hanPin, meterConfig.baud, meterConfig.parity, meterConfig.invert);
|
||||
config.ackMeterChanged();
|
||||
delete hc;
|
||||
hc = NULL;
|
||||
}
|
||||
delay(1);
|
||||
|
||||
readHanPort();
|
||||
ws.loop();
|
||||
delay(1); // Needed for auto modem sleep
|
||||
}
|
||||
|
||||
void setupHanPort(int pin, int newMeterType) {
|
||||
debugI("Setting up HAN on pin %d for meter type %d", pin, newMeterType);
|
||||
void setupHanPort(uint8_t pin, uint32_t baud, uint8_t parityOrdinal, bool invert) {
|
||||
if(Debug.isActive(RemoteDebug::INFO)) Debug.printf("(setupHanPort) Setting up HAN on pin %d with baud %d and parity %d\n", pin, baud, parityOrdinal);
|
||||
|
||||
HardwareSerial *hwSerial = NULL;
|
||||
if(pin == 3) {
|
||||
if(pin == 3 || pin == 113) {
|
||||
hwSerial = &Serial;
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
if(pin == 9) {
|
||||
hwSerial = &Serial1;
|
||||
@@ -479,42 +511,77 @@ void setupHanPort(int pin, int newMeterType) {
|
||||
if(hwSerial != NULL) {
|
||||
debugD("Hardware serial");
|
||||
Serial.flush();
|
||||
switch(newMeterType) {
|
||||
case METER_TYPE_KAMSTRUP:
|
||||
case METER_TYPE_OMNIPOWER:
|
||||
hwSerial->begin(2400, SERIAL_8N1);
|
||||
#if defined(ESP8266)
|
||||
SerialConfig serialConfig;
|
||||
#elif defined(ESP32)
|
||||
uint32_t serialConfig;
|
||||
#endif
|
||||
switch(parityOrdinal) {
|
||||
case 2:
|
||||
serialConfig = SERIAL_7N1;
|
||||
break;
|
||||
case 3:
|
||||
serialConfig = SERIAL_8N1;
|
||||
break;
|
||||
case 10:
|
||||
serialConfig = SERIAL_7E1;
|
||||
break;
|
||||
default:
|
||||
hwSerial->begin(2400, SERIAL_8E1);
|
||||
serialConfig = SERIAL_8E1;
|
||||
break;
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
hwSerial->begin(baud, serialConfig, -1, -1, invert);
|
||||
#else
|
||||
hwSerial->begin(baud, serialConfig, SERIAL_FULL, 1, invert);
|
||||
#endif
|
||||
|
||||
#if defined(ESP8266)
|
||||
if(pin == 3) {
|
||||
debugI("Switching UART0 to pin 1 & 3");
|
||||
Serial.pins(1,3);
|
||||
} else if(pin == 113) {
|
||||
debugI("Switching UART0 to pin 15 & 13");
|
||||
Serial.pins(15,13);
|
||||
}
|
||||
#endif
|
||||
|
||||
hanSerial = hwSerial;
|
||||
} else {
|
||||
debugD("Software serial");
|
||||
Serial.flush();
|
||||
SoftwareSerial *swSerial = new SoftwareSerial(pin);
|
||||
|
||||
if(swSerial != NULL) {
|
||||
swSerial->end();
|
||||
delete swSerial;
|
||||
}
|
||||
swSerial = new SoftwareSerial(pin);
|
||||
|
||||
switch(newMeterType) {
|
||||
case METER_TYPE_KAMSTRUP:
|
||||
case METER_TYPE_OMNIPOWER:
|
||||
swSerial->begin(2400, SWSERIAL_8N1);
|
||||
SoftwareSerialConfig serialConfig;
|
||||
switch(parityOrdinal) {
|
||||
case 2:
|
||||
serialConfig = SWSERIAL_7N1;
|
||||
break;
|
||||
case 3:
|
||||
serialConfig = SWSERIAL_8N1;
|
||||
break;
|
||||
case 10:
|
||||
serialConfig = SWSERIAL_7E1;
|
||||
break;
|
||||
default:
|
||||
swSerial->begin(2400, SWSERIAL_8E1);
|
||||
serialConfig = SWSERIAL_8E1;
|
||||
break;
|
||||
}
|
||||
|
||||
SoftwareSerial *swSerial = new SoftwareSerial(pin, -1, invert);
|
||||
swSerial->begin(baud, serialConfig);
|
||||
hanSerial = swSerial;
|
||||
|
||||
Serial.end();
|
||||
Serial.begin(115200);
|
||||
}
|
||||
|
||||
hanReader.setup(hanSerial, &Debug);
|
||||
hanReader.setEncryptionKey(meterConfig.encryptionKey);
|
||||
hanReader.setAuthenticationKey(meterConfig.authenticationKey);
|
||||
|
||||
// Compensate for the known Kaifa bug
|
||||
hanReader.compensateFor09HeaderBug = (newMeterType == 1);
|
||||
|
||||
// Empty buffer before starting
|
||||
while (hanSerial->available() > 0) {
|
||||
hanSerial->read();
|
||||
@@ -528,13 +595,13 @@ void errorBlink() {
|
||||
for(;lastError < 3;lastError++) {
|
||||
switch(lastError) {
|
||||
case 0:
|
||||
if(lastErrorBlink - lastSuccessfulRead > 30000) {
|
||||
if(lastErrorBlink - meterState.getLastUpdateMillis() > 30000) {
|
||||
hw.ledBlink(LED_RED, 1); // If no message received from AMS in 30 sec, blink once
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if(mqttEnabled && mqtt.lastError() != 0) {
|
||||
if(mqttEnabled && mqtt != NULL && mqtt->lastError() != 0) {
|
||||
hw.ledBlink(LED_RED, 2); // If MQTT error, blink twice
|
||||
return;
|
||||
}
|
||||
@@ -586,107 +653,152 @@ void swapWifiMode() {
|
||||
}
|
||||
}
|
||||
|
||||
void mqttMessageReceived(String &topic, String &payload)
|
||||
{
|
||||
|
||||
if (Debug.isActive(RemoteDebug::DEBUG)) {
|
||||
debugD("Incoming MQTT message: [%s] %s", topic.c_str(), payload.c_str());
|
||||
}
|
||||
|
||||
// Do whatever needed here...
|
||||
// Ideas could be to query for values or to initiate OTA firmware update
|
||||
}
|
||||
|
||||
int currentMeterType = 0;
|
||||
int len = 0;
|
||||
uint8_t buf[BUF_SIZE];
|
||||
int currentMeterType = -1;
|
||||
void readHanPort() {
|
||||
if (hanReader.read()) {
|
||||
lastSuccessfulRead = millis();
|
||||
delay(1);
|
||||
if(!hanSerial->available()) return;
|
||||
|
||||
if(meterConfig.type > 0) {
|
||||
if(!hw.ledBlink(LED_GREEN, 1))
|
||||
hw.ledBlink(LED_INTERNAL, 1);
|
||||
|
||||
AmsData data(meterConfig.type, meterConfig.substituteMissing, hanReader);
|
||||
if(data.getListType() > 0) {
|
||||
if(mqttEnabled && mqttHandler != NULL) {
|
||||
if(mqttHandler->publish(&data, &meterState)) {
|
||||
if(data.getListType() == 3 && eapi != NULL) {
|
||||
mqttHandler->publishPrices(eapi);
|
||||
}
|
||||
if(data.getListType() >= 2) {
|
||||
mqttHandler->publishSystem(&hw);
|
||||
}
|
||||
time_t now = time(nullptr);
|
||||
if(now < EPOCH_2021_01_01 || data.getListType() == 3) {
|
||||
if(data.getMeterTimestamp() > EPOCH_2021_01_01 || !ntpEnabled) {
|
||||
debugI("Using timestamp from meter");
|
||||
now = data.getMeterTimestamp();
|
||||
} else if(data.getPackageTimestamp() > EPOCH_2021_01_01) {
|
||||
debugI("Using timestamp from meter (DLMS)");
|
||||
now = data.getPackageTimestamp();
|
||||
}
|
||||
if(now > EPOCH_2021_01_01) {
|
||||
timeval tv { now, 0};
|
||||
settimeofday(&tv, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
mqtt.loop();
|
||||
delay(10);
|
||||
if(currentMeterType == -1) {
|
||||
hanSerial->readBytes(buf, BUF_SIZE);
|
||||
currentMeterType = 0;
|
||||
return;
|
||||
}
|
||||
if(currentMeterType == 0) {
|
||||
uint8_t flag = hanSerial->read();
|
||||
if(flag == 0x7E || flag == 0x68) currentMeterType = 1;
|
||||
else currentMeterType = 2;
|
||||
hanSerial->readBytes(buf, BUF_SIZE);
|
||||
return;
|
||||
}
|
||||
CosemDateTime timestamp = {0};
|
||||
AmsData data;
|
||||
if(currentMeterType == 1) {
|
||||
while(hanSerial->available()) {
|
||||
buf[len++] = hanSerial->read();
|
||||
delay(1);
|
||||
}
|
||||
if(len > 0) {
|
||||
if(len >= BUF_SIZE) {
|
||||
hanSerial->readBytes(buf, BUF_SIZE);
|
||||
len = 0;
|
||||
debugI("Buffer overflow, resetting");
|
||||
return;
|
||||
}
|
||||
int pos = HDLC_validate((uint8_t *) buf, len, hc, ×tamp);
|
||||
if(pos == HDLC_FRAME_INCOMPLETE) {
|
||||
return;
|
||||
}
|
||||
for(int i = len; i<BUF_SIZE; i++) {
|
||||
buf[i] = 0x00;
|
||||
}
|
||||
if(pos == HDLC_ENCRYPTION_CONFIG_MISSING) {
|
||||
hc = new HDLCConfig();
|
||||
memcpy(hc->encryption_key, meterConfig.encryptionKey, 16);
|
||||
memcpy(hc->authentication_key, meterConfig.authenticationKey, 16);
|
||||
}
|
||||
if(Debug.isActive(RemoteDebug::DEBUG)) {
|
||||
debugD("Frame dump (%db):", len);
|
||||
debugPrint(buf, 0, len);
|
||||
if(hc != NULL) {
|
||||
debugD("System title:");
|
||||
debugPrint(hc->system_title, 0, 8);
|
||||
debugD("Initialization vector:");
|
||||
debugPrint(hc->initialization_vector, 0, 12);
|
||||
debugD("Additional authenticated data:");
|
||||
debugPrint(hc->additional_authenticated_data, 0, 17);
|
||||
debugD("Authentication tag:");
|
||||
debugPrint(hc->authentication_tag, 0, 12);
|
||||
}
|
||||
meterState.apply(data);
|
||||
}
|
||||
len = 0;
|
||||
if(pos > 0) {
|
||||
while(hanSerial->available()) hanSerial->read();
|
||||
debugI("Valid HDLC, start at %d", pos);
|
||||
data = IEC6205675(((char *) (buf)) + pos, meterState.getMeterType(), timestamp);
|
||||
} else {
|
||||
debugW("Invalid HDLC, returned with %d", pos);
|
||||
currentMeterType = 0;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Auto detect meter if not set
|
||||
for(int i = 1; i <= 3; i++) {
|
||||
String list;
|
||||
switch(i) {
|
||||
case 1:
|
||||
list = hanReader.getString((int) Kaifa_List1Phase::ListVersionIdentifier);
|
||||
break;
|
||||
case 2:
|
||||
list = hanReader.getString((int) Aidon_List1Phase::ListVersionIdentifier);
|
||||
break;
|
||||
case 3:
|
||||
list = hanReader.getString((int) Kamstrup_List1Phase::ListVersionIdentifier);
|
||||
break;
|
||||
}
|
||||
if(!list.isEmpty()) {
|
||||
list.toLowerCase();
|
||||
if(list.startsWith("kfm")) {
|
||||
meterConfig.type = 1;
|
||||
config.setMeterConfig(meterConfig);
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Kaifa meter");
|
||||
break;
|
||||
} else if(list.startsWith("aidon")) {
|
||||
meterConfig.type = 2;
|
||||
config.setMeterConfig(meterConfig);
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Aidon meter");
|
||||
break;
|
||||
} else if(list.startsWith("kamstrup")) {
|
||||
meterConfig.type = 3;
|
||||
config.setMeterConfig(meterConfig);
|
||||
if(Debug.isActive(RemoteDebug::INFO)) debugI("Detected Kamstrup meter");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if(currentMeterType == 2) {
|
||||
String payload = hanSerial->readString();
|
||||
data = IEC6205621(payload);
|
||||
if(data.getListType() == 0) {
|
||||
currentMeterType = 1;
|
||||
} else {
|
||||
if(Debug.isActive(RemoteDebug::DEBUG)) {
|
||||
debugD("Frame dump: %d", payload.length());
|
||||
debugD("%s", payload.c_str());
|
||||
}
|
||||
hanReader.compensateFor09HeaderBug = (meterConfig.type == 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch parity if meter is still not detected
|
||||
if(meterConfig.type == 0 && millis() - lastSuccessfulRead > 10000) {
|
||||
lastSuccessfulRead = millis();
|
||||
debugD("No data for current setting, switching parity");
|
||||
Serial.flush();
|
||||
if(++currentMeterType == 4) currentMeterType = 1;
|
||||
setupHanPort(gpioConfig.hanPin, currentMeterType);
|
||||
if(data.getListType() > 0) {
|
||||
if(!hw.ledBlink(LED_GREEN, 1))
|
||||
hw.ledBlink(LED_INTERNAL, 1);
|
||||
if(mqttEnabled && mqttHandler != NULL && mqtt != NULL) {
|
||||
if(mqttHandler->publish(&data, &meterState)) {
|
||||
if(data.getListType() == 3 && eapi != NULL) {
|
||||
mqttHandler->publishPrices(eapi);
|
||||
}
|
||||
if(data.getListType() >= 2) {
|
||||
mqttHandler->publishSystem(&hw);
|
||||
}
|
||||
}
|
||||
if(mqtt != NULL) {
|
||||
mqtt->loop();
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
time_t now = time(nullptr);
|
||||
if(now < EPOCH_2021_01_01 && data.getListType() == 3 && !ntpEnabled) {
|
||||
if(data.getMeterTimestamp() > EPOCH_2021_01_01) {
|
||||
debugI("Using timestamp from meter");
|
||||
now = data.getMeterTimestamp();
|
||||
} else if(data.getPackageTimestamp() > EPOCH_2021_01_01) {
|
||||
debugI("Using timestamp from meter (DLMS)");
|
||||
now = data.getPackageTimestamp();
|
||||
}
|
||||
if(now > EPOCH_2021_01_01) {
|
||||
timeval tv { now, 0};
|
||||
settimeofday(&tv, nullptr);
|
||||
}
|
||||
}
|
||||
if(meterState.getListType() < 3 && now > EPOCH_2021_01_01) {
|
||||
// TODO: Load an estimated value from dayplot
|
||||
}
|
||||
|
||||
meterState.apply(data);
|
||||
|
||||
if(ds.update(&data)) {
|
||||
debugI("Saving day plot");
|
||||
ds.save();
|
||||
}
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
|
||||
void 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("");
|
||||
}
|
||||
|
||||
unsigned long wifiTimeout = WIFI_CONNECTION_TIMEOUT;
|
||||
unsigned long lastWifiRetry = -WIFI_CONNECTION_TIMEOUT;
|
||||
void WiFi_connect() {
|
||||
@@ -697,6 +809,46 @@ void WiFi_connect() {
|
||||
lastWifiRetry = millis();
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
if(WiFi.getMode() != WIFI_OFF) {
|
||||
if(wifiReconnectCount > 3) {
|
||||
ESP.restart();
|
||||
return;
|
||||
}
|
||||
if (Debug.isActive(RemoteDebug::INFO)) debugI("Not connected to WiFi, closing resources");
|
||||
if(mqtt != NULL) {
|
||||
mqtt->disconnect();
|
||||
mqtt->loop();
|
||||
yield();
|
||||
delete mqtt;
|
||||
mqtt = NULL;
|
||||
ws.setMqtt(NULL);
|
||||
}
|
||||
|
||||
if(mqttClient != NULL) {
|
||||
mqttClient->stop();
|
||||
delete mqttClient;
|
||||
mqttClient = NULL;
|
||||
if(mqttSecureClient != NULL) {
|
||||
mqttSecureClient = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(ESP8266)
|
||||
WiFiClient::stopAll();
|
||||
#endif
|
||||
|
||||
MDNS.end();
|
||||
WiFi.persistent(false);
|
||||
WiFi.disconnect(true);
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.enableAP(false);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
yield();
|
||||
wifiTimeout = 5000;
|
||||
return;
|
||||
}
|
||||
wifiTimeout = WIFI_CONNECTION_TIMEOUT;
|
||||
|
||||
WiFiConfig wifi;
|
||||
if(!config.getWiFiConfig(wifi) || strlen(wifi.ssid) == 0) {
|
||||
swapWifiMode();
|
||||
@@ -705,11 +857,8 @@ void WiFi_connect() {
|
||||
|
||||
if (Debug.isActive(RemoteDebug::INFO)) debugI("Connecting to WiFi network: %s", wifi.ssid);
|
||||
|
||||
MDNS.end();
|
||||
WiFi.disconnect();
|
||||
yield();
|
||||
wifiReconnectCount++;
|
||||
|
||||
WiFi.enableAP(false);
|
||||
WiFi.mode(WIFI_STA);
|
||||
if(strlen(wifi.ip) > 0) {
|
||||
IPAddress ip, gw, sn(255,255,255,0), dns1, dns2;
|
||||
@@ -721,7 +870,8 @@ void WiFi_connect() {
|
||||
WiFi.config(ip, gw, sn, dns1, dns2);
|
||||
} else {
|
||||
#if defined(ESP32)
|
||||
WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); // Workaround to make DHCP hostname work for ESP32. See: https://github.com/espressif/arduino-esp32/issues/2537
|
||||
// This trick does not work anymore...
|
||||
// WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); // Workaround to make DHCP hostname work for ESP32. See: https://github.com/espressif/arduino-esp32/issues/2537
|
||||
#endif
|
||||
}
|
||||
if(strlen(wifi.hostname) > 0) {
|
||||
@@ -730,10 +880,15 @@ void WiFi_connect() {
|
||||
#elif defined(ESP32)
|
||||
WiFi.setHostname(wifi.hostname);
|
||||
#endif
|
||||
}
|
||||
WiFi.setAutoReconnect(true);
|
||||
WiFi.persistent(true);
|
||||
if(WiFi.begin(wifi.ssid, wifi.psk)) {
|
||||
yield();
|
||||
} else {
|
||||
if (Debug.isActive(RemoteDebug::ERROR)) debugI("Unable to start WiFi");
|
||||
}
|
||||
WiFi.begin(wifi.ssid, wifi.psk);
|
||||
yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long lastMqttRetry = -10000;
|
||||
@@ -746,24 +901,28 @@ void MQTT_connect() {
|
||||
config.ackMqttChange();
|
||||
return;
|
||||
}
|
||||
if(millis() - lastMqttRetry < (mqtt.lastError() == 0 || config.isMqttChanged() ? 5000 : 30000)) {
|
||||
if(mqtt != NULL) {
|
||||
if(millis() - lastMqttRetry < (mqtt->lastError() == 0 || config.isMqttChanged() ? 5000 : 30000)) {
|
||||
yield();
|
||||
return;
|
||||
}
|
||||
lastMqttRetry = millis();
|
||||
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugD("Disconnecting MQTT before connecting");
|
||||
}
|
||||
|
||||
mqtt->disconnect();
|
||||
yield();
|
||||
return;
|
||||
} else {
|
||||
mqtt = new MQTTClient(512);
|
||||
ws.setMqtt(mqtt);
|
||||
}
|
||||
lastMqttRetry = millis();
|
||||
|
||||
mqttEnabled = true;
|
||||
ws.setMqttEnabled(true);
|
||||
payloadFormat = mqttConfig.payloadFormat;
|
||||
topic = String(mqttConfig.publishTopic);
|
||||
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugD("Disconnecting MQTT before connecting");
|
||||
}
|
||||
|
||||
mqtt.disconnect();
|
||||
yield();
|
||||
|
||||
if(mqttHandler != NULL) {
|
||||
delete mqttHandler;
|
||||
mqttHandler = NULL;
|
||||
@@ -771,94 +930,102 @@ void MQTT_connect() {
|
||||
|
||||
switch(mqttConfig.payloadFormat) {
|
||||
case 0:
|
||||
mqttHandler = new JsonMqttHandler(&mqtt, mqttConfig.clientId, mqttConfig.publishTopic, &hw);
|
||||
mqttHandler = new JsonMqttHandler(mqtt, mqttConfig.clientId, mqttConfig.publishTopic, &hw);
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
mqttHandler = new RawMqttHandler(&mqtt, mqttConfig.publishTopic, mqttConfig.payloadFormat == 2);
|
||||
mqttHandler = new RawMqttHandler(mqtt, mqttConfig.publishTopic, mqttConfig.payloadFormat == 2);
|
||||
break;
|
||||
case 3:
|
||||
DomoticzConfig domo;
|
||||
config.getDomoticzConfig(domo);
|
||||
mqttHandler = new DomoticzMqttHandler(&mqtt, domo);
|
||||
mqttHandler = new DomoticzMqttHandler(mqtt, domo);
|
||||
break;
|
||||
}
|
||||
|
||||
WiFiClientSecure *secureClient = NULL;
|
||||
Client *client = NULL;
|
||||
if(mqttConfig.ssl) {
|
||||
debugI("MQTT SSL is configured");
|
||||
|
||||
secureClient = new WiFiClientSecure();
|
||||
if(mqttSecureClient == NULL) {
|
||||
mqttSecureClient = new WiFiClientSecure();
|
||||
}
|
||||
#if defined(ESP8266)
|
||||
secureClient->setBufferSizes(512, 512);
|
||||
mqttSecureClient->setBufferSizes(512, 512);
|
||||
#endif
|
||||
|
||||
if(SPIFFS.begin()) {
|
||||
if(LittleFS.begin()) {
|
||||
char *ca = NULL;
|
||||
char *cert = NULL;
|
||||
char *key = NULL;
|
||||
File file;
|
||||
|
||||
if(SPIFFS.exists(FILE_MQTT_CA)) {
|
||||
if(LittleFS.exists(FILE_MQTT_CA)) {
|
||||
debugI("Found MQTT CA file");
|
||||
File file = SPIFFS.open(FILE_MQTT_CA, "r");
|
||||
secureClient->loadCACert(file, file.size());
|
||||
file = LittleFS.open(FILE_MQTT_CA, "r");
|
||||
#if defined(ESP8266)
|
||||
BearSSL::X509List *serverTrustedCA = new BearSSL::X509List(file);
|
||||
mqttSecureClient->setTrustAnchors(serverTrustedCA);
|
||||
#elif defined(ESP32)
|
||||
mqttSecureClient->loadCACert(file, file.size());
|
||||
#endif
|
||||
}
|
||||
if(SPIFFS.exists(FILE_MQTT_CERT)) {
|
||||
debugI("Found MQTT certificate file");
|
||||
File file = SPIFFS.open(FILE_MQTT_CERT, "r");
|
||||
secureClient->loadCertificate(file, file.size());
|
||||
|
||||
if(LittleFS.exists(FILE_MQTT_CERT) && LittleFS.exists(FILE_MQTT_KEY)) {
|
||||
#if defined(ESP8266)
|
||||
file = LittleFS.open(FILE_MQTT_CERT, "r");
|
||||
BearSSL::X509List *serverCertList = new BearSSL::X509List(file);
|
||||
file = LittleFS.open(FILE_MQTT_KEY, "r");
|
||||
BearSSL::PrivateKey *serverPrivKey = new BearSSL::PrivateKey(file);
|
||||
mqttSecureClient->setClientRSACert(serverCertList, serverPrivKey);
|
||||
#elif defined(ESP32)
|
||||
debugI("Found MQTT certificate file");
|
||||
file = LittleFS.open(FILE_MQTT_CERT, "r");
|
||||
mqttSecureClient->loadCertificate(file, file.size());
|
||||
|
||||
debugI("Found MQTT key file");
|
||||
file = LittleFS.open(FILE_MQTT_KEY, "r");
|
||||
mqttSecureClient->loadPrivateKey(file, file.size());
|
||||
#endif
|
||||
}
|
||||
if(SPIFFS.exists(FILE_MQTT_KEY)) {
|
||||
debugI("Found MQTT key file");
|
||||
File file = SPIFFS.open(FILE_MQTT_KEY, "r");
|
||||
secureClient->loadPrivateKey(file, file.size());
|
||||
}
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
}
|
||||
client = secureClient;
|
||||
} else {
|
||||
client = new WiFiClient();
|
||||
mqttClient = mqttSecureClient;
|
||||
} else if(mqttClient == NULL) {
|
||||
mqttClient = new WiFiClient();
|
||||
}
|
||||
|
||||
if(Debug.isActive(RemoteDebug::INFO)) {
|
||||
debugI("Connecting to MQTT %s:%d", mqttConfig.host, mqttConfig.port);
|
||||
}
|
||||
mqtt.begin(mqttConfig.host, mqttConfig.port, *client);
|
||||
|
||||
mqtt->begin(mqttConfig.host, mqttConfig.port, *mqttClient);
|
||||
|
||||
#if defined(ESP8266)
|
||||
if(secureClient) {
|
||||
time_t epoch = time(nullptr);
|
||||
debugD("Setting NTP time %i for secure MQTT connection", epoch);
|
||||
secureClient->setX509Time(epoch);
|
||||
}
|
||||
if(mqttSecureClient) {
|
||||
time_t epoch = time(nullptr);
|
||||
debugD("Setting NTP time %i for secure MQTT connection", epoch);
|
||||
mqttSecureClient->setX509Time(epoch);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Connect to a unsecure or secure MQTT server
|
||||
if ((strlen(mqttConfig.username) == 0 && mqtt.connect(mqttConfig.clientId)) ||
|
||||
(strlen(mqttConfig.username) > 0 && mqtt.connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
|
||||
if ((strlen(mqttConfig.username) == 0 && mqtt->connect(mqttConfig.clientId)) ||
|
||||
(strlen(mqttConfig.username) > 0 && mqtt->connect(mqttConfig.clientId, mqttConfig.username, mqttConfig.password))) {
|
||||
if (Debug.isActive(RemoteDebug::INFO)) debugI("Successfully connected to MQTT!");
|
||||
config.ackMqttChange();
|
||||
|
||||
// Subscribe to the chosen MQTT topic, if set in configuration
|
||||
if (strlen(mqttConfig.subscribeTopic) > 0) {
|
||||
mqtt.subscribe(mqttConfig.subscribeTopic);
|
||||
if (Debug.isActive(RemoteDebug::INFO)) debugI(" Subscribing to [%s]\r\n", mqttConfig.subscribeTopic);
|
||||
}
|
||||
|
||||
if(mqttHandler != NULL) {
|
||||
mqttHandler->publishSystem(&hw);
|
||||
}
|
||||
} else {
|
||||
if (Debug.isActive(RemoteDebug::ERROR)) {
|
||||
debugE("Failed to connect to MQTT");
|
||||
#if defined(ESP8266)
|
||||
if(secureClient) {
|
||||
char buf[64];
|
||||
secureClient->getLastSSLError(buf,64);
|
||||
Debug.println(buf);
|
||||
}
|
||||
#endif
|
||||
debugE("Failed to connect to MQTT: %d", mqtt->lastError());
|
||||
#if defined(ESP8266)
|
||||
if(mqttSecureClient) {
|
||||
char buf[64];
|
||||
mqttSecureClient->getLastSSLError(buf,64);
|
||||
Debug.println(buf);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
yield();
|
||||
|
||||
145
src/HwTools.cpp
145
src/HwTools.cpp
@@ -14,18 +14,35 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
|
||||
config->tempSensorPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->vccPin > 0 && config->vccPin < 40) {
|
||||
getAdcChannel(config->vccPin, voltAdc);
|
||||
if(voltAdc.unit != 0xFF) {
|
||||
#if defined(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->vccPin > 0 && config->vccPin < 40) {
|
||||
pinMode(config->vccPin, INPUT);
|
||||
} else {
|
||||
config->vccPin = 0xFF;
|
||||
}
|
||||
|
||||
if(config->ledPin > 0 && config->ledPin < 40) {
|
||||
pinMode(config->ledPin, OUTPUT);
|
||||
ledOff(LED_INTERNAL);
|
||||
@@ -55,19 +72,128 @@ void HwTools::setup(GpioConfig* config, AmsConfiguration* amsConf) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
double HwTools::getVcc() {
|
||||
double volts = 0.0;
|
||||
if(config->vccPin != 0xFF) {
|
||||
#if defined(ESP8266)
|
||||
volts = (analogRead(config->vccPin) / 1024.0) * 3.3;
|
||||
uint32_t x = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
x += analogRead(config->vccPin);
|
||||
}
|
||||
volts = x / 10240;
|
||||
#elif defined(ESP32)
|
||||
volts = (analogRead(config->vccPin) / 4095.0) * 3.3;
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#if defined(ESP8266)
|
||||
volts = ((double) ESP.getVcc()) / 1024.0;
|
||||
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;
|
||||
@@ -217,6 +343,7 @@ bool HwTools::ledBlink(uint8_t color, uint8_t blink) {
|
||||
if(i != blink)
|
||||
delay(50);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HwTools::writeLedPin(uint8_t color, uint8_t state) {
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#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>
|
||||
@@ -26,6 +29,11 @@ struct TempSensorData {
|
||||
bool changed;
|
||||
};
|
||||
|
||||
struct AdcConfig {
|
||||
uint8_t unit;
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
class HwTools {
|
||||
public:
|
||||
void setup(GpioConfig*, AmsConfiguration*);
|
||||
@@ -43,6 +51,10 @@ public:
|
||||
|
||||
HwTools() {};
|
||||
private:
|
||||
AdcConfig voltAdc, tempAdc;
|
||||
#if defined(ESP32)
|
||||
esp_adc_cal_characteristics_t* voltAdcChar, tempAdcChar;
|
||||
#endif
|
||||
GpioConfig* config;
|
||||
AmsConfiguration* amsConf;
|
||||
bool tempSensorInit;
|
||||
@@ -53,6 +65,7 @@ private:
|
||||
|
||||
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
|
||||
461
src/IEC6205675.cpp
Normal file
461
src/IEC6205675.cpp
Normal file
@@ -0,0 +1,461 @@
|
||||
#include "IEC6205675.h"
|
||||
#include "lwip/def.h"
|
||||
#include "Timezone.h"
|
||||
|
||||
IEC6205675::IEC6205675(const char* d, uint8_t useMeterType, CosemDateTime packageTimestamp) {
|
||||
uint32_t ui;
|
||||
double val;
|
||||
char str[64];
|
||||
|
||||
this->packageTimestamp = getTimestamp(packageTimestamp);
|
||||
|
||||
ui = getNumber(AMS_OBIS_ACTIVE_IMPORT, sizeof(AMS_OBIS_ACTIVE_IMPORT), ((char *) (d)));
|
||||
if(ui == 0xFFFFFFFF) {
|
||||
CosemData* data = getCosemDataAt(1, ((char *) (d)));
|
||||
|
||||
// Kaifa special case...
|
||||
if(data->base.type == CosemTypeOctetString) {
|
||||
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 == 3) {
|
||||
data = getCosemDataAt(idx++, ((char *) (d)));
|
||||
switch(data->base.type) {
|
||||
case CosemTypeOctetString: {
|
||||
if(data->oct.length == 0x0C) {
|
||||
AmsOctetTimestamp* ts = (AmsOctetTimestamp*) data;
|
||||
meterTimestamp = getTimestamp(ts->dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
listType = 1;
|
||||
meterType = AmsTypeKaifa;
|
||||
activeImportPower = ntohl(data->dlu.data);
|
||||
lastUpdateMillis = millis();
|
||||
}
|
||||
// Kaifa end
|
||||
} else {
|
||||
listType = 1;
|
||||
activeImportPower = ui;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120};
|
||||
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60};
|
||||
Timezone tz(CEST, CET);
|
||||
|
||||
if(meterType == AmsTypeKamstrup || meterType == AmsTypeAidon) {
|
||||
this->packageTimestamp = this->packageTimestamp > 0 ? tz.toUTC(this->packageTimestamp) : 0;
|
||||
}
|
||||
|
||||
ui = getString(AMS_OBIS_VERSION, sizeof(AMS_OBIS_VERSION), ((char *) (d)), str);
|
||||
if(ui > 0) {
|
||||
listId = String(str);
|
||||
}
|
||||
|
||||
ui = getNumber(AMS_OBIS_ACTIVE_EXPORT, sizeof(AMS_OBIS_ACTIVE_EXPORT), ((char *) (d)));
|
||||
if(ui != 0xFFFFFFFF) {
|
||||
activeExportPower = ui;
|
||||
}
|
||||
|
||||
ui = getNumber(AMS_OBIS_REACTIVE_IMPORT, sizeof(AMS_OBIS_REACTIVE_IMPORT), ((char *) (d)));
|
||||
if(ui != 0xFFFFFFFF) {
|
||||
reactiveImportPower = ui;
|
||||
}
|
||||
|
||||
ui = getNumber(AMS_OBIS_REACTIVE_EXPORT, sizeof(AMS_OBIS_REACTIVE_EXPORT), ((char *) (d)));
|
||||
if(ui != 0xFFFFFFFF) {
|
||||
reactiveExportPower = ui;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L1, sizeof(AMS_OBIS_VOLTAGE_L1), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l1voltage = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L2, sizeof(AMS_OBIS_VOLTAGE_L2), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l2voltage = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_VOLTAGE_L3, sizeof(AMS_OBIS_VOLTAGE_L3), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l3voltage = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_CURRENT_L1, sizeof(AMS_OBIS_CURRENT_L1), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l1current = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_CURRENT_L2, sizeof(AMS_OBIS_CURRENT_L2), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l2current = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_CURRENT_L3, sizeof(AMS_OBIS_CURRENT_L3), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 2;
|
||||
l3current = val;
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_ACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_IMPORT_COUNT), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 3;
|
||||
activeImportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_ACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_ACTIVE_EXPORT_COUNT), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 3;
|
||||
activeExportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_REACTIVE_IMPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_IMPORT_COUNT), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 3;
|
||||
reactiveImportCounter = val / 1000.0;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_REACTIVE_EXPORT_COUNT, sizeof(AMS_OBIS_REACTIVE_EXPORT_COUNT), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
listType = 3;
|
||||
reactiveExportCounter = val / 1000.0;
|
||||
}
|
||||
|
||||
ui = getString(AMS_OBIS_METER_MODEL, sizeof(AMS_OBIS_METER_MODEL), ((char *) (d)), str);
|
||||
if(ui > 0) {
|
||||
meterModel = String(str);
|
||||
} else {
|
||||
ui = getString(AMS_OBIS_METER_MODEL_2, sizeof(AMS_OBIS_METER_MODEL_2), ((char *) (d)), str);
|
||||
if(ui > 0) {
|
||||
meterModel = String(str);
|
||||
}
|
||||
}
|
||||
|
||||
ui = getString(AMS_OBIS_METER_ID, sizeof(AMS_OBIS_METER_ID), ((char *) (d)), str);
|
||||
if(ui > 0) {
|
||||
meterId = String(str);
|
||||
} else {
|
||||
ui = getString(AMS_OBIS_METER_ID_2, sizeof(AMS_OBIS_METER_ID_2), ((char *) (d)), str);
|
||||
if(ui > 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) {
|
||||
this->meterTimestamp = tz.toUTC(ts);
|
||||
} else {
|
||||
meterTimestamp = ts;
|
||||
}
|
||||
}
|
||||
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR, sizeof(AMS_OBIS_POWER_FACTOR), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
powerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L1, sizeof(AMS_OBIS_POWER_FACTOR_L1), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
l1PowerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L2, sizeof(AMS_OBIS_POWER_FACTOR_L2), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
l2PowerFactor = val;
|
||||
}
|
||||
val = getNumber(AMS_OBIS_POWER_FACTOR_L3, sizeof(AMS_OBIS_POWER_FACTOR_L3), ((char *) (d)));
|
||||
if(val != 0xFFFFFFFF) {
|
||||
l3PowerFactor = val;
|
||||
}
|
||||
|
||||
if(meterType == AmsTypeKamstrup) {
|
||||
activeImportCounter *= 10;
|
||||
activeExportCounter *= 10;
|
||||
reactiveImportCounter *= 10;
|
||||
reactiveExportCounter *= 10;
|
||||
l1current /= 100;
|
||||
l2current /= 100;
|
||||
l3current /= 100;
|
||||
powerFactor /= 100;
|
||||
l1PowerFactor /= 100;
|
||||
l2PowerFactor /= 100;
|
||||
l3PowerFactor /= 100;
|
||||
}
|
||||
|
||||
lastUpdateMillis = millis();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
double val = 0xFFFFFFFF;
|
||||
if(item != NULL) {
|
||||
char* pos = ((char*) item);
|
||||
switch(item->base.type) {
|
||||
case CosemTypeLongUnsigned:
|
||||
val = ntohs(item->lu.data);
|
||||
pos += 3;
|
||||
break;
|
||||
case CosemTypeDLongUnsigned:
|
||||
val = ntohl(item->dlu.data);
|
||||
pos += 5;
|
||||
break;
|
||||
case CosemTypeLongSigned:
|
||||
val = ntohs(item->lu.data);
|
||||
pos += 3;
|
||||
break;
|
||||
}
|
||||
if(*pos++ == 0x02 && *pos++ == 0x02) {
|
||||
int8_t scale = *++pos;
|
||||
val *= pow(10, scale);
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
65
src/IEC6205675.h
Normal file
65
src/IEC6205675.h
Normal file
@@ -0,0 +1,65 @@
|
||||
#ifndef _IEC62056_7_5_H
|
||||
#define _IEC62056_7_5_H
|
||||
|
||||
#include "AmsData.h"
|
||||
#include "ams/hdlc.h"
|
||||
|
||||
struct AmsOctetTimestamp {
|
||||
uint8_t type;
|
||||
CosemDateTime dt;
|
||||
} __attribute__((packed));
|
||||
|
||||
class IEC6205675 : public AmsData {
|
||||
public:
|
||||
IEC6205675(const char* payload, uint8_t useMeterType, CosemDateTime packageTimestamp);
|
||||
|
||||
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
|
||||
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
|
||||
232
src/ams/hdlc.cpp
Normal file
232
src/ams/hdlc.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
#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) {
|
||||
if(length < 10)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
int len;
|
||||
int headersize = 3;
|
||||
int footersize = 1;
|
||||
HDLCHeader* h = (HDLCHeader*) d;
|
||||
uint8_t* ptr = (uint8_t*) &h[1];
|
||||
// Frame format type 3
|
||||
if(h->flag == HDLC_FLAG && (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;
|
||||
} else if(h->flag == MBUS_START) {
|
||||
// TODO: Check that the two next bytes are identical
|
||||
|
||||
// Ignore: Control field + Address + Flag
|
||||
ptr += 3;
|
||||
headersize += 3;
|
||||
footersize++;
|
||||
}
|
||||
|
||||
// Extract LLC
|
||||
HDLCLLC* llc = (HDLCLLC*) ptr;
|
||||
ptr += sizeof *llc;
|
||||
headersize += 3;
|
||||
|
||||
if(((*ptr) & 0xFF) == 0x0F) {
|
||||
// Unencrypted APDU
|
||||
int i = 0;
|
||||
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 -99;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Length field
|
||||
len = (ntohs(h->format) & 0xFFFF);
|
||||
|
||||
ptr += 3;
|
||||
headersize += 3;
|
||||
}
|
||||
//len = ceil(len/16.0) * 16; // Technically GCM is 128bit blocks. This works for Austrian meters, but not Danish...
|
||||
if(len + headersize + footersize > length)
|
||||
return HDLC_FRAME_INCOMPLETE;
|
||||
|
||||
//Serial.printf("\nL: %d : %d, %d : %d\n", length, len, headersize, footersize);
|
||||
|
||||
// TODO: FCS
|
||||
|
||||
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
|
||||
|
||||
ptr += 5; // TODO: Come to this number in a proper way...
|
||||
|
||||
// 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 -99;
|
||||
}
|
||||
|
||||
return ptr-d;
|
||||
}
|
||||
|
||||
// Unknown payload
|
||||
return HDLC_UNKNOWN_DATA;
|
||||
}
|
||||
129
src/ams/hdlc.h
Normal file
129
src/ams/hdlc.h
Normal file
@@ -0,0 +1,129 @@
|
||||
#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 MBUS_START 0x68
|
||||
#define MBUS_END 0x16
|
||||
|
||||
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 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);
|
||||
|
||||
#endif
|
||||
@@ -32,6 +32,10 @@ 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);
|
||||
@@ -73,7 +77,7 @@ float EntsoeApi::getValueForHour(time_t cur, uint8_t hour) {
|
||||
}
|
||||
|
||||
bool EntsoeApi::loop() {
|
||||
if(strlen(config->token) == 0)
|
||||
if(strlen(getToken()) == 0)
|
||||
return false;
|
||||
bool ret = false;
|
||||
|
||||
@@ -112,7 +116,7 @@ bool EntsoeApi::loop() {
|
||||
|
||||
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", config->token,
|
||||
"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);
|
||||
@@ -142,7 +146,7 @@ bool EntsoeApi::loop() {
|
||||
|
||||
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", config->token,
|
||||
"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);
|
||||
@@ -179,14 +183,12 @@ bool EntsoeApi::retrieve(const char* url, Stream* doc) {
|
||||
client.setBufferSizes(bufSize, bufSize);
|
||||
}
|
||||
*/
|
||||
|
||||
client.setInsecure();
|
||||
#endif
|
||||
|
||||
client.setInsecure();
|
||||
|
||||
HTTPClient https;
|
||||
#if defined(ESP8266)
|
||||
https.setFollowRedirects(true);
|
||||
#endif
|
||||
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
|
||||
if(https.begin(client, url)) {
|
||||
printD("Connection established");
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
bool loop();
|
||||
|
||||
char* getToken();
|
||||
char* getCurrency();
|
||||
float getValueForHour(uint8_t);
|
||||
float getValueForHour(time_t, uint8_t);
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ bool JsonMqttHandler::publish(AmsData* data, AmsData* previousState) {
|
||||
);
|
||||
return mqtt->publish(topic, json);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw) {
|
||||
@@ -112,7 +113,7 @@ bool JsonMqttHandler::publishTemperatures(AmsConfiguration* config, HwTools* hw)
|
||||
bool JsonMqttHandler::publishPrices(EntsoeApi* eapi) {
|
||||
if(topic.isEmpty() || !mqtt->connected())
|
||||
return false;
|
||||
if(strcmp(eapi->getToken(), "") == 0)
|
||||
if(strlen(eapi->getToken()) == 0)
|
||||
return false;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
|
||||
@@ -19,6 +19,18 @@ bool RawMqttHandler::publish(AmsData* data, AmsData* meterState) {
|
||||
mqtt->publish(topic + "/meter/import/active/accumulated", String(data->getActiveImportCounter(), 2), true, 0);
|
||||
mqtt->publish(topic + "/meter/export/reactive/accumulated", String(data->getReactiveExportCounter(), 2), true, 0);
|
||||
mqtt->publish(topic + "/meter/export/active/accumulated", String(data->getActiveExportCounter(), 2), 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/l1/powerfactor", String(data->getL1PowerFactor(), 2));
|
||||
}
|
||||
if(full || meterState->getL3PowerFactor() != data->getL3PowerFactor()) {
|
||||
mqtt->publish(topic + "/meter/l1/powerfactor", String(data->getL1PowerFactor(), 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()) {
|
||||
|
||||
@@ -2,8 +2,18 @@
|
||||
#include "version.h"
|
||||
#include "AmsStorage.h"
|
||||
#include "hexutils.h"
|
||||
#include "AmsData.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
#include "root/head8266_html.h"
|
||||
#define HEAD_HTML HEAD8266_HTML
|
||||
#define HEAD_HTML_LEN HEAD8266_HTML_LEN
|
||||
#elif defined(ESP32)
|
||||
#include "root/head32_html.h"
|
||||
#define HEAD_HTML HEAD32_HTML
|
||||
#define HEAD_HTML_LEN HEAD32_HTML_LEN
|
||||
#endif
|
||||
|
||||
#include "root/head_html.h"
|
||||
#include "root/foot_html.h"
|
||||
#include "root/index_html.h"
|
||||
#include "root/application_js.h"
|
||||
@@ -20,9 +30,9 @@
|
||||
#include "root/restart_html.h"
|
||||
#include "root/restartwait_html.h"
|
||||
#include "root/boot_css.h"
|
||||
#include "root/gaugemeter_js.h"
|
||||
#include "root/github_svg.h"
|
||||
#include "root/upload_html.h"
|
||||
#include "root/firmware_html.h"
|
||||
#include "root/delete_html.h"
|
||||
#include "root/reset_html.h"
|
||||
#include "root/temperature_html.h"
|
||||
@@ -31,6 +41,9 @@
|
||||
#include "root/data_json.h"
|
||||
#include "root/tempsensor_json.h"
|
||||
#include "root/lowmem_html.h"
|
||||
#include "root/dayplot_json.h"
|
||||
#include "root/monthplot_json.h"
|
||||
#include "root/energyprice_json.h"
|
||||
|
||||
#include "base64.h"
|
||||
|
||||
@@ -39,12 +52,12 @@ AmsWebServer::AmsWebServer(RemoteDebug* Debug, HwTools* hw) {
|
||||
this->hw = hw;
|
||||
}
|
||||
|
||||
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, MQTTClient* mqtt) {
|
||||
void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, MeterConfig* meterConfig, AmsData* meterState, AmsDataStorage* ds) {
|
||||
this->config = config;
|
||||
this->gpioConfig = gpioConfig;
|
||||
this->meterConfig = meterConfig;
|
||||
this->meterState = meterState;
|
||||
this->mqtt = mqtt;
|
||||
this->ds = ds;
|
||||
|
||||
char jsuri[32];
|
||||
snprintf(jsuri, 32, "/application-%s.js", VERSION);
|
||||
@@ -63,9 +76,11 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
|
||||
server.on("/domoticz",HTTP_GET, std::bind(&AmsWebServer::configDomoticzHtml, this));
|
||||
server.on("/entsoe",HTTP_GET, std::bind(&AmsWebServer::configEntsoeHtml, this));
|
||||
server.on("/boot.css", HTTP_GET, std::bind(&AmsWebServer::bootCss, this));
|
||||
server.on("/gaugemeter.js", HTTP_GET, std::bind(&AmsWebServer::gaugemeterJs, this));
|
||||
server.on("/github.svg", HTTP_GET, std::bind(&AmsWebServer::githubSvg, this));
|
||||
server.on("/data.json", HTTP_GET, std::bind(&AmsWebServer::dataJson, this));
|
||||
server.on("/dayplot.json", HTTP_GET, std::bind(&AmsWebServer::dayplotJson, this));
|
||||
server.on("/monthplot.json", HTTP_GET, std::bind(&AmsWebServer::monthplotJson, this));
|
||||
server.on("/energyprice.json", HTTP_GET, std::bind(&AmsWebServer::energyPriceJson, this));
|
||||
|
||||
server.on("/save", HTTP_POST, std::bind(&AmsWebServer::handleSave, this));
|
||||
|
||||
@@ -101,6 +116,10 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
|
||||
mqttEnabled = strlen(mqttConfig.host) > 0;
|
||||
}
|
||||
|
||||
void AmsWebServer::setMqtt(MQTTClient* mqtt) {
|
||||
this->mqtt = mqtt;
|
||||
}
|
||||
|
||||
void AmsWebServer::setTimezone(Timezone* tz) {
|
||||
this->tz = tz;
|
||||
}
|
||||
@@ -191,14 +210,14 @@ void AmsWebServer::temperaturePost() {
|
||||
delay(1);
|
||||
}
|
||||
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger);
|
||||
//if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger);
|
||||
if(config->save()) {
|
||||
printD("Successfully saved temperature sensors");
|
||||
server.sendHeader("Location", String("/temperature"), true);
|
||||
server.send (302, "text/plain", "");
|
||||
} else {
|
||||
printE("Error saving configuration");
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></form>";
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
}
|
||||
}
|
||||
@@ -335,6 +354,8 @@ void AmsWebServer::indexHtml() {
|
||||
|
||||
html.replace("{P}", String(meterState->getActiveImportPower()));
|
||||
html.replace("{PO}", String(meterState->getActiveExportPower()));
|
||||
html.replace("{Q}", String(meterState->getReactiveImportPower()));
|
||||
html.replace("{QO}", String(meterState->getReactiveExportPower()));
|
||||
html.replace("{de}", meterConfig->productionCapacity > 0 ? "" : "none");
|
||||
html.replace("{dn}", meterConfig->productionCapacity > 0 ? "none" : "");
|
||||
html.replace("{ti}", meterConfig->productionCapacity > 0 ? "Import" : "Use");
|
||||
@@ -364,6 +385,8 @@ void AmsWebServer::indexHtml() {
|
||||
int rssi = hw->getWifiRssi();
|
||||
html.replace("{rssi}", String(rssi));
|
||||
|
||||
html.replace("{mem}", String(ESP.getFreeHeap()/1000, 1));
|
||||
|
||||
html.replace("{cs}", String((uint32_t)(millis64()/1000), 10));
|
||||
|
||||
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
|
||||
@@ -391,10 +414,44 @@ void AmsWebServer::configMeterHtml() {
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
|
||||
html.replace("{m}", String(meterConfig->mainFuse));
|
||||
for(int i = 0; i<5; i++) {
|
||||
html.replace("{m" + String(i) + "}", meterConfig->type == i ? "selected" : "");
|
||||
String manufacturer;
|
||||
switch(meterState->getMeterType()) {
|
||||
case AmsTypeAidon:
|
||||
manufacturer = "Aidon";
|
||||
break;
|
||||
case AmsTypeKaifa:
|
||||
manufacturer = "Kaifa";
|
||||
break;
|
||||
case AmsTypeKamstrup:
|
||||
manufacturer = "Kamstrup";
|
||||
break;
|
||||
case AmsTypeIskra:
|
||||
manufacturer = "Iskra";
|
||||
break;
|
||||
case AmsTypeLandis:
|
||||
manufacturer = "Landis + Gyro";
|
||||
break;
|
||||
case AmsTypeSagemcom:
|
||||
manufacturer = "Sagemcom";
|
||||
break;
|
||||
default:
|
||||
manufacturer = "Unknown";
|
||||
break;
|
||||
}
|
||||
|
||||
html.replace("{maf}", manufacturer);
|
||||
html.replace("{mod}", meterState->getMeterModel());
|
||||
html.replace("{mid}", meterState->getMeterId());
|
||||
html.replace("{b}", String(meterConfig->baud));
|
||||
html.replace("{b2400}", meterConfig->baud == 2400 ? "selected" : "");
|
||||
html.replace("{b9600}", meterConfig->baud == 9600 ? "selected" : "");
|
||||
html.replace("{b115200}", meterConfig->baud == 115200 ? "selected" : "");
|
||||
html.replace("{c}", String(meterConfig->baud));
|
||||
html.replace("{c2}", meterConfig->parity == 2 ? "selected" : "");
|
||||
html.replace("{c3}", meterConfig->parity == 3 ? "selected" : "");
|
||||
html.replace("{c10}", meterConfig->parity == 10 ? "selected" : "");
|
||||
html.replace("{c11}", meterConfig->parity == 11 ? "selected" : "");
|
||||
html.replace("{i}", meterConfig->invert ? "checked" : "");
|
||||
html.replace("{d}", String(meterConfig->distributionSystem));
|
||||
for(int i = 0; i<3; i++) {
|
||||
html.replace("{d" + String(i) + "}", meterConfig->distributionSystem == i ? "selected" : "");
|
||||
@@ -405,7 +462,7 @@ void AmsWebServer::configMeterHtml() {
|
||||
}
|
||||
html.replace("{p}", String(meterConfig->productionCapacity));
|
||||
|
||||
if(meterConfig->type == METER_TYPE_OMNIPOWER) {
|
||||
if(meterConfig->encryptionKey[0] != 0x00) {
|
||||
String encryptionKeyHex = "0x";
|
||||
encryptionKeyHex += toHex(meterConfig->encryptionKey, 16);
|
||||
html.replace("{e}", encryptionKeyHex);
|
||||
@@ -418,8 +475,6 @@ void AmsWebServer::configMeterHtml() {
|
||||
html.replace("{a}", "");
|
||||
}
|
||||
|
||||
html.replace("{s}", meterConfig->substituteMissing ? "checked" : "");
|
||||
|
||||
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
|
||||
server.send_P(200, "text/html", HEAD_HTML);
|
||||
server.sendContent(html);
|
||||
@@ -490,14 +545,14 @@ void AmsWebServer::configMqttHtml() {
|
||||
|
||||
html.replace("{s}", mqtt.ssl ? "checked" : "");
|
||||
|
||||
if(SPIFFS.begin()) {
|
||||
html.replace("{dcu}", SPIFFS.exists(FILE_MQTT_CA) ? "none" : "");
|
||||
html.replace("{dcf}", SPIFFS.exists(FILE_MQTT_CA) ? "" : "none");
|
||||
html.replace("{deu}", SPIFFS.exists(FILE_MQTT_CERT) ? "none" : "");
|
||||
html.replace("{def}", SPIFFS.exists(FILE_MQTT_CERT) ? "" : "none");
|
||||
html.replace("{dku}", SPIFFS.exists(FILE_MQTT_KEY) ? "none" : "");
|
||||
html.replace("{dkf}", SPIFFS.exists(FILE_MQTT_KEY) ? "" : "none");
|
||||
SPIFFS.end();
|
||||
if(LittleFS.begin()) {
|
||||
html.replace("{dcu}", LittleFS.exists(FILE_MQTT_CA) ? "none" : "");
|
||||
html.replace("{dcf}", LittleFS.exists(FILE_MQTT_CA) ? "" : "none");
|
||||
html.replace("{deu}", LittleFS.exists(FILE_MQTT_CERT) ? "none" : "");
|
||||
html.replace("{def}", LittleFS.exists(FILE_MQTT_CERT) ? "" : "none");
|
||||
html.replace("{dku}", LittleFS.exists(FILE_MQTT_KEY) ? "none" : "");
|
||||
html.replace("{dkf}", LittleFS.exists(FILE_MQTT_KEY) ? "" : "none");
|
||||
LittleFS.end();
|
||||
} else {
|
||||
html.replace("{dcu}", "");
|
||||
html.replace("{dcf}", "none");
|
||||
@@ -568,10 +623,10 @@ void AmsWebServer::configEntsoeHtml() {
|
||||
html.replace("{eaDk1}", strcmp(entsoe.area, "10YDK-1--------W") == 0 ? "selected" : "");
|
||||
html.replace("{eaDk2}", strcmp(entsoe.area, "10YDK-2--------M") == 0 ? "selected" : "");
|
||||
|
||||
html.replace("{ecNOK}", strcmp(entsoe.area, "NOK") == 0 ? "selected" : "");
|
||||
html.replace("{ecSEK}", strcmp(entsoe.area, "SEK") == 0 ? "selected" : "");
|
||||
html.replace("{ecDKK}", strcmp(entsoe.area, "DKK") == 0 ? "selected" : "");
|
||||
html.replace("{ecEUR}", strcmp(entsoe.area, "EUR") == 0 ? "selected" : "");
|
||||
html.replace("{ecNOK}", strcmp(entsoe.currency, "NOK") == 0 ? "selected" : "");
|
||||
html.replace("{ecSEK}", strcmp(entsoe.currency, "SEK") == 0 ? "selected" : "");
|
||||
html.replace("{ecDKK}", strcmp(entsoe.currency, "DKK") == 0 ? "selected" : "");
|
||||
html.replace("{ecEUR}", strcmp(entsoe.currency, "EUR") == 0 ? "selected" : "");
|
||||
|
||||
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
|
||||
server.send_P(200, "text/html", HEAD_HTML);
|
||||
@@ -616,13 +671,6 @@ void AmsWebServer::bootCss() {
|
||||
server.send_P(200, "text/css", BOOT_CSS);
|
||||
}
|
||||
|
||||
void AmsWebServer::gaugemeterJs() {
|
||||
printD("Serving /gaugemeter.js over http...");
|
||||
|
||||
server.sendHeader("Cache-Control", "public, max-age=3600");
|
||||
server.send_P(200, "application/javascript", GAUGEMETER_JS);
|
||||
}
|
||||
|
||||
void AmsWebServer::githubSvg() {
|
||||
printD("Serving /github.svg over http...");
|
||||
|
||||
@@ -665,7 +713,7 @@ void AmsWebServer::dataJson() {
|
||||
|
||||
|
||||
uint8_t hanStatus;
|
||||
if(meterConfig->type == 0) {
|
||||
if(meterConfig->baud == 0) {
|
||||
hanStatus = 0;
|
||||
} else if(now - meterState->getLastUpdateMillis() < 15000) {
|
||||
hanStatus = 1;
|
||||
@@ -687,15 +735,19 @@ void AmsWebServer::dataJson() {
|
||||
uint8_t mqttStatus;
|
||||
if(!mqttEnabled) {
|
||||
mqttStatus = 0;
|
||||
} else if(mqtt->connected()) {
|
||||
} else if(mqtt != NULL && mqtt->connected()) {
|
||||
mqttStatus = 1;
|
||||
} else if(mqtt->lastError() == 0) {
|
||||
} else if(mqtt != NULL && mqtt->lastError() == 0) {
|
||||
mqttStatus = 2;
|
||||
} else {
|
||||
mqttStatus = 3;
|
||||
}
|
||||
|
||||
char json[290];
|
||||
float price = ENTSOE_NO_VALUE;
|
||||
if(eapi != NULL && strlen(eapi->getToken()) > 0)
|
||||
price = eapi->getValueForHour(0);
|
||||
|
||||
char json[340];
|
||||
snprintf_P(json, sizeof(json), DATA_JSON,
|
||||
maxPwr == 0 ? meterState->isThreePhase() ? 20000 : 10000 : maxPwr,
|
||||
meterConfig->productionCapacity,
|
||||
@@ -714,6 +766,10 @@ void AmsWebServer::dataJson() {
|
||||
meterState->getL1Current(),
|
||||
meterState->getL2Current(),
|
||||
meterState->getL3Current(),
|
||||
meterState->getPowerFactor(),
|
||||
meterState->getL1PowerFactor(),
|
||||
meterState->getL2PowerFactor(),
|
||||
meterState->getL3PowerFactor(),
|
||||
vcc,
|
||||
rssi,
|
||||
hw->getTemperature(),
|
||||
@@ -723,7 +779,157 @@ void AmsWebServer::dataJson() {
|
||||
hanStatus,
|
||||
wifiStatus,
|
||||
mqttStatus,
|
||||
(int) mqtt->lastError()
|
||||
mqtt == NULL ? 0 : (int) mqtt->lastError(),
|
||||
price == ENTSOE_NO_VALUE ? "null" : String(price, 2).c_str()
|
||||
);
|
||||
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
server.sendHeader("Expires", "-1");
|
||||
|
||||
server.setContentLength(strlen(json));
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void AmsWebServer::dayplotJson() {
|
||||
printD("Serving /dayplot.json over http...");
|
||||
if(ds == NULL) {
|
||||
notFound();
|
||||
} else {
|
||||
char json[384];
|
||||
snprintf_P(json, sizeof(json), DAYPLOT_JSON,
|
||||
ds->getHour(0) / 1000.0,
|
||||
ds->getHour(1) / 1000.0,
|
||||
ds->getHour(2) / 1000.0,
|
||||
ds->getHour(3) / 1000.0,
|
||||
ds->getHour(4) / 1000.0,
|
||||
ds->getHour(5) / 1000.0,
|
||||
ds->getHour(6) / 1000.0,
|
||||
ds->getHour(7) / 1000.0,
|
||||
ds->getHour(8) / 1000.0,
|
||||
ds->getHour(9) / 1000.0,
|
||||
ds->getHour(10) / 1000.0,
|
||||
ds->getHour(11) / 1000.0,
|
||||
ds->getHour(12) / 1000.0,
|
||||
ds->getHour(13) / 1000.0,
|
||||
ds->getHour(14) / 1000.0,
|
||||
ds->getHour(15) / 1000.0,
|
||||
ds->getHour(16) / 1000.0,
|
||||
ds->getHour(17) / 1000.0,
|
||||
ds->getHour(18) / 1000.0,
|
||||
ds->getHour(19) / 1000.0,
|
||||
ds->getHour(20) / 1000.0,
|
||||
ds->getHour(21) / 1000.0,
|
||||
ds->getHour(22) / 1000.0,
|
||||
ds->getHour(23) / 1000.0
|
||||
);
|
||||
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
server.sendHeader("Expires", "-1");
|
||||
|
||||
server.setContentLength(strlen(json));
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
}
|
||||
|
||||
void AmsWebServer::monthplotJson() {
|
||||
printD("Serving /monthplot.json over http...");
|
||||
|
||||
if(ds == NULL) {
|
||||
notFound();
|
||||
} else {
|
||||
char json[512];
|
||||
snprintf_P(json, sizeof(json), MONTHPLOT_JSON,
|
||||
ds->getDay(1) / 1000.0,
|
||||
ds->getDay(2) / 1000.0,
|
||||
ds->getDay(3) / 1000.0,
|
||||
ds->getDay(4) / 1000.0,
|
||||
ds->getDay(5) / 1000.0,
|
||||
ds->getDay(6) / 1000.0,
|
||||
ds->getDay(7) / 1000.0,
|
||||
ds->getDay(8) / 1000.0,
|
||||
ds->getDay(9) / 1000.0,
|
||||
ds->getDay(10) / 1000.0,
|
||||
ds->getDay(11) / 1000.0,
|
||||
ds->getDay(12) / 1000.0,
|
||||
ds->getDay(13) / 1000.0,
|
||||
ds->getDay(14) / 1000.0,
|
||||
ds->getDay(15) / 1000.0,
|
||||
ds->getDay(16) / 1000.0,
|
||||
ds->getDay(17) / 1000.0,
|
||||
ds->getDay(18) / 1000.0,
|
||||
ds->getDay(19) / 1000.0,
|
||||
ds->getDay(20) / 1000.0,
|
||||
ds->getDay(21) / 1000.0,
|
||||
ds->getDay(22) / 1000.0,
|
||||
ds->getDay(23) / 1000.0,
|
||||
ds->getDay(24) / 1000.0,
|
||||
ds->getDay(25) / 1000.0,
|
||||
ds->getDay(26) / 1000.0,
|
||||
ds->getDay(27) / 1000.0,
|
||||
ds->getDay(28) / 1000.0,
|
||||
ds->getDay(29) / 1000.0,
|
||||
ds->getDay(30) / 1000.0,
|
||||
ds->getDay(31) / 1000.0
|
||||
);
|
||||
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
server.sendHeader("Expires", "-1");
|
||||
|
||||
server.setContentLength(strlen(json));
|
||||
server.send(200, "application/json", json);
|
||||
}
|
||||
}
|
||||
|
||||
void AmsWebServer::energyPriceJson() {
|
||||
printD("Serving /energyprice.json over http...");
|
||||
|
||||
float prices[36];
|
||||
for(int i = 0; i < 36; i++) {
|
||||
prices[i] = eapi == NULL ? ENTSOE_NO_VALUE : eapi->getValueForHour(i);
|
||||
}
|
||||
|
||||
char json[768];
|
||||
snprintf_P(json, sizeof(json), ENERGYPRICE_JSON,
|
||||
eapi == NULL ? "" : eapi->getCurrency(),
|
||||
prices[0] == ENTSOE_NO_VALUE ? "null" : String(prices[0], 2).c_str(),
|
||||
prices[1] == ENTSOE_NO_VALUE ? "null" : String(prices[1], 2).c_str(),
|
||||
prices[2] == ENTSOE_NO_VALUE ? "null" : String(prices[2], 2).c_str(),
|
||||
prices[3] == ENTSOE_NO_VALUE ? "null" : String(prices[3], 2).c_str(),
|
||||
prices[4] == ENTSOE_NO_VALUE ? "null" : String(prices[4], 2).c_str(),
|
||||
prices[5] == ENTSOE_NO_VALUE ? "null" : String(prices[5], 2).c_str(),
|
||||
prices[6] == ENTSOE_NO_VALUE ? "null" : String(prices[6], 2).c_str(),
|
||||
prices[7] == ENTSOE_NO_VALUE ? "null" : String(prices[7], 2).c_str(),
|
||||
prices[8] == ENTSOE_NO_VALUE ? "null" : String(prices[8], 2).c_str(),
|
||||
prices[9] == ENTSOE_NO_VALUE ? "null" : String(prices[9], 2).c_str(),
|
||||
prices[10] == ENTSOE_NO_VALUE ? "null" : String(prices[10], 2).c_str(),
|
||||
prices[11] == ENTSOE_NO_VALUE ? "null" : String(prices[11], 2).c_str(),
|
||||
prices[12] == ENTSOE_NO_VALUE ? "null" : String(prices[12], 2).c_str(),
|
||||
prices[13] == ENTSOE_NO_VALUE ? "null" : String(prices[13], 2).c_str(),
|
||||
prices[14] == ENTSOE_NO_VALUE ? "null" : String(prices[14], 2).c_str(),
|
||||
prices[15] == ENTSOE_NO_VALUE ? "null" : String(prices[15], 2).c_str(),
|
||||
prices[16] == ENTSOE_NO_VALUE ? "null" : String(prices[16], 2).c_str(),
|
||||
prices[17] == ENTSOE_NO_VALUE ? "null" : String(prices[17], 2).c_str(),
|
||||
prices[18] == ENTSOE_NO_VALUE ? "null" : String(prices[18], 2).c_str(),
|
||||
prices[19] == ENTSOE_NO_VALUE ? "null" : String(prices[19], 2).c_str(),
|
||||
prices[20] == ENTSOE_NO_VALUE ? "null" : String(prices[20], 2).c_str(),
|
||||
prices[21] == ENTSOE_NO_VALUE ? "null" : String(prices[21], 2).c_str(),
|
||||
prices[22] == ENTSOE_NO_VALUE ? "null" : String(prices[22], 2).c_str(),
|
||||
prices[23] == ENTSOE_NO_VALUE ? "null" : String(prices[23], 2).c_str(),
|
||||
prices[24] == ENTSOE_NO_VALUE ? "null" : String(prices[24], 2).c_str(),
|
||||
prices[25] == ENTSOE_NO_VALUE ? "null" : String(prices[25], 2).c_str(),
|
||||
prices[26] == ENTSOE_NO_VALUE ? "null" : String(prices[26], 2).c_str(),
|
||||
prices[27] == ENTSOE_NO_VALUE ? "null" : String(prices[27], 2).c_str(),
|
||||
prices[28] == ENTSOE_NO_VALUE ? "null" : String(prices[28], 2).c_str(),
|
||||
prices[29] == ENTSOE_NO_VALUE ? "null" : String(prices[29], 2).c_str(),
|
||||
prices[30] == ENTSOE_NO_VALUE ? "null" : String(prices[30], 2).c_str(),
|
||||
prices[31] == ENTSOE_NO_VALUE ? "null" : String(prices[31], 2).c_str(),
|
||||
prices[32] == ENTSOE_NO_VALUE ? "null" : String(prices[32], 2).c_str(),
|
||||
prices[33] == ENTSOE_NO_VALUE ? "null" : String(prices[33], 2).c_str(),
|
||||
prices[34] == ENTSOE_NO_VALUE ? "null" : String(prices[34], 2).c_str(),
|
||||
prices[35] == ENTSOE_NO_VALUE ? "null" : String(prices[35], 2).c_str()
|
||||
);
|
||||
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
@@ -748,9 +954,6 @@ void AmsWebServer::handleSetup() {
|
||||
|
||||
config->clear();
|
||||
|
||||
config->clearGpio(*gpioConfig);
|
||||
config->clearMeter(*meterConfig);
|
||||
|
||||
switch(sys.boardType) {
|
||||
case 0: // roarfred
|
||||
gpioConfig->hanPin = 3;
|
||||
@@ -874,10 +1077,6 @@ void AmsWebServer::handleSetup() {
|
||||
printD("Unable to set web config");
|
||||
success = false;
|
||||
}
|
||||
if(!config->setMeterConfig(*meterConfig)) {
|
||||
printD("Unable to set meter config");
|
||||
success = false;
|
||||
}
|
||||
if(!config->setGpioConfig(*gpioConfig)) {
|
||||
printD("Unable to set GPIO config");
|
||||
success = false;
|
||||
@@ -895,7 +1094,7 @@ void AmsWebServer::handleSetup() {
|
||||
server.send(303);
|
||||
} else {
|
||||
printE("Error saving configuration");
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></form>";
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
}
|
||||
}
|
||||
@@ -910,7 +1109,9 @@ void AmsWebServer::handleSave() {
|
||||
|
||||
if(server.hasArg("mc") && server.arg("mc") == "true") {
|
||||
printD("Received meter config");
|
||||
meterConfig->type = server.arg("m").toInt();
|
||||
meterConfig->baud = server.arg("b").toInt();
|
||||
meterConfig->parity = server.arg("c").toInt();
|
||||
meterConfig->invert = server.hasArg("i") && server.arg("i") == "true";
|
||||
meterConfig->distributionSystem = server.arg("d").toInt();
|
||||
meterConfig->mainFuse = server.arg("f").toInt();
|
||||
meterConfig->productionCapacity = server.arg("p").toInt();
|
||||
@@ -926,7 +1127,6 @@ void AmsWebServer::handleSave() {
|
||||
authenticationKeyHex.replace("0x", "");
|
||||
fromHex(meterConfig->authenticationKey, authenticationKeyHex, 16);
|
||||
}
|
||||
meterConfig->substituteMissing = server.hasArg("s") && server.arg("s") == "true";
|
||||
config->setMeterConfig(*meterConfig);
|
||||
}
|
||||
|
||||
@@ -1017,6 +1217,8 @@ void AmsWebServer::handleSave() {
|
||||
gpioConfig->vccOffset = server.hasArg("vccOffset") && !server.arg("vccOffset").isEmpty() ? server.arg("vccOffset").toFloat() * 100 : 0;
|
||||
gpioConfig->vccMultiplier = server.hasArg("vccMultiplier") && !server.arg("vccMultiplier").isEmpty() ? server.arg("vccMultiplier").toFloat() * 1000 : 1000;
|
||||
gpioConfig->vccBootLimit = server.hasArg("vccBootLimit") && !server.arg("vccBootLimit").isEmpty() ? server.arg("vccBootLimit").toFloat() * 10 : 0;
|
||||
gpioConfig->vccResistorGnd = server.hasArg("vccResistorGnd") && !server.arg("vccResistorGnd").isEmpty() ? server.arg("vccResistorGnd").toInt() : 0;
|
||||
gpioConfig->vccResistorVcc = server.hasArg("vccResistorVcc") && !server.arg("vccResistorVcc").isEmpty() ? server.arg("vccResistorVcc").toInt() : 0;
|
||||
config->setGpioConfig(*gpioConfig);
|
||||
}
|
||||
|
||||
@@ -1068,7 +1270,7 @@ void AmsWebServer::handleSave() {
|
||||
|
||||
printI("Saving configuration now...");
|
||||
|
||||
if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger);
|
||||
//if (debugger->isActive(RemoteDebug::DEBUG)) config->print(debugger);
|
||||
if (config->save()) {
|
||||
printI("Successfully saved.");
|
||||
if(config->isWifiChanged()) {
|
||||
@@ -1083,7 +1285,7 @@ void AmsWebServer::handleSave() {
|
||||
}
|
||||
} else {
|
||||
printE("Error saving configuration");
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></form>";
|
||||
String html = "<html><body><h1>Error saving configuration!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
}
|
||||
}
|
||||
@@ -1152,6 +1354,9 @@ void AmsWebServer::configGpioHtml() {
|
||||
html.replace("${config.vccMultiplier}", gpioConfig->vccMultiplier > 0 ? String(gpioConfig->vccMultiplier / 1000.0, 2) : "");
|
||||
html.replace("${config.vccBootLimit}", gpioConfig->vccBootLimit > 0 ? String(gpioConfig->vccBootLimit / 10.0, 1) : "");
|
||||
|
||||
html.replace("${config.vccResistorGnd}", gpioConfig->vccResistorGnd > 0 ? String(gpioConfig->vccResistorGnd) : "");
|
||||
html.replace("${config.vccResistorVcc}", gpioConfig->vccResistorVcc > 0 ? String(gpioConfig->vccResistorVcc) : "");
|
||||
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
|
||||
@@ -1211,6 +1416,11 @@ String AmsWebServer::getSerialSelectOptions(int selected) {
|
||||
#elif defined(ESP8266)
|
||||
int numGpio = 9;
|
||||
int gpios[] = {4,5,9,10,12,13,14,15,16};
|
||||
if(selected == 113) {
|
||||
gpioOptions += "<option value=\"113\" selected>UART2 (GPIO13)</option>";
|
||||
} else {
|
||||
gpioOptions += "<option value=\"113\">UART2 (GPIO13)</option>";
|
||||
}
|
||||
#endif
|
||||
|
||||
for(int i = 0; i < numGpio; i++) {
|
||||
@@ -1235,31 +1445,75 @@ void AmsWebServer::uploadFile(const char* path) {
|
||||
if(upload.status == UPLOAD_FILE_START){
|
||||
if(uploading) {
|
||||
printE("Upload already in progress");
|
||||
String html = "<html><body><h1>Upload already in progress!</h1></form>";
|
||||
String html = "<html><body><h1>Upload already in progress!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
} else if (!SPIFFS.begin()) {
|
||||
printE("An Error has occurred while mounting SPIFFS");
|
||||
String html = "<html><body><h1>Unable to mount SPIFFS!</h1></form>";
|
||||
} else if (!LittleFS.begin()) {
|
||||
printE("An Error has occurred while mounting LittleFS");
|
||||
String html = "<html><body><h1>Unable to mount LittleFS!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
} else {
|
||||
uploading = true;
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload file: %s\n", path);
|
||||
}
|
||||
file = SPIFFS.open(path, "w");
|
||||
file.write(upload.buf, upload.currentSize);
|
||||
#if defined(ESP32)
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Free heap: %lu\n", ESP.getFreeHeap());
|
||||
debugger->printf("handleFileUpload LittleFS size: %lu\n", LittleFS.totalBytes());
|
||||
debugger->printf("handleFileUpload LittleFS used: %lu\n", LittleFS.usedBytes());
|
||||
debugger->printf("handleFileUpload LittleFS free: %lu\n", LittleFS.totalBytes()-LittleFS.usedBytes());
|
||||
}
|
||||
#endif
|
||||
file = LittleFS.open(path, "w");
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Open file and write: %lu\n", upload.currentSize);
|
||||
}
|
||||
size_t written = file.write(upload.buf, upload.currentSize);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Written: %lu\n", written);
|
||||
}
|
||||
}
|
||||
} else if(upload.status == UPLOAD_FILE_WRITE) {
|
||||
if(file)
|
||||
file.write(upload.buf, upload.currentSize);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Writing: %lu\n", upload.currentSize);
|
||||
}
|
||||
if(file) {
|
||||
size_t written = file.write(upload.buf, upload.currentSize);
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Written: %lu\n", written);
|
||||
}
|
||||
delay(1);
|
||||
if(written != upload.currentSize) {
|
||||
#if defined(ESP32)
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Free heap: %lu\n", ESP.getFreeHeap());
|
||||
debugger->printf("handleFileUpload LittleFS size: %lu\n", LittleFS.totalBytes());
|
||||
debugger->printf("handleFileUpload LittleFS used: %lu\n", LittleFS.usedBytes());
|
||||
debugger->printf("handleFileUpload LittleFS free: %lu\n", LittleFS.totalBytes()-LittleFS.usedBytes());
|
||||
}
|
||||
#endif
|
||||
|
||||
file.flush();
|
||||
file.close();
|
||||
LittleFS.remove(path);
|
||||
LittleFS.end();
|
||||
|
||||
printE("An Error has occurred while writing file");
|
||||
String html = "<html><body><h1>Unable to write file!</h1></body></html>";
|
||||
server.send(500, "text/html", html);
|
||||
}
|
||||
}
|
||||
} else if(upload.status == UPLOAD_FILE_END) {
|
||||
if(file) {
|
||||
file.flush();
|
||||
file.close();
|
||||
SPIFFS.end();
|
||||
file = LittleFS.open(path, "r");
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) {
|
||||
debugger->printf("handleFileUpload Size: %lu\n", upload.totalSize);
|
||||
debugger->printf("handleFileUpload File size: %lu\n", file.size());
|
||||
}
|
||||
file.close();
|
||||
LittleFS.end();
|
||||
} else {
|
||||
server.send(500, "text/plain", "500: couldn't create file");
|
||||
}
|
||||
@@ -1267,9 +1521,9 @@ void AmsWebServer::uploadFile(const char* path) {
|
||||
}
|
||||
|
||||
void AmsWebServer::deleteFile(const char* path) {
|
||||
if(SPIFFS.begin()) {
|
||||
SPIFFS.remove(path);
|
||||
SPIFFS.end();
|
||||
if(LittleFS.begin()) {
|
||||
LittleFS.remove(path);
|
||||
LittleFS.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1279,7 +1533,21 @@ void AmsWebServer::firmwareHtml() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
uploadHtml("Firmware", "/firmware", "system");
|
||||
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
server.sendHeader("Pragma", "no-cache");
|
||||
|
||||
String html = String((const __FlashStringHelper*) FIRMWARE_HTML);
|
||||
|
||||
#if defined(ESP8266)
|
||||
html.replace("{chipset}", "ESP8266");
|
||||
#elif defined(ESP32)
|
||||
html.replace("{chipset}", "ESP32");
|
||||
#endif
|
||||
|
||||
server.setContentLength(html.length() + HEAD_HTML_LEN + FOOT_HTML_LEN);
|
||||
server.send_P(200, "text/html", HEAD_HTML);
|
||||
server.sendContent(html);
|
||||
server.sendContent_P(FOOT_HTML);
|
||||
}
|
||||
|
||||
void AmsWebServer::firmwareUpload() {
|
||||
@@ -1315,32 +1583,31 @@ void AmsWebServer::firmwareDownload() {
|
||||
WiFiClientSecure client;
|
||||
#if defined(ESP8266)
|
||||
client.setBufferSizes(512, 512);
|
||||
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp8266-" + versionStripped + ".bin";
|
||||
#elif defined(ESP32)
|
||||
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp32-" + versionStripped + ".bin";
|
||||
#endif
|
||||
client.setInsecure();
|
||||
#endif
|
||||
String url = "https://github.com/gskjold/AmsToMqttBridge/releases/download/" + version + "/ams2mqtt-esp12e-" + versionStripped + ".bin";
|
||||
HTTPClient https;
|
||||
#if defined(ESP8266)
|
||||
https.setFollowRedirects(true);
|
||||
#endif
|
||||
https.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
|
||||
https.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
|
||||
if(https.begin(client, url)) {
|
||||
https.addHeader("Referer", "https://github.com/gskjold/AmsToMqttBridge/releases");
|
||||
printD("HTTP client setup successful");
|
||||
int status = https.GET();
|
||||
if(status == HTTP_CODE_OK) {
|
||||
printD("Received OK from server");
|
||||
if(SPIFFS.begin()) {
|
||||
printI("Downloading firmware to SPIFFS");
|
||||
file = SPIFFS.open(FILE_FIRMWARE, "w");
|
||||
// The following does not work... Maybe someone will make it work in the future? It seems to be disconnected at this point.
|
||||
if(LittleFS.begin()) {
|
||||
printI("Downloading firmware to LittleFS");
|
||||
file = LittleFS.open(FILE_FIRMWARE, "w");
|
||||
int len = https.writeToStream(&file);
|
||||
file.close();
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
performRestart = true;
|
||||
server.sendHeader("Location","/restart-wait");
|
||||
server.send(303);
|
||||
} else {
|
||||
printE("Unable to open SPIFFS for writing");
|
||||
printE("Unable to open LittleFS for writing");
|
||||
server.sendHeader("Location","/");
|
||||
server.send(303);
|
||||
}
|
||||
@@ -1365,6 +1632,7 @@ void AmsWebServer::firmwareDownload() {
|
||||
server.send(303);
|
||||
}
|
||||
https.end();
|
||||
client.stop();
|
||||
} else {
|
||||
printI("No firmware version specified...");
|
||||
server.sendHeader("Location","/");
|
||||
@@ -1426,7 +1694,9 @@ void AmsWebServer::restartWaitHtml() {
|
||||
|
||||
yield();
|
||||
if(performRestart) {
|
||||
SPIFFS.end();
|
||||
if(ds != NULL) {
|
||||
ds->save();
|
||||
}
|
||||
printI("Rebooting");
|
||||
delay(1000);
|
||||
#if defined(ESP8266)
|
||||
@@ -1469,13 +1739,13 @@ void AmsWebServer::mqttCa() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
if(SPIFFS.begin()) {
|
||||
if(SPIFFS.exists(FILE_MQTT_CA)) {
|
||||
if(LittleFS.begin()) {
|
||||
if(LittleFS.exists(FILE_MQTT_CA)) {
|
||||
deleteHtml("CA file", "/mqtt-ca/delete", "mqtt");
|
||||
} else {
|
||||
uploadHtml("CA file", "/mqtt-ca", "mqtt");
|
||||
}
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
} else {
|
||||
server.sendHeader("Location","/config-mqtt");
|
||||
server.send(303);
|
||||
@@ -1523,13 +1793,13 @@ void AmsWebServer::mqttCert() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
if(SPIFFS.begin()) {
|
||||
if(SPIFFS.exists(FILE_MQTT_CERT)) {
|
||||
if(LittleFS.begin()) {
|
||||
if(LittleFS.exists(FILE_MQTT_CERT)) {
|
||||
deleteHtml("Certificate", "/mqtt-cert/delete", "mqtt");
|
||||
} else {
|
||||
uploadHtml("Certificate", "/mqtt-cert", "mqtt");
|
||||
}
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
} else {
|
||||
server.sendHeader("Location","/config-mqtt");
|
||||
server.send(303);
|
||||
@@ -1576,13 +1846,13 @@ void AmsWebServer::mqttKey() {
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
if(SPIFFS.begin()) {
|
||||
if(SPIFFS.exists(FILE_MQTT_KEY)) {
|
||||
if(LittleFS.begin()) {
|
||||
if(LittleFS.exists(FILE_MQTT_KEY)) {
|
||||
deleteHtml("Private key", "/mqtt-key/delete", "mqtt");
|
||||
} else {
|
||||
uploadHtml("Private key", "/mqtt-key", "mqtt");
|
||||
}
|
||||
SPIFFS.end();
|
||||
LittleFS.end();
|
||||
} else {
|
||||
server.sendHeader("Location","/config-mqtt");
|
||||
server.send(303);
|
||||
@@ -1641,8 +1911,8 @@ void AmsWebServer::factoryResetPost() {
|
||||
|
||||
printD("Performing factory reset");
|
||||
if(server.hasArg("perform") && server.arg("perform") == "true") {
|
||||
printD("Formatting SPIFFS");
|
||||
SPIFFS.format();
|
||||
printD("Formatting LittleFS");
|
||||
LittleFS.format();
|
||||
printD("Clearing configuration");
|
||||
config->clear();
|
||||
printD("Setting restart flag and redirecting");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "AmsConfiguration.h"
|
||||
#include "HwTools.h"
|
||||
#include "AmsData.h"
|
||||
#include "AmsDataStorage.h"
|
||||
#include "Uptime.h"
|
||||
#include "RemoteDebug.h"
|
||||
#include "entsoe/EntsoeApi.h"
|
||||
@@ -20,16 +21,18 @@
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <HTTPClient.h>
|
||||
#include "SPIFFS.h"
|
||||
#else
|
||||
#warning "Unsupported board type"
|
||||
#endif
|
||||
|
||||
#include "LittleFS.h"
|
||||
|
||||
class AmsWebServer {
|
||||
public:
|
||||
AmsWebServer(RemoteDebug* Debug, HwTools* hw);
|
||||
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, MQTTClient*);
|
||||
void setup(AmsConfiguration*, GpioConfig*, MeterConfig*, AmsData*, AmsDataStorage*);
|
||||
void loop();
|
||||
void setMqtt(MQTTClient* mqtt);
|
||||
void setTimezone(Timezone* tz);
|
||||
void setMqttEnabled(bool);
|
||||
void setEntsoeApi(EntsoeApi* eapi);
|
||||
@@ -46,7 +49,8 @@ private:
|
||||
MeterConfig* meterConfig;
|
||||
WebConfig webConfig;
|
||||
AmsData* meterState;
|
||||
MQTTClient* mqtt;
|
||||
AmsDataStorage* ds;
|
||||
MQTTClient* mqtt = NULL;
|
||||
bool uploading = false;
|
||||
File file;
|
||||
bool performRestart = false;
|
||||
@@ -75,9 +79,11 @@ private:
|
||||
void configGpioHtml();
|
||||
void configDebugHtml();
|
||||
void bootCss();
|
||||
void gaugemeterJs();
|
||||
void githubSvg();
|
||||
void dataJson();
|
||||
void dayplotJson();
|
||||
void monthplotJson();
|
||||
void energyPriceJson();
|
||||
|
||||
void handleSetup();
|
||||
void handleSave();
|
||||
|
||||
@@ -1,46 +1,162 @@
|
||||
var nextVersion;
|
||||
var im, em, vm, am;
|
||||
$(function() {
|
||||
im = $("#im");
|
||||
if(im && im.gaugeMeter) {
|
||||
im.gaugeMeter({
|
||||
percent: 0,
|
||||
text: "-",
|
||||
append: "W"
|
||||
});
|
||||
}
|
||||
|
||||
em = $("#em");
|
||||
if(em && em.gaugeMeter) {
|
||||
em.gaugeMeter({
|
||||
percent: 0,
|
||||
text: "-",
|
||||
append: "W"
|
||||
});
|
||||
}
|
||||
|
||||
vm = $("#vm");
|
||||
if(vm && vm.gaugeMeter) {
|
||||
vm.gaugeMeter({
|
||||
percent: 0,
|
||||
text: "-",
|
||||
append: "V"
|
||||
});
|
||||
}
|
||||
|
||||
am = $("#am");
|
||||
if(am && am.gaugeMeter) {
|
||||
am.gaugeMeter({
|
||||
percent: 0,
|
||||
text: "-",
|
||||
append: "A"
|
||||
});
|
||||
}
|
||||
var im, em;
|
||||
|
||||
var meters = $('.SimpleMeter');
|
||||
// Price plot
|
||||
var pp;
|
||||
var pa;
|
||||
var po = {
|
||||
title: 'Future energy price',
|
||||
titleTextStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
bar: { groupWidth: '90%' },
|
||||
legend: { position: 'none' },
|
||||
vAxis: {
|
||||
viewWindowMode: 'maximized'
|
||||
},
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
};
|
||||
var pl = null; // Last price
|
||||
|
||||
// Day plot
|
||||
var ep;
|
||||
var ea;
|
||||
var eo = {
|
||||
title: 'Energy use last 24 hours',
|
||||
titleTextStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
bar: { groupWidth: '90%' },
|
||||
legend: { position: 'none' },
|
||||
vAxis: {
|
||||
title: 'kWh',
|
||||
viewWindowMode: 'maximized'
|
||||
},
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
};
|
||||
|
||||
// Month plot
|
||||
var mp;
|
||||
var ma;
|
||||
var mo = {
|
||||
title: 'Energy use last month',
|
||||
titleTextStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
bar: { groupWidth: '90%' },
|
||||
legend: { position: 'none' },
|
||||
vAxis: {
|
||||
title: 'kWh',
|
||||
viewWindowMode: 'maximized'
|
||||
},
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
};
|
||||
|
||||
// Voltage plot
|
||||
var vp;
|
||||
var va;
|
||||
var vo = {
|
||||
title: 'Phase voltage',
|
||||
titleTextStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
bar: { groupWidth: '90%' },
|
||||
vAxis: {
|
||||
minValue: 200,
|
||||
maxValue: 260,
|
||||
ticks: [
|
||||
{ v: 207, f: '-10%'},
|
||||
{ v: 230, f: '230V'},
|
||||
{ v: 253, f: '+10%'}
|
||||
]
|
||||
},
|
||||
legend: { position: 'none' },
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
};
|
||||
|
||||
// Amperage plot
|
||||
var ap;
|
||||
var aa;
|
||||
var ao = {
|
||||
title: 'Phase current',
|
||||
titleTextStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
bar: { groupWidth: '90%' },
|
||||
vAxis: {
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
ticks: [
|
||||
{ v: 25, f: '25%'},
|
||||
{ v: 50, f: '50%'},
|
||||
{ v: 75, f: '75%'},
|
||||
{ v: 100, f: '100%'}
|
||||
]
|
||||
},
|
||||
legend: { position: 'none' },
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
};
|
||||
|
||||
// Import plot
|
||||
var ip;
|
||||
var ia;
|
||||
var io = {
|
||||
legend: 'none',
|
||||
pieHole: 0.6,
|
||||
pieSliceText: 'none',
|
||||
pieStartAngle: 216,
|
||||
slices: {
|
||||
0: { color: 'green' },
|
||||
1: { color: '#eee' },
|
||||
2: { color: 'transparent' }
|
||||
},
|
||||
legend: { position: 'none' },
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
chartArea: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}
|
||||
};
|
||||
|
||||
// Export plot
|
||||
var xp;
|
||||
var xa;
|
||||
var xo = {
|
||||
legend: 'none',
|
||||
pieHole: 0.6,
|
||||
pieSliceText: 'none',
|
||||
pieStartAngle: 216,
|
||||
slices: {
|
||||
0: { color: 'green' },
|
||||
1: { color: '#eee' },
|
||||
2: { color: 'transparent' }
|
||||
},
|
||||
legend: { position: 'none' },
|
||||
tooltip: { trigger: 'none'},
|
||||
enableInteractivity: false,
|
||||
chartArea: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
var meters = $('.plot1');
|
||||
|
||||
if(meters.length > 0) {
|
||||
fetch();
|
||||
// Chart
|
||||
google.charts.load('current', {'packages':['corechart']});
|
||||
google.charts.setOnLoadCallback(setupChart);
|
||||
}
|
||||
|
||||
// For mqtt
|
||||
@@ -74,25 +190,6 @@ $(function() {
|
||||
$('#m').trigger('change');
|
||||
$('#f').trigger('change');
|
||||
|
||||
// For meter
|
||||
$('.sd').on('change', function() {
|
||||
if(($('#mt').val() == 2 || $('#mt').val() == 3) && $('#d').val() == 1) {
|
||||
$('#ss').show();
|
||||
} else {
|
||||
$('#ss').hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('#mt').on('change', function() {
|
||||
if($('#mt').val() == 4) {
|
||||
$('.enc').show();
|
||||
} else {
|
||||
$('.enc').hide();
|
||||
}
|
||||
});
|
||||
|
||||
$('#mt').trigger('change');
|
||||
|
||||
// For wifi
|
||||
$('#st').on('change', function() {
|
||||
if($(this).is(':checked')) {
|
||||
@@ -116,6 +213,9 @@ $(function() {
|
||||
var fileName = $(this).val();
|
||||
$(this).next('.custom-file-label').html(fileName);
|
||||
})
|
||||
$('.upload-form').on('submit', function(i, form) {
|
||||
$('#loading-indicator').show();
|
||||
});
|
||||
|
||||
// For NTP
|
||||
$('#n').on('change', function() {
|
||||
@@ -154,8 +254,9 @@ $(function() {
|
||||
}
|
||||
|
||||
// Check for software upgrade
|
||||
var swv = $('#swVersion')
|
||||
if(meters.length > 0 && swv.length == 1 && swv.text() != "SNAPSHOT") {
|
||||
var swv = $('#swVersion');
|
||||
var fwl = $('#fwLink');
|
||||
if((meters.length > 0 || fwl.length > 0) && swv.length == 1) {
|
||||
var v = swv.text().substring(1).split('.');
|
||||
var v_major = parseInt(v[0]);
|
||||
var v_minor = parseInt(v[1]);
|
||||
@@ -164,48 +265,65 @@ $(function() {
|
||||
url: swv.data('url'),
|
||||
dataType: 'json'
|
||||
}).done(function(releases) {
|
||||
releases.reverse();
|
||||
var me;
|
||||
var next_patch;
|
||||
var next_minor;
|
||||
var next_major;
|
||||
$.each(releases, function(i, release) {
|
||||
var ver2 = release.tag_name;
|
||||
var v2 = ver2.substring(1).split('.');
|
||||
var v2_major = parseInt(v2[0]);
|
||||
var v2_minor = parseInt(v2[1]);
|
||||
var v2_patch = parseInt(v2[2]);
|
||||
if(/^v\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(swv.text()) && fwl.length == 0) {
|
||||
releases.reverse();
|
||||
var next_patch;
|
||||
var next_minor;
|
||||
var next_major;
|
||||
$.each(releases, function(i, release) {
|
||||
var ver2 = release.tag_name;
|
||||
var v2 = ver2.substring(1).split('.');
|
||||
var v2_major = parseInt(v2[0]);
|
||||
var v2_minor = parseInt(v2[1]);
|
||||
var v2_patch = parseInt(v2[2]);
|
||||
|
||||
if(v2_major == v_major) {
|
||||
if(v2_minor == v_minor) {
|
||||
if(v2_patch > v_patch) {
|
||||
next_patch = release;
|
||||
if(v2_major == v_major) {
|
||||
if(v2_minor == v_minor) {
|
||||
if(v2_patch > v_patch) {
|
||||
next_patch = release;
|
||||
}
|
||||
} else if(v2_minor == v_minor+1) {
|
||||
next_minor = release;
|
||||
}
|
||||
} else if(v2_minor == v_minor+1) {
|
||||
next_minor = release;
|
||||
}
|
||||
} else if(v2_major == v_major+1) {
|
||||
if(next_major) {
|
||||
var mv = next_major.tag_name.substring(1).split('.');
|
||||
var mv_major = parseInt(mv[0]);
|
||||
var mv_minor = parseInt(mv[1]);
|
||||
var mv_patch = parseInt(mv[2]);
|
||||
if(v2_minor == mv_minor) {
|
||||
} else if(v2_major == v_major+1) {
|
||||
if(next_major) {
|
||||
var mv = next_major.tag_name.substring(1).split('.');
|
||||
var mv_major = parseInt(mv[0]);
|
||||
var mv_minor = parseInt(mv[1]);
|
||||
var mv_patch = parseInt(mv[2]);
|
||||
if(v2_minor == mv_minor) {
|
||||
next_major = release;
|
||||
}
|
||||
} else {
|
||||
next_major = release;
|
||||
}
|
||||
} else {
|
||||
next_major = release;
|
||||
}
|
||||
});
|
||||
if(next_minor) {
|
||||
nextVersion = next_minor;
|
||||
} else if(next_major) {
|
||||
nextVersion = next_major;
|
||||
} else if(next_patch) {
|
||||
nextVersion = next_patch;
|
||||
}
|
||||
});
|
||||
if(next_minor) {
|
||||
nextVersion = next_minor;
|
||||
} else if(next_major) {
|
||||
nextVersion = next_major;
|
||||
} else if(next_patch) {
|
||||
nextVersion = next_patch;
|
||||
} else {
|
||||
nextVersion = releases[0];
|
||||
}
|
||||
if(nextVersion) {
|
||||
if(fwl.length > 0) {
|
||||
var chipset = fwl.data('chipset').toLowerCase();
|
||||
$.each(releases, function(i, release) {
|
||||
if(release.tag_name == nextVersion.tag_name) {
|
||||
$.each(release.assets, function(i, asset) {
|
||||
if(asset.name.includes(chipset) && !asset.name.includes("partitions")) {
|
||||
fwl.prop('href', asset.browser_download_url);
|
||||
$('#fwDownload').show();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
$('#newVersionTag').text(nextVersion.tag_name);
|
||||
$('#newVersionUrl').prop('href', nextVersion.html_url);
|
||||
$('#newVersion').removeClass('d-none');
|
||||
@@ -220,6 +338,143 @@ $(function() {
|
||||
}
|
||||
});
|
||||
|
||||
var resizeTO;
|
||||
$( window ).resize(function() {
|
||||
if(resizeTO) clearTimeout(resizeTO);
|
||||
resizeTO = setTimeout(function() {
|
||||
$(this).trigger('resizeEnd');
|
||||
}, 250);
|
||||
});
|
||||
|
||||
$(window).on('resizeEnd', function() {
|
||||
redraw();
|
||||
});
|
||||
|
||||
var zeropad = function(num) {
|
||||
num = num.toString();
|
||||
while (num.length < 2) num = "0" + num;
|
||||
return num;
|
||||
}
|
||||
|
||||
var setupChart = function() {
|
||||
pp = new google.visualization.ColumnChart(document.getElementById('pp'));
|
||||
ep = new google.visualization.ColumnChart(document.getElementById('ep'));
|
||||
mp = new google.visualization.ColumnChart(document.getElementById('mp'));
|
||||
vp = new google.visualization.ColumnChart(document.getElementById('vp'));
|
||||
ap = new google.visualization.ColumnChart(document.getElementById('ap'));
|
||||
ip = new google.visualization.PieChart(document.getElementById('ip'));
|
||||
xp = new google.visualization.PieChart(document.getElementById('xp'));
|
||||
fetch();
|
||||
drawDay();
|
||||
drawMonth();
|
||||
};
|
||||
|
||||
var redraw = function() {
|
||||
if(pl != null) {
|
||||
pp.draw(pa, po);
|
||||
}
|
||||
ep.draw(ea, eo);
|
||||
mp.draw(ma, mo);
|
||||
vp.draw(va, vo);
|
||||
ap.draw(aa, ao);
|
||||
ip.draw(ia, io);
|
||||
xp.draw(xa, xo);
|
||||
};
|
||||
|
||||
var drawPrices = function() {
|
||||
$('#ppc').show();
|
||||
$.ajax({
|
||||
url: '/energyprice.json',
|
||||
timeout: 30000,
|
||||
dataType: 'json',
|
||||
}).done(function(json) {
|
||||
data = [['Hour',json.currency + '/kWh', { role: 'style' }, { role: 'annotation' }]];
|
||||
var r = 1;
|
||||
var hour = moment.utc().hours();
|
||||
var offset = moment().utcOffset()/60;
|
||||
var min = 0;
|
||||
var h = 0;
|
||||
var d = json["20"] == null ? 2 : 1;
|
||||
for(var i = hour; i<24; i++) {
|
||||
var val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
|
||||
Math.min(0, val);
|
||||
};
|
||||
for(var i = 0; i < 24; i++) {
|
||||
var val = json[zeropad(h++)];
|
||||
if(val == null) break;
|
||||
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val == null ? "" : val.toFixed(d)];
|
||||
Math.min(0, val);
|
||||
};
|
||||
pa = google.visualization.arrayToDataTable(data);
|
||||
po.vAxis.title = json.currency;
|
||||
if(min == 0)
|
||||
po.vAxis.minValue = 0;
|
||||
pp.draw(pa, po);
|
||||
});
|
||||
}
|
||||
|
||||
var drawDay = function() {
|
||||
$.ajax({
|
||||
url: '/dayplot.json',
|
||||
timeout: 30000,
|
||||
dataType: 'json',
|
||||
}).done(function(json) {
|
||||
data = [['Hour','kWh', { role: 'style' }, { role: 'annotation' }]];
|
||||
var r = 1;
|
||||
var hour = moment.utc().hours();
|
||||
var offset = moment().utcOffset()/60;
|
||||
var min = 0;
|
||||
for(var i = hour; i<24; i++) {
|
||||
var val = json["h"+zeropad(i)];
|
||||
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(1)];
|
||||
Math.min(0, val);
|
||||
};
|
||||
for(var i = 0; i < hour; i++) {
|
||||
var val = json["h"+zeropad(i)];
|
||||
data[r++] = [zeropad((i+offset)%24), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(1)];
|
||||
Math.min(0, val);
|
||||
};
|
||||
ea = google.visualization.arrayToDataTable(data);
|
||||
if(min == 0)
|
||||
eo.vAxis.minValue = 0;
|
||||
ep.draw(ea, eo);
|
||||
|
||||
setTimeout(drawDay, (61-moment().minute())*60000);
|
||||
});
|
||||
};
|
||||
|
||||
var drawMonth = function() {
|
||||
$.ajax({
|
||||
url: '/monthplot.json',
|
||||
timeout: 30000,
|
||||
dataType: 'json',
|
||||
}).done(function(json) {
|
||||
data = [['Day','kWh', { role: 'style' }, { role: 'annotation' }]];
|
||||
var r = 1;
|
||||
var day = moment().date();
|
||||
var eom = moment().subtract(1, 'months').endOf('month').date();
|
||||
var min = 0;
|
||||
for(var i = day; i<=eom; i++) {
|
||||
var val = json["d"+zeropad(i)];
|
||||
data[r++] = [zeropad((i)), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(0)];
|
||||
Math.min(0, val);
|
||||
}
|
||||
for(var i = 1; i < day; i++) {
|
||||
var val = json["d"+zeropad(i)];
|
||||
data[r++] = [zeropad((i)), val, "color: #6f42c1;opacity: 0.9;", val.toFixed(0)];
|
||||
Math.min(0, val);
|
||||
}
|
||||
ma = google.visualization.arrayToDataTable(data);
|
||||
if(min == 0)
|
||||
mo.vAxis.minValue = 0;
|
||||
mp.draw(ma, mo);
|
||||
|
||||
setTimeout(drawMonth, (24-moment().hour())*60000);
|
||||
});
|
||||
};
|
||||
|
||||
var setStatus = function(id, sid) {
|
||||
var item = $('#'+id);
|
||||
item.removeClass('d-none');
|
||||
@@ -239,6 +494,26 @@ var setStatus = function(id, sid) {
|
||||
item.addClass('badge badge-' + status);
|
||||
};
|
||||
|
||||
var voltcol = function(pct) {
|
||||
if(pct > 85) return '#d90000';
|
||||
else if(pct > 75) return'#e32100';
|
||||
else if(pct > 70) return '#ffb800';
|
||||
else if(pct > 65) return '#dcd800';
|
||||
else if(pct > 35) return '#32d900';
|
||||
else if(pct > 25) return '#dcd800';
|
||||
else if(pct > 20) return '#ffb800';
|
||||
else if(pct > 15) return'#e32100';
|
||||
else return '#d90000';
|
||||
};
|
||||
|
||||
var ampcol = function(pct) {
|
||||
if(pct > 85) return '#d90000';
|
||||
else if(pct > 75) return'#e32100';
|
||||
else if(pct > 70) return '#ffb800';
|
||||
else if(pct > 65) return '#dcd800';
|
||||
else return '#32d900';
|
||||
};
|
||||
|
||||
var interval = 5000;
|
||||
var fetch = function() {
|
||||
$.ajax({
|
||||
@@ -250,8 +525,7 @@ var fetch = function() {
|
||||
$(".SimpleMeter").hide();
|
||||
im.show();
|
||||
em.show();
|
||||
vm.show();
|
||||
am.show();
|
||||
|
||||
}
|
||||
|
||||
for(var id in json) {
|
||||
@@ -272,13 +546,19 @@ var fetch = function() {
|
||||
$('.ju').html(moment.duration(parseInt(json.u), 'seconds').humanize());
|
||||
}
|
||||
|
||||
var kib = parseInt(json.m)/1000;
|
||||
$('.jm').html(kib.toFixed(1));
|
||||
if(kib > 32) {
|
||||
$('.ssl-capable').removeClass('d-none');
|
||||
}
|
||||
|
||||
setStatus("esp", json.em);
|
||||
setStatus("han", json.hm);
|
||||
setStatus("wifi", json.wm);
|
||||
setStatus("mqtt", json.mm);
|
||||
|
||||
|
||||
if(im && im.gaugeMeter) {
|
||||
if(ip) {
|
||||
var v = parseInt(json.i);
|
||||
var pct = (v*100)/parseInt(json.im);
|
||||
var append = "W";
|
||||
@@ -286,14 +566,20 @@ var fetch = function() {
|
||||
v = (v/1000).toFixed(1);
|
||||
append = "kW";
|
||||
}
|
||||
im.gaugeMeter({
|
||||
percent: pct,
|
||||
text: v,
|
||||
append: append
|
||||
});
|
||||
$('.ipo').html(v);
|
||||
$('.ipoa').html(append);
|
||||
var arr = [
|
||||
['Slice', 'Value'],
|
||||
['', (pct*2.88)],
|
||||
['', ((100-pct)*2.88)],
|
||||
['', 72],
|
||||
];
|
||||
io.slices[0].color = ampcol(pct);
|
||||
ia = google.visualization.arrayToDataTable(arr);
|
||||
ip.draw(ia, io);
|
||||
}
|
||||
|
||||
if(em && em.gaugeMeter && json.om) {
|
||||
if(xp) {
|
||||
var v = parseInt(json.e);
|
||||
var pct = (v*100)/(parseInt(json.om)*1000);
|
||||
var append = "W";
|
||||
@@ -301,52 +587,78 @@ var fetch = function() {
|
||||
v = (v/1000).toFixed(1);
|
||||
append = "kW";
|
||||
}
|
||||
em.gaugeMeter({
|
||||
percent: pct,
|
||||
text: v,
|
||||
append: append
|
||||
});
|
||||
$('.epo').html(v);
|
||||
$('.epoa').html(append);
|
||||
var arr = [
|
||||
['Slice', 'Value'],
|
||||
['', (pct*2.88)],
|
||||
['', ((100-pct)*2.88)],
|
||||
['', 72],
|
||||
];
|
||||
xo.slices[0].color = ampcol(pct);
|
||||
xa = google.visualization.arrayToDataTable(arr);
|
||||
xp.draw(xa, xo);
|
||||
}
|
||||
|
||||
if(vm && vm.gaugeMeter) {
|
||||
if(vp) {
|
||||
var c = 0;
|
||||
var t = 0;
|
||||
var r = 1;
|
||||
var arr = [['Phase', 'Voltage', { role: 'style' }, { role: 'annotation' }]];
|
||||
if(json.u1) {
|
||||
t += parseFloat(json.u1);
|
||||
var u1 = parseFloat(json.u1);
|
||||
t += u1;
|
||||
c++;
|
||||
var pct = (Math.max(parseFloat(json.u1)-195.5, 1)*100/69);
|
||||
arr[r++] = ['L1', u1, "color: " + voltcol(pct) + ";opacity: 0.9;", u1 + "V"];
|
||||
}
|
||||
if(json.u2) {
|
||||
t += parseFloat(json.u2);
|
||||
var u2 = parseFloat(json.u2);
|
||||
t += u2;
|
||||
c++;
|
||||
var pct = (Math.max(parseFloat(json.u2)-195.5, 1)*100/69);
|
||||
arr[r++] = ['L2', u2, "color: " + voltcol(pct) + ";opacity: 0.9;", u2 + "V"];
|
||||
}
|
||||
if(json.u3) {
|
||||
t += parseFloat(json.u3);
|
||||
var u3 = parseFloat(json.u3);
|
||||
t += u3;
|
||||
c++;
|
||||
var pct = (Math.max(parseFloat(json.u3)-195.5, 1)*100/69);
|
||||
arr[r++] = ['L3', u3, "color: " + voltcol(pct) + ";opacity: 0.9;", u3 + "V"];
|
||||
}
|
||||
v = t/c;
|
||||
var pct = (Math.max(v-207, 1)*100/46);
|
||||
vm.gaugeMeter({
|
||||
percent: pct,
|
||||
text: v.toFixed(1)
|
||||
});
|
||||
if(v > 0) {
|
||||
va = google.visualization.arrayToDataTable(arr);
|
||||
vp.draw(va, vo);
|
||||
}
|
||||
}
|
||||
|
||||
if(am && am.gaugeMeter && json.mf) {
|
||||
if(ap && json.mf) {
|
||||
var a = 0;
|
||||
var r = 1;
|
||||
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];
|
||||
if(json.i1) {
|
||||
a = Math.max(a, parseFloat(json.i1));
|
||||
var i1 = parseFloat(json.i1);
|
||||
a = Math.max(a, i1);
|
||||
var pct = (parseFloat(json.i1)/parseInt(json.mf))*100;
|
||||
arr[r++] = ['L1', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i1 + "A"];
|
||||
}
|
||||
if(json.i2) {
|
||||
a = Math.max(a, parseFloat(json.i2));
|
||||
var i2 = parseFloat(json.i2);
|
||||
a = Math.max(a, i2);
|
||||
var pct = (parseFloat(json.i2)/parseInt(json.mf))*100;
|
||||
arr[r++] = ['L2', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i2 + "A"];
|
||||
}
|
||||
if(json.i3) {
|
||||
a = Math.max(a, parseFloat(json.i3));
|
||||
var i3 = parseFloat(json.i3);
|
||||
a = Math.max(a, i3);
|
||||
var pct = (parseFloat(json.i3)/parseInt(json.mf))*100;
|
||||
arr[r++] = ['L3', pct, "color: " + ampcol(pct) + ";opacity: 0.9;", i3 + "A"];
|
||||
}
|
||||
if(a > 0) {
|
||||
aa = google.visualization.arrayToDataTable(arr);
|
||||
ap.draw(aa, ao);
|
||||
}
|
||||
var pct = (a*100)/parseInt(json.mf);
|
||||
am.gaugeMeter({
|
||||
percent: pct,
|
||||
text: a.toFixed(1)
|
||||
});
|
||||
}
|
||||
|
||||
if(json.me) {
|
||||
@@ -360,6 +672,12 @@ var fetch = function() {
|
||||
$('.jt').html("N/A");
|
||||
}
|
||||
setTimeout(fetch, interval);
|
||||
|
||||
var price = parseFloat(json.p);
|
||||
if(price && price != pl) {
|
||||
pl = price;
|
||||
drawPrices();
|
||||
}
|
||||
}).fail(function(x, text, error) {
|
||||
console.log("Failed request");
|
||||
console.log(text);
|
||||
@@ -376,6 +694,7 @@ var fetch = function() {
|
||||
var upgrade = function() {
|
||||
if(nextVersion) {
|
||||
if(confirm("Are you sure you want to perform upgrade to " + nextVersion.tag_name + "?")) {
|
||||
$('#loading-indicator').show();
|
||||
window.location.href="/upgrade?version=" + nextVersion.tag_name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"i1" : %.2f,
|
||||
"i2" : %.2f,
|
||||
"i3" : %.2f,
|
||||
"f" : %.2f,
|
||||
"f1" : %.2f,
|
||||
"f2" : %.2f,
|
||||
"f3" : %.2f,
|
||||
"v" : %.3f,
|
||||
"r" : %d,
|
||||
"t" : %.2f,
|
||||
@@ -25,5 +29,6 @@
|
||||
"hm" : %d,
|
||||
"wm" : %d,
|
||||
"mm" : %d,
|
||||
"me" : %d
|
||||
"me" : %d,
|
||||
"p" : %s
|
||||
}
|
||||
26
web/dayplot.json
Normal file
26
web/dayplot.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"h00" : %.2f,
|
||||
"h01" : %.2f,
|
||||
"h02" : %.2f,
|
||||
"h03" : %.2f,
|
||||
"h04" : %.2f,
|
||||
"h05" : %.2f,
|
||||
"h06" : %.2f,
|
||||
"h07" : %.2f,
|
||||
"h08" : %.2f,
|
||||
"h09" : %.2f,
|
||||
"h10" : %.2f,
|
||||
"h11" : %.2f,
|
||||
"h12" : %.2f,
|
||||
"h13" : %.2f,
|
||||
"h14" : %.2f,
|
||||
"h15" : %.2f,
|
||||
"h16" : %.2f,
|
||||
"h17" : %.2f,
|
||||
"h18" : %.2f,
|
||||
"h19" : %.2f,
|
||||
"h20" : %.2f,
|
||||
"h21" : %.2f,
|
||||
"h22" : %.2f,
|
||||
"h23" : %.2f
|
||||
}
|
||||
39
web/energyprice.json
Normal file
39
web/energyprice.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"currency" : "%s",
|
||||
"00" : %s,
|
||||
"01" : %s,
|
||||
"02" : %s,
|
||||
"03" : %s,
|
||||
"04" : %s,
|
||||
"05" : %s,
|
||||
"06" : %s,
|
||||
"07" : %s,
|
||||
"08" : %s,
|
||||
"09" : %s,
|
||||
"10" : %s,
|
||||
"11" : %s,
|
||||
"12" : %s,
|
||||
"13" : %s,
|
||||
"14" : %s,
|
||||
"15" : %s,
|
||||
"16" : %s,
|
||||
"17" : %s,
|
||||
"18" : %s,
|
||||
"19" : %s,
|
||||
"20" : %s,
|
||||
"21" : %s,
|
||||
"22" : %s,
|
||||
"23" : %s,
|
||||
"24" : %s,
|
||||
"25" : %s,
|
||||
"26" : %s,
|
||||
"27" : %s,
|
||||
"28" : %s,
|
||||
"29" : %s,
|
||||
"30" : %s,
|
||||
"31" : %s,
|
||||
"32" : %s,
|
||||
"33" : %s,
|
||||
"34" : %s,
|
||||
"35" : %s
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<select name="ea" class="form-control">
|
||||
<optgroup label="Norway">
|
||||
<option value="10YNO-1--------2" {eaNo1}>NO1</option>
|
||||
<option value="10YNO-2--------T" {eaNo1}>NO2</option>
|
||||
<option value="10YNO-2--------T" {eaNo2}>NO2</option>
|
||||
<option value="10YNO-3--------J" {eaNo3}>NO3</option>
|
||||
<option value="10YNO-4--------9" {eaNo4}>NO4</option>
|
||||
<option value="10Y1001A1001A48H" {eaNo5}>NO5</option>
|
||||
|
||||
34
web/firmware.html
Normal file
34
web/firmware.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<div class="alert alert-danger">
|
||||
WARNING: Units powered over M-bus must be connected to an external power supply during firmware upload. Failure to do so may cause power-down during upload resulting in non-functioning unit.
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
Your board is using {chipset} chipset. Only upload firmware designed for this chipset. Failure to do so may result in non-functioning unit.
|
||||
<span id="fwDownload" style="display: none;"><br/>Download latest firmware file <a id="fwLink" href="#" data-chipset="{chipset}">here</a></span>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="upload-form">
|
||||
<div class="my-3 p-3 bg-white rounded shadow">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Upload</span>
|
||||
</div>
|
||||
<div class="custom-file">
|
||||
<input name="file" type="file" class="custom-file-input" id="fileUploadField">
|
||||
<label class="custom-file-label" for="fileUploadField">Choose file</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="row form-group">
|
||||
<div class="col-6">
|
||||
<a href="javascript:history.back();" class="btn btn-outline-secondary">Back</a>
|
||||
</div>
|
||||
<div class="col-6 text-right">
|
||||
<button class="btn btn-primary">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,12 +1,20 @@
|
||||
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>! <a id="newVersionUrl" href="#" target="_blank">view</a></div>
|
||||
<div id="newVersion" class="alert alert-info d-none">New version <span id="newVersionTag"></span>!
|
||||
<a id="newVersionUrl" href="#" target="_blank">view</a>
|
||||
<span class="d-none ssl-capable"> or <a href="javascript:upgrade();">upgrade</a></span>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="loading-indicator" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #dddddd99; z-index: 999999; padding-top: 20%; display: none;" class="text-center">
|
||||
<div class="spinner-border text-primary" role="status" style="width: 5rem; height: 5rem;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
|
||||
<script src="gaugemeter.js"></script>
|
||||
<script src="https://www.gstatic.com/charts/loader.js"></script>
|
||||
<script src="application-${version}.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
/*
|
||||
* AshAlom Gauge Meter. Version 2.0.0
|
||||
* Copyright AshAlom.com All rights reserved.
|
||||
* https://github.com/AshAlom/GaugeMeter <- Deleted!
|
||||
* https://github.com/githubsrinath/GaugeMeter <- Backup original.
|
||||
*
|
||||
* Original created by Dr Ash Alom
|
||||
*
|
||||
* This is a bug fixed and modified version of the AshAlom Gauge Meter.
|
||||
* Copyright 2018 Michael Wolf (Mictronics)
|
||||
* https://github.com/mictronics/GaugeMeter
|
||||
*
|
||||
*/
|
||||
!function ($) {
|
||||
$.fn.gaugeMeter = function (t) {
|
||||
var defaults = $.extend({
|
||||
id: "",
|
||||
percent: 0,
|
||||
used: null,
|
||||
min: null,
|
||||
total: null,
|
||||
size: 100,
|
||||
prepend: "",
|
||||
append: "",
|
||||
theme: "Red-Gold-Green",
|
||||
color: "",
|
||||
back: "RGBa(0,0,0,.06)",
|
||||
width: 3,
|
||||
style: "Full",
|
||||
stripe: "0",
|
||||
animationstep: 1,
|
||||
animate_gauge_colors: false,
|
||||
animate_text_colors: false,
|
||||
label: "",
|
||||
label_color: "Black",
|
||||
text: "",
|
||||
text_size: 0.22,
|
||||
fill: "",
|
||||
showvalue: false
|
||||
}, t);
|
||||
return this.each(function () {
|
||||
|
||||
function getThemeColor(e) {
|
||||
var t = "#2C94E0";
|
||||
return e || (e = 1e-14),
|
||||
"Red-Gold-Green" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#e32100"), e > 20 && (t = "#f35100"), e > 30 && (t = "#ff8700"), e > 40 && (t = "#ffb800"), e > 50 && (t = "#ffd900"), e > 60 && (t = "#dcd800"), e > 70 && (t = "#a6d900"), e > 80 && (t = "#69d900"), e > 90 && (t = "#32d900")),
|
||||
"Green-Gold-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#69d900"), e > 20 && (t = "#a6d900"), e > 30 && (t = "#dcd800"), e > 40 && (t = "#ffd900"), e > 50 && (t = "#ffb800"), e > 60 && (t = "#ff8700"), e > 70 && (t = "#f35100"), e > 80 && (t = "#e32100"), e > 90 && (t = "#d90000")),
|
||||
"Green-Red" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#41c900"), e > 20 && (t = "#56b300"), e > 30 && (t = "#6f9900"), e > 40 && (t = "#8a7b00"), e > 50 && (t = "#a75e00"), e > 60 && (t = "#c24000"), e > 70 && (t = "#db2600"), e > 80 && (t = "#f01000"), e > 90 && (t = "#ff0000")),
|
||||
"Red-Green" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#f01000"), e > 20 && (t = "#db2600"), e > 30 && (t = "#c24000"), e > 40 && (t = "#a75e00"), e > 50 && (t = "#8a7b00"), e > 60 && (t = "#6f9900"), e > 70 && (t = "#56b300"), e > 80 && (t = "#41c900"), e > 90 && (t = "#32d900")),
|
||||
"DarkBlue-LightBlue" === option.theme && (e > 0 && (t = "#2c94e0"), e > 10 && (t = "#2b96e1"), e > 20 && (t = "#2b99e4"), e > 30 && (t = "#2a9ce7"), e > 40 && (t = "#28a0e9"), e > 50 && (t = "#26a4ed"), e > 60 && (t = "#25a8f0"), e > 70 && (t = "#24acf3"), e > 80 && (t = "#23aff5"), e > 90 && (t = "#21b2f7")),
|
||||
"LightBlue-DarkBlue" === option.theme && (e > 0 && (t = "#21b2f7"), e > 10 && (t = "#23aff5"), e > 20 && (t = "#24acf3"), e > 30 && (t = "#25a8f0"), e > 40 && (t = "#26a4ed"), e > 50 && (t = "#28a0e9"), e > 60 && (t = "#2a9ce7"), e > 70 && (t = "#2b99e4"), e > 80 && (t = "#2b96e1"), e > 90 && (t = "#2c94e0")),
|
||||
"DarkRed-LightRed" === option.theme && (e > 0 && (t = "#d90000"), e > 10 && (t = "#dc0000"), e > 20 && (t = "#e00000"), e > 30 && (t = "#e40000"), e > 40 && (t = "#ea0000"), e > 50 && (t = "#ee0000"), e > 60 && (t = "#f30000"), e > 70 && (t = "#f90000"), e > 80 && (t = "#fc0000"), e > 90 && (t = "#ff0000")),
|
||||
"LightRed-DarkRed" === option.theme && (e > 0 && (t = "#ff0000"), e > 10 && (t = "#fc0000"), e > 20 && (t = "#f90000"), e > 30 && (t = "#f30000"), e > 40 && (t = "#ee0000"), e > 50 && (t = "#ea0000"), e > 60 && (t = "#e40000"), e > 70 && (t = "#e00000"), e > 80 && (t = "#dc0000"), e > 90 && (t = "#d90000")),
|
||||
"DarkGreen-LightGreen" === option.theme && (e > 0 && (t = "#32d900"), e > 10 && (t = "#33db00"), e > 20 && (t = "#34df00"), e > 30 && (t = "#34e200"), e > 40 && (t = "#36e700"), e > 50 && (t = "#37ec00"), e > 60 && (t = "#38f100"), e > 70 && (t = "#38f600"), e > 80 && (t = "#39f900"), e > 90 && (t = "#3afc00")),
|
||||
"LightGreen-DarkGreen" === option.theme && (e > 0 && (t = "#3afc00"), e > 10 && (t = "#39f900"), e > 20 && (t = "#38f600"), e > 30 && (t = "#38f100"), e > 40 && (t = "#37ec00"), e > 50 && (t = "#36e700"), e > 60 && (t = "#34e200"), e > 70 && (t = "#34df00"), e > 80 && (t = "#33db00"), e > 90 && (t = "#32d900")),
|
||||
"DarkGold-LightGold" === option.theme && (e > 0 && (t = "#ffb800"), e > 10 && (t = "#ffba00"), e > 20 && (t = "#ffbd00"), e > 30 && (t = "#ffc200"), e > 40 && (t = "#ffc600"), e > 50 && (t = "#ffcb00"), e > 60 && (t = "#ffcf00"), e > 70 && (t = "#ffd400"), e > 80 && (t = "#ffd600"), e > 90 && (t = "#ffd900")),
|
||||
"LightGold-DarkGold" === option.theme && (e > 0 && (t = "#ffd900"), e > 10 && (t = "#ffd600"), e > 20 && (t = "#ffd400"), e > 30 && (t = "#ffcf00"), e > 40 && (t = "#ffcb00"), e > 50 && (t = "#ffc600"), e > 60 && (t = "#ffc200"), e > 70 && (t = "#ffbd00"), e > 80 && (t = "#ffba00"), e > 90 && (t = "#ffb800")),
|
||||
"Voltage" === option.theme && (e <= 0 && (t = "#d90000"), e > 0 && (t = "#e32100"), e > 10 && (t = "#ffb800"), e > 20 && (t = "#dcd800"), e > 30 && (t = "#32d900"), e > 40 && (t = "#32d900"), e > 50 && (t = "#32d900"), e > 60 && (t = "#32d900"), e > 70 && (t = "#dcd800"), e > 80 && (t = "#ffb800"), e > 90 && (t = "#e32100"), e >= 100 && (t = "#d90000")),
|
||||
"White" === option.theme && (t = "#fff"),
|
||||
"Black" === option.theme && (t = "#000"),
|
||||
t;
|
||||
};
|
||||
/* The label below gauge. */
|
||||
function createLabel(t, a) {
|
||||
if(t.children("b").length === 0){
|
||||
$("<b></b>").appendTo(t).html(option.label).css({
|
||||
"line-height": option.size + 5 * a + "px",
|
||||
color: option.label_color
|
||||
});
|
||||
}
|
||||
};
|
||||
/* Prepend and append text, the gauge text or percentage value. */
|
||||
function createSpanTag(t) {
|
||||
var fgcolor = "";
|
||||
if (option.animate_text_colors === true){
|
||||
fgcolor = option.fgcolor;
|
||||
}
|
||||
var child = t.children("span");
|
||||
if(child.length !== 0){
|
||||
child.html(r).css({color: fgcolor});
|
||||
return;
|
||||
}
|
||||
if(option.text_size <= 0.0 || Number.isNaN(option.text_size)){
|
||||
option.text_size = 0.22;
|
||||
}
|
||||
if(option.text_size > 0.5){
|
||||
option.text_size = 0.5;
|
||||
}
|
||||
$("<span></span>").appendTo(t).html(r).css({
|
||||
"line-height": option.size + "px",
|
||||
"font-size": option.text_size * option.size + "px",
|
||||
color: fgcolor
|
||||
});
|
||||
};
|
||||
/* Get data attributes as options from div tag. Fall back to defaults when not exists. */
|
||||
function getDataAttr(t) {
|
||||
$.each(dataAttr, function (index, element) {
|
||||
if(t.data(element) !== undefined && t.data(element) !== null){
|
||||
option[element] = t.data(element);
|
||||
} else {
|
||||
option[element] = $(defaults).attr(element);
|
||||
}
|
||||
|
||||
if(element === "fill"){
|
||||
s = option[element];
|
||||
}
|
||||
|
||||
if((element === "size" ||
|
||||
element === "width" ||
|
||||
element === "animationstep" ||
|
||||
element === "stripe"
|
||||
) && !Number.isInteger(option[element])){
|
||||
option[element] = parseInt(option[element]);
|
||||
}
|
||||
|
||||
if(element === "text_size"){
|
||||
option[element] = parseFloat(option[element]);
|
||||
}
|
||||
});
|
||||
};
|
||||
/* Draws the gauge. */
|
||||
function drawGauge(a) {
|
||||
if(M < 0) M = 0;
|
||||
if(M > 100) M = 100;
|
||||
var lw = option.width < 1 || isNaN(option.width) ? option.size / 20 : option.width;
|
||||
g.clearRect(0, 0, b.width, b.height);
|
||||
g.beginPath();
|
||||
g.arc(m, v, x, G, k, !1);
|
||||
if(s){
|
||||
g.fillStyle = option.fill;
|
||||
g.fill();
|
||||
}
|
||||
g.lineWidth = lw;
|
||||
g.strokeStyle = option.back;
|
||||
option.stripe > parseInt(0) ? g.setLineDash([option.stripe], 1) : g.lineCap = "round";
|
||||
g.stroke();
|
||||
g.beginPath();
|
||||
g.arc(m, v, x, -I, P * a - I, !1);
|
||||
g.lineWidth = lw;
|
||||
g.strokeStyle = option.fgcolor;
|
||||
g.stroke();
|
||||
c > M && (M += z, requestAnimationFrame(function(){
|
||||
drawGauge(Math.min(M, c) / 100);
|
||||
}, p));
|
||||
};
|
||||
|
||||
$(this).attr("data-id", $(this).attr("id"));
|
||||
var r,
|
||||
dataAttr = ["percent",
|
||||
"used",
|
||||
"min",
|
||||
"total",
|
||||
"size",
|
||||
"prepend",
|
||||
"append",
|
||||
"theme",
|
||||
"color",
|
||||
"back",
|
||||
"width",
|
||||
"style",
|
||||
"stripe",
|
||||
"animationstep",
|
||||
"animate_gauge_colors",
|
||||
"animate_text_colors",
|
||||
"label",
|
||||
"label_color",
|
||||
"text",
|
||||
"text_size",
|
||||
"fill",
|
||||
"showvalue"],
|
||||
option = {},
|
||||
c = 0,
|
||||
p = $(this),
|
||||
s = false;
|
||||
p.addClass("gaugeMeter");
|
||||
getDataAttr(p);
|
||||
|
||||
if(Number.isInteger(option.used) && Number.isInteger(option.total)){
|
||||
var u = option.used;
|
||||
var t = option.total;
|
||||
if(Number.isInteger(option.min)) {
|
||||
if(option.min < 0) {
|
||||
t -= option.min;
|
||||
u -= option.min;
|
||||
}
|
||||
}
|
||||
c = u / (t / 100);
|
||||
} else {
|
||||
if(Number.isInteger(option.percent)){
|
||||
c = option.percent;
|
||||
} else {
|
||||
c = parseInt(defaults.percent);
|
||||
}
|
||||
}
|
||||
if(c < 0) c = 0;
|
||||
if(c > 100) c = 100;
|
||||
|
||||
if( option.text !== "" && option.text !== null && option.text !== undefined){
|
||||
if(option.append !== "" && option.append !== null && option.append !== undefined){
|
||||
r = option.text + "<u>" + option.append + "</u>";
|
||||
} else {
|
||||
r = option.text;
|
||||
}
|
||||
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
|
||||
r = "<s>" + option.prepend + "</s>" + r;
|
||||
}
|
||||
} else {
|
||||
if(defaults.showvalue === true || option.showvalue === true){
|
||||
r = option.used;
|
||||
} else {
|
||||
r = c.toString();
|
||||
}
|
||||
if(option.prepend !== "" && option.prepend !== null && option.prepend !== undefined){
|
||||
r = "<s>" + option.prepend + "</s>" + r;
|
||||
}
|
||||
|
||||
if(option.append !== "" && option.append !== null && option.append !== undefined){
|
||||
r = r + "<u>" + option.append + "</u>";
|
||||
}
|
||||
}
|
||||
|
||||
option.fgcolor = getThemeColor(c);
|
||||
if(option.color !== "" && option.color !== null && option.color !== undefined){
|
||||
option.fgcolor = option.color;
|
||||
}
|
||||
|
||||
if(option.animate_gauge_colors === true){
|
||||
option.fgcolor = getThemeColor(c);
|
||||
}
|
||||
createSpanTag(p);
|
||||
|
||||
if(option.style !== "" && option.style !== null && option.style !== undefined){
|
||||
createLabel(p, option.size / 13);
|
||||
}
|
||||
|
||||
$(this).width(option.size + "px");
|
||||
|
||||
var b = $("<canvas></canvas>").attr({width: option.size, height: option.size}).get(0),
|
||||
g = b.getContext("2d"),
|
||||
m = b.width / 2,
|
||||
v = b.height / 2,
|
||||
_ = 360 * option.percent,
|
||||
x = (_ * (Math.PI / 180), b.width / 2.5),
|
||||
k = 2.3 * Math.PI,
|
||||
G = 0,
|
||||
M = 0 === option.animationstep ? c : 0,
|
||||
z = Math.max(option.animationstep, 0),
|
||||
P = 2 * Math.PI,
|
||||
I = Math.PI / 2,
|
||||
R = option.style;
|
||||
var child = $(this).children("canvas");
|
||||
if(child.length !== 0){
|
||||
/* Replace existing canvas when new percentage was written. */
|
||||
child.replaceWith(b);
|
||||
} else {
|
||||
/* Initially create canvas. */
|
||||
$(b).appendTo($(this));
|
||||
}
|
||||
|
||||
if ("Semi" === R){
|
||||
k = 2 * Math.PI;
|
||||
G = 3.13;
|
||||
P = 1 * Math.PI;
|
||||
I = Math.PI / .996;
|
||||
}
|
||||
if ("Arch" === R){
|
||||
k = 2.195 * Math.PI;
|
||||
G = 1, G = 655.99999;
|
||||
P = 1.4 * Math.PI;
|
||||
I = Math.PI / .8335;
|
||||
}
|
||||
drawGauge(M / 100);
|
||||
});
|
||||
};
|
||||
}
|
||||
(jQuery);
|
||||
@@ -60,6 +60,24 @@
|
||||
</div>
|
||||
<input name="vccPin" type="number" min="0" max="${gpio.max}" class="form-control" value="${config.vccPin}"/>
|
||||
</div>
|
||||
<div class="m-2 input-group input-group-sm" style="width: 200px;">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">GND resistor</span>
|
||||
</div>
|
||||
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorGnd" value="${config.vccResistorGnd}" />
|
||||
<div class="input-group-append" title="Inverted">
|
||||
<label class="input-group-text">kΩ</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-2 input-group input-group-sm" style="width: 190px;">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Vcc resistor</span>
|
||||
</div>
|
||||
<input type="number" min="1" max="1000" step="1" class="form-control" name="vccResistorVcc" value="${config.vccResistorVcc}" />
|
||||
<div class="input-group-append" title="Inverted">
|
||||
<label class="input-group-text">kΩ</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-2 input-group input-group-sm" style="width: 140px;">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Multiplier</span>
|
||||
@@ -70,7 +88,7 @@
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Offset</span>
|
||||
</div>
|
||||
<input type="number" min="0.1" max="3.5" step="0.01" class="form-control" name="vccOffset" value="${config.vccOffset}" />
|
||||
<input type="number" min="0.0" max="3.5" step="0.01" class="form-control" name="vccOffset" value="${config.vccOffset}" />
|
||||
</div>
|
||||
<div class="m-2 input-group input-group-sm" style="width: 130px;">
|
||||
<div class="input-group-prepend">
|
||||
|
||||
@@ -6,51 +6,40 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
|
||||
<style>
|
||||
.GaugeMeter {
|
||||
position: Relative;
|
||||
text-align: Center;
|
||||
overflow: Hidden;
|
||||
cursor: Default;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.GaugeMeter SPAN, .GaugeMeter B {
|
||||
width: 54%;
|
||||
position: Absolute;
|
||||
text-align: Center;
|
||||
display: Inline-Block;
|
||||
color: RGBa(0,0,0,.8);
|
||||
font-weight: 100;
|
||||
font-family: "Open Sans", Arial;
|
||||
overflow: Hidden;
|
||||
white-space: NoWrap;
|
||||
text-overflow: Ellipsis;
|
||||
margin: 0 23%;
|
||||
}
|
||||
|
||||
.GaugeMeter[data-style="Semi"] B {
|
||||
width: 80%;
|
||||
margin: 0 10%;
|
||||
}
|
||||
|
||||
.GaugeMeter S, .GaugeMeter U {
|
||||
text-decoration: None;
|
||||
font-size: .60em;
|
||||
font-weight: 200;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.GaugeMeter B {
|
||||
color: #000;
|
||||
font-weight: 200;
|
||||
opacity: .8;
|
||||
}
|
||||
.navbar-expand .navbar-nav .nav-link,.navbar-brand {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.plot1 {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.plot2 {
|
||||
width: 100%;
|
||||
height: 224px;
|
||||
}
|
||||
.overlay-plot {
|
||||
position: relative;
|
||||
}
|
||||
.plot-overlay {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
.ipo,.xpo {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
.ipoa,.xpoa {
|
||||
font-size: 1.0rem;
|
||||
color: grey;
|
||||
}
|
||||
.pol {
|
||||
font-size: 1.0rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
@@ -64,9 +53,6 @@
|
||||
<li class="nav-item">
|
||||
<a id="temp-link" class="nav-link" href="/temperature">Temp<span class="d-none d-sm-inline">erature</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a id="price-link" class="nav-link" href="/price"><span class="d-none d-sm-inline">Energy</span> Price</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle nav-link" href="#" role="button" id="config-link" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
103
web/head8266.html
Normal file
103
web/head8266.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AMS reader</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
|
||||
<style>
|
||||
.navbar-expand .navbar-nav .nav-link,.navbar-brand {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
.plot1 {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
.plot2 {
|
||||
width: 100%;
|
||||
height: 224px;
|
||||
}
|
||||
.overlay-plot {
|
||||
position: relative;
|
||||
}
|
||||
.plot-overlay {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
.ipo,.xpo {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
.ipoa,.xpoa {
|
||||
font-size: 1.0rem;
|
||||
color: grey;
|
||||
}
|
||||
.pol {
|
||||
font-size: 1.0rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<main role="main" class="container">
|
||||
<header class="navbar navbar-expand navbar-dark flex-column flex-lg-row rounded shadow mt-2 mb-3" style="background-color: var(--purple);">
|
||||
<a href="/" class="">
|
||||
<h6 class="navbar-brand">AMS reader <small id="swVersion" data-url="https://api.github.com/repos/gskjold/AmsToMqttBridge/releases">${version}</small></h6>
|
||||
</a>
|
||||
<div class="navbar-nav-scroll">
|
||||
<ul class="navbar-nav bd-navbar-nav flex-row">
|
||||
<li class="nav-item">
|
||||
<a id="temp-link" class="nav-link" href="/temperature">Temp<span class="d-none d-sm-inline">erature</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle nav-link" href="#" role="button" id="config-link" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Config<span class="d-none d-md-inline d-lg-none d-xl-inline">uration</span>
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu" aria-labelledby="config-link">
|
||||
<a class="dropdown-item" href="/meter">Meter</a>
|
||||
<a class="dropdown-item" href="/wifi">WiFi</a>
|
||||
<a class="dropdown-item" href="/mqtt">MQTT</a>
|
||||
<a class="dropdown-item" href="/web">Web</a>
|
||||
<a class="dropdown-item" href="/ntp">NTP</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="https://github.com/gskjold/AmsToMqttBridge/wiki" target="_blank">Documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-toggle nav-link" href="#" role="button" id="system-link" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Sys<span class="d-none d-sm-inline">tem</span>
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="system-link">
|
||||
<a class="dropdown-item" href="/gpio">GPIO</a>
|
||||
<a class="dropdown-item" href="/debugging">Debugging</a>
|
||||
<a class="dropdown-item" href="/firmware">Firmware</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="/restart">Restart</a>
|
||||
<a class="dropdown-item text-danger" href="/reset">Factory reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
<div id="esp" class="d-none m-2">ESP</div>
|
||||
<div id="han" class="d-none m-2">HAN</div>
|
||||
<div id="wifi" class="d-none m-2">WiFi</div>
|
||||
<div id="mqtt" class="d-none m-2">MQTT</div>
|
||||
</div>
|
||||
<ul class="navbar-nav flex-row ml-md-auto d-none d-lg-flex">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link p-2" href="https://github.com/gskjold/AmsToMqttBridge" target="_blank" rel="noopener" aria-label="GitHub">
|
||||
<img class="d-inline-block align-text-top" style="width: 2rem; height: 2rem;" src="github.svg"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
190
web/index.html
190
web/index.html
@@ -1,132 +1,121 @@
|
||||
<div class="bg-white rounded shadow p-1">
|
||||
<div class="bg-white rounded shadow p-1 mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-2 col-6">
|
||||
<div class="text-center">Up <span class="ju">{cs}</span></div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="text-center">Temperature: <span class="jt">{temp}</span>°C</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-2 col-6">
|
||||
<div class="text-center">ESP volt: <span class="jv">{vcc}</span>V</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="text-center">WiFi RSSI: <span class="jr">{rssi}</span>dBm</div>
|
||||
</div>
|
||||
<div class="col-md-2 col-6">
|
||||
<div class="text-center">Free mem: <span class="jm">{mem}</span>kb</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="row">
|
||||
<div class="col-sm-6 mt-3">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center">
|
||||
<div class="SimpleMeter ji" style="display: inline;">
|
||||
{P} W
|
||||
</div>
|
||||
<div id="im" class="GaugeMeter rounded"
|
||||
style="display: none;"
|
||||
data-size="180px"
|
||||
data-text_size="0.15"
|
||||
data-width="25"
|
||||
data-style="Arch"
|
||||
data-theme="Green-Gold-Red"
|
||||
data-animationstep="0"
|
||||
data-label="{ti}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row ric" style="display: {da};">
|
||||
<div class="col-12 text-right"><span class="jic">{tPI}</span> kWh</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mb-3">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center overlay-plot">
|
||||
<div id="ip" class="plot1"></div>
|
||||
<span class="plot-overlay">
|
||||
<span class="ipo">{P}</span>
|
||||
<span class="ipoa">W</span>
|
||||
<br/>
|
||||
<span class="pol">{ti}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-3" style="display: {de};">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center">
|
||||
<div class="SimpleMeter je" style="display: inline;">
|
||||
{PO} W
|
||||
</div>
|
||||
<div id="em" class="GaugeMeter rounded"
|
||||
style="display: none;"
|
||||
data-size="180px"
|
||||
data-text_size="0.15"
|
||||
data-width="25"
|
||||
data-style="Arch"
|
||||
data-theme="DarkGreen-LightGreen"
|
||||
data-animationstep="0"
|
||||
data-label="Export"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row rec" style="display: {da};">
|
||||
<div class="col-12 text-right"><span class="jec">{tPO}</span> kWh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 mt-3" style="display: {dn};">
|
||||
<div class="bg-white rounded shadow p-3" style="display: {da};">
|
||||
<h5 class="text-center">Reactive</h5>
|
||||
<div class="row rric">
|
||||
<div class="col-4">In</div>
|
||||
<div class="col-8 text-right"><span class="jric">{tQI}</span> kvarh</div>
|
||||
<div class="col-4">Out</div>
|
||||
<div class="col-8 text-right"><span class="jrec">{tQO}</span> kvarh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 mt-3" style="display: {de};">
|
||||
<div class="bg-white rounded shadow p-3" style="display: {da};">
|
||||
<div class="row rrec">
|
||||
<div class="col-4 col-sm-2">In</div>
|
||||
<div class="col-8 col-sm-4 text-right"><span class="jric">{tQI}</span> kvarh</div>
|
||||
<div class="col-4 col-sm-2">Out</div>
|
||||
<div class="col-8 col-sm-4 text-right"><span class="jrec">{tQO}</span> kvarh</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row ric" style="display: {da};">
|
||||
<div class="col-12 text-right"><span class="jic">{tPI}</span> kWh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mt-3">
|
||||
<div class="col-lg-3 col-sm-6 mb-3" style="display: {de};">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center">
|
||||
<div id="vm" class="GaugeMeter rounded"
|
||||
style="display: none;"
|
||||
data-size="180px"
|
||||
data-text_size="0.15"
|
||||
data-width="25"
|
||||
data-style="Arch"
|
||||
data-theme="Voltage"
|
||||
data-animationstep="0"
|
||||
data-label="Volt"
|
||||
></div>
|
||||
<div class="text-center overlay-plot">
|
||||
<div id="xp" class="plot1"></div>
|
||||
<span class="plot-overlay">
|
||||
<span class="xpo">{PO}</span>
|
||||
<span class="xpoa">W</span>
|
||||
<br/>
|
||||
<span class="pol">Export</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="row ru2" style="display: {3p};">
|
||||
<div class="col-4"><span class="ju1">{U1}</span>V</div>
|
||||
<div class="col-4 text-center"><span class="ju2">{U2}</span>V</div>
|
||||
<div class="col-4 text-right"><span class="ju3">{U3}</span>V</div>
|
||||
<div class="row rec" style="display: {da};">
|
||||
<div class="col-12 text-right"><span class="jec">{tPO}</span> kWh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mb-3 mt-3">
|
||||
<div class="col-lg-3 col-sm-6 mb-3">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center">
|
||||
<div id="am" class="GaugeMeter rounded"
|
||||
style="display: none;"
|
||||
data-size="180px"
|
||||
data-text_size="0.15"
|
||||
data-width="25"
|
||||
data-style="Arch"
|
||||
data-theme="Green-Gold-Red"
|
||||
data-animationstep="0"
|
||||
data-label="Ampere"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row ru2" style="display: {3p};">
|
||||
<div class="col-4"><span class="ji1">{I1}</span>A</div>
|
||||
<div class="col-4 text-center"><span class="ji2">{I2}</span>A</div>
|
||||
<div class="col-4 text-right"><span class="ji3">{I3}</span>A</div>
|
||||
<div id="vp" class="plot2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mb-3">
|
||||
<div class="bg-white rounded shadow p-3">
|
||||
<div class="text-center">
|
||||
<div id="ap" class="plot2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mb-3" style="display: {dn};">
|
||||
<div class="bg-white rounded shadow p-3" style="display: {da};">
|
||||
<h5 class="text-center">Reactive</h5>
|
||||
<div class="row rri">
|
||||
<div class="col-12 font-weight-bold">Instant</div>
|
||||
<div class="col-4">In</div>
|
||||
<div class="col-8 text-right"><span class="jri">{Q}</span> VAr</div>
|
||||
<div class="col-4">Out</div>
|
||||
<div class="col-8 text-right"><span class="jre">{QO}</span> VAr</div>
|
||||
</div>
|
||||
<div class="row rric">
|
||||
<div class="col-12 font-weight-bold">Total</div>
|
||||
<div class="col-4">In</div>
|
||||
<div class="col-8 text-right"><span class="jric">{tQI}</span> kVArh</div>
|
||||
<div class="col-4">Out</div>
|
||||
<div class="col-8 text-right"><span class="jrec">{tQO}</span> kVArh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 mb-3" style="display: {de};">
|
||||
<div class="bg-white rounded shadow p-3" style="display: {da};">
|
||||
<div class="row rrec">
|
||||
<div class="col-sm-4 font-weight-bold">Instant reactive</div>
|
||||
<div class="col-4 col-sm-1">In</div>
|
||||
<div class="col-8 col-sm-3 text-right"><span class="jri">{Q}</span> VAr</div>
|
||||
<div class="col-4 col-sm-1">Out</div>
|
||||
<div class="col-8 col-sm-3 text-right"><span class="jre">{QO}</span> VAr</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 mb-3" style="display: {de};">
|
||||
<div class="bg-white rounded shadow p-3" style="display: {da};">
|
||||
<div class="row rrec">
|
||||
<div class="col-sm-4 font-weight-bold">Total reactive</div>
|
||||
<div class="col-4 col-sm-1">In</div>
|
||||
<div class="col-8 col-sm-3 text-right"><span class="jric">{tQI}</span> kVArh</div>
|
||||
<div class="col-4 col-sm-1">Out</div>
|
||||
<div class="col-8 col-sm-3 text-right"><span class="jrec">{tQO}</span> kVArh</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ppc" class="col-xl-12 mb-3" style="display: none;">
|
||||
<div class="bg-white rounded shadow" id="pp" style="width: 100%; height: 224px;"></div>
|
||||
</div>
|
||||
<div class="col-xl-12 mb-3">
|
||||
<div class="bg-white rounded shadow" id="ep" style="width: 100%; height: 224px;"></div>
|
||||
</div>
|
||||
<div class="col-xl-12 mb-3">
|
||||
<div class="bg-white rounded shadow" id="mp" style="width: 100%; height: 224px;"></div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 mb-3 d-none me me-1 me-2 me-3 me-4 me-5 me-6 me-7 me-8 me-9 me-10 me-11 me-12 me-13">
|
||||
<div class="d-none badge badge-danger me me-1 me-2 me-5 me-6 me-7 me-8 me-9 me-12">MQTT communication error (<span id="ml">-</span>)</div>
|
||||
<div class="d-none badge badge-danger me me-3">MQTT failed to connect</div>
|
||||
@@ -136,3 +125,4 @@
|
||||
<div class="d-none badge badge-danger me me-13">MQTT lost connection</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,23 +6,67 @@
|
||||
<div class="col-lg-3 col-md-4 col-sm-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Meter type</span>
|
||||
<span class="input-group-text">Manufacturer</span>
|
||||
</div>
|
||||
<select id="mt" class="form-control sd" name="m">
|
||||
<option value="0" {m0}>Autodetect</option>
|
||||
<option value="1" {m1}>Kaifa</option>
|
||||
<option value="2" {m2}>Aidon</option>
|
||||
<option value="3" {m3}>Kamstrup non-encrypted</option>
|
||||
<option value="4" {m4}>Kamstrup encrypted</option>
|
||||
<input class="form-control" value="{maf}" disabled/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Model</span>
|
||||
</div>
|
||||
<input class="form-control" value="{mod}" disabled/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">ID</span>
|
||||
</div>
|
||||
<input class="form-control" value="{mid}" disabled/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-4 col-sm-5">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Baud rate</span>
|
||||
</div>
|
||||
<select class="form-control sd" name="b">
|
||||
<option value="2400" {b2400}>2400</option>
|
||||
<option value="9600" {b9600}>9600</option>
|
||||
<option value="115200" {b115200}>115200</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-sm-4 col-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Parity</span>
|
||||
</div>
|
||||
<select class="form-control sd" name="c">
|
||||
<option value="2" {c2}>7N1</option>
|
||||
<option value="3" {c3}>8N1</option>
|
||||
<option value="10" {c10}>7E1</option>
|
||||
<option value="11" {c11}>8E1</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-4 col-sm-3 col-6">
|
||||
<div class="m-2">
|
||||
<label class="small"><input type="checkbox" name="i" value="true" {i}/> Invert <span class="d-none d-md-inline">signal</span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-lg-4 col-md-5 col-sm-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Distribution system</span>
|
||||
</div>
|
||||
<select id="d" class="form-control sd" name="d">
|
||||
<select class="form-control sd" name="d">
|
||||
<option value="0" {d0}></option>
|
||||
<option value="1" {d1}>IT (230V)</option>
|
||||
<option value="2" {d2}>TN (400V)</option>
|
||||
@@ -36,6 +80,8 @@
|
||||
</div>
|
||||
<select class="form-control" name="f">
|
||||
<option value="0" {f0}></option>
|
||||
<option value="16" {f16}>16A</option>
|
||||
<option value="20" {f20}>20A</option>
|
||||
<option value="25" {f25}>25A</option>
|
||||
<option value="32" {f32}>32A</option>
|
||||
<option value="35" {f35}>35A</option>
|
||||
@@ -56,25 +102,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 enc">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Encryption key</span>
|
||||
</div>
|
||||
<input class="form-control" name="e" type="text" value="{e}"/>
|
||||
<input class="form-control" name="e" type="text" value="{e}" placeholder="If applicable"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 enc">
|
||||
<div class="col-lg-6">
|
||||
<div class="m-2 input-group input-group-sm">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">Authentication key</span>
|
||||
</div>
|
||||
<input class="form-control" name="a" type="text" value="{a}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ss" class="col-lg-3 col-md-4 col-sm-5">
|
||||
<div class="m-2">
|
||||
<label class="small"><input type="checkbox" name="s" value="true" {s}/> Substitute missing values</label>
|
||||
<input class="form-control" name="a" type="text" value="{a}" placeholder="If applicable"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
33
web/monthplot.json
Normal file
33
web/monthplot.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"d01" : %.2f,
|
||||
"d02" : %.2f,
|
||||
"d03" : %.2f,
|
||||
"d04" : %.2f,
|
||||
"d05" : %.2f,
|
||||
"d06" : %.2f,
|
||||
"d07" : %.2f,
|
||||
"d08" : %.2f,
|
||||
"d09" : %.2f,
|
||||
"d10" : %.2f,
|
||||
"d11" : %.2f,
|
||||
"d12" : %.2f,
|
||||
"d13" : %.2f,
|
||||
"d14" : %.2f,
|
||||
"d15" : %.2f,
|
||||
"d16" : %.2f,
|
||||
"d17" : %.2f,
|
||||
"d18" : %.2f,
|
||||
"d19" : %.2f,
|
||||
"d20" : %.2f,
|
||||
"d21" : %.2f,
|
||||
"d22" : %.2f,
|
||||
"d23" : %.2f,
|
||||
"d24" : %.2f,
|
||||
"d25" : %.2f,
|
||||
"d26" : %.2f,
|
||||
"d27" : %.2f,
|
||||
"d28" : %.2f,
|
||||
"d29" : %.2f,
|
||||
"d30" : %.2f,
|
||||
"d31" : %.2f
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<form method="post" enctype="multipart/form-data" class="upload-form">
|
||||
<div class="my-3 p-3 bg-white rounded shadow">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
|
||||
Reference in New Issue
Block a user