Compare commits

..

84 Commits

Author SHA1 Message Date
Gunnar Skjold
bd0b3ebb26 Removed debugging 2021-12-12 10:38:40 +01:00
Gunnar Skjold
895a9bc6b1 Fixed api key overwrite 2021-12-12 10:36:50 +01:00
Gunnar Skjold
ff935fc920 Specify that prices only work for ESP32 2021-12-12 08:36:23 +01:00
Gunnar Skjold
3833421e5f Fixed "new version" announcement 2021-12-10 19:09:24 +01:00
Gunnar Skjold
ebdc357a47 Check for scaling and use if available 2021-12-10 18:46:57 +01:00
Gunnar Skjold
7da617e8c2 Merge pull request #154 from gskjold/dev-1.6.0
v2.0
2021-12-10 08:52:10 +01:00
Gunnar Skjold
0b86761d2c Fixed a couple of things before release 2021-12-10 08:38:48 +01:00
Gunnar Skjold
d697f7e37f Final changes before 2.0 2021-12-10 08:19:48 +01:00
Gunnar Skjold
db859e3ff5 Fixed readme 2021-12-08 21:06:52 +01:00
Gunnar Skjold
94d22957bd Removed old screenshot 2021-12-08 21:06:00 +01:00
Gunnar Skjold
1ebaf443cc Updated readme 2021-12-08 21:05:42 +01:00
Gunnar Skjold
a336f711b0 Fixed day graph 2021-12-08 20:35:53 +01:00
Gunnar Skjold
e6d3b47d4f Some adjustment to data storage 2021-12-07 19:34:34 +01:00
Gunnar Skjold
eb479f8216 More adaptations for Austian meters 2021-12-07 19:07:43 +01:00
Gunnar Skjold
38cba4e8da Various fixes 2021-12-06 19:33:40 +01:00
Gunnar Skjold
bc6d45ecf2 Fixed WiFi instability? 2021-12-06 16:58:55 +01:00
Gunnar Skjold
097131d7fb Some changes 2021-12-06 10:47:42 +01:00
Gunnar Skjold
2107ca50e4 More changes for Austrian meters, untested 2021-12-06 10:47:08 +01:00
Gunnar Skjold
178f603937 Fix 2021-12-05 20:14:46 +01:00
Gunnar Skjold
21687368c6 Preparing for austrian meters 2021-12-05 20:09:34 +01:00
Gunnar Skjold
6054e900e6 Experimenting 2021-12-05 13:08:34 +01:00
Gunnar Skjold
3da5275624 Fixed merge problem 2021-12-05 12:21:21 +01:00
Gunnar Skjold
3fda2cfe7a Removed test code 2021-12-05 12:16:13 +01:00
Gunnar Skjold
2bb651f95c Merge branch 'master' into dev-1.6.0 2021-12-05 12:15:39 +01:00
Gunnar Skjold
e2442a26ee Trying to figure out esp8266 wifi reconnect 2021-12-05 12:07:34 +01:00
Gunnar Skjold
cbd2ab4a7a Trying to figure out esp8266 wifi reconnect 2021-12-05 12:07:20 +01:00
Gunnar Skjold
76f8e2c343 2.0 development 2021-12-04 15:41:13 +01:00
Gunnar Skjold
ab101c8622 Some changes after testing 2021-11-30 08:01:20 +01:00
Gunnar Skjold
f425abb52d Added missing file from last commit 2021-11-27 20:29:18 +01:00
Gunnar Skjold
dc5bfff655 Merge branch 'master' into dev-1.6.0 2021-11-27 20:23:35 +01:00
Gunnar Skjold
eb59245118 Added usage plots and ADC reading for Vcc 2021-11-27 20:16:26 +01:00
Gunnar Skjold
d18fd27a24 Fixed accumulated values from Kaifa 1p 2021-11-23 20:55:55 +01:00
Gunnar Skjold
6f09f523e4 Added possibility to upgrade from GUI if there is enough RAM for SSL 2021-11-18 20:42:13 +01:00
Gunnar Skjold
e78d59c31a Different menu for ESP8266 and ESP32 2021-11-18 20:28:29 +01:00
Gunnar Skjold
e7f3217d7b Added support for power factor 2021-11-18 20:20:51 +01:00
Gunnar Skjold
ab534ce60a Ignore firmware update if AP button is pressed 2021-11-18 19:43:48 +01:00
Gunnar Skjold
933246eae8 Changed from SPIFFS to LittleFS 2021-11-18 19:40:20 +01:00
Gunnar Skjold
5c38d1cf3e Fixes after testing 2021-11-18 19:06:33 +01:00
Gunnar Skjold
580085f717 Fixed kamstrup list version 2021-11-17 21:47:00 +01:00
Gunnar Skjold
9831d4aa78 Removed buffer size 2021-11-17 21:46:37 +01:00
Gunnar Skjold
a3561d5c58 Fixed free mem formatting bug 2021-11-17 20:50:30 +01:00
Gunnar Skjold
2a524cd0ac Fixed Kamstrup bug 2021-11-17 20:42:45 +01:00
Gunnar Skjold
254e010594 Fixed save crash 2021-11-16 20:40:21 +01:00
Gunnar Skjold
24025d6785 Added support for IEC62056-21 2021-11-16 20:07:47 +01:00
Gunnar Skjold
4c92e592d6 Added support for uart_swap for ESP8266 2021-11-08 12:05:48 +01:00
Gunnar Skjold
f192ddae81 Some changes while testing 2021-11-06 21:02:38 +01:00
Gunnar Skjold
1cd2446365 Some modifications to support timestamp from all meters 2021-11-06 19:44:50 +01:00
Gunnar Skjold
6d26102b8e New meter config 2021-11-06 19:30:58 +01:00
Gunnar Skjold
8e9da8f255 First step in implementing a new DLMS parser 2021-11-06 16:56:02 +01:00
Gunnar Skjold
72bdb6e363 Update issue templates 2021-11-01 07:14:54 +01:00
Gunnar Skjold
6df942f488 Revert time version update 2021-10-31 09:37:58 +01:00
Gunnar Skjold
1e323ac3b9 Various changes for version updates and some debugging for upload 2021-10-31 09:33:25 +01:00
Gunnar Skjold
05bdbaf1f5 Fixed error 2021-10-30 18:42:59 +02:00
Gunnar Skjold
6cca25788e Fixed copy/paste error 2021-10-23 10:45:57 +02:00
Gunnar Skjold
e929f87ea9 Fixed decimals in json 2021-10-23 10:38:53 +02:00
Gunnar Skjold
21a4102553 Changes for correct time conversion 2021-10-23 10:05:14 +02:00
Gunnar Skjold
768645fc0a Added more decimals in data.json 2021-10-23 10:04:52 +02:00
Gunnar Skjold
c670549dea Warning for BUS powered devices on firmware upgrade 2021-10-23 09:21:53 +02:00
Gunnar Skjold
147b2ca33e Fixed build error 2021-10-23 09:07:07 +02:00
Gunnar Skjold
c645b82ed3 Fixed voltage and amp meters 2021-10-23 09:04:00 +02:00
Gunnar Skjold
26bb8a0fea Support two phase power calculation 2021-10-23 08:59:16 +02:00
Gunnar Skjold
507ed13770 Added GPIO designation to UART in dropdown 2021-10-23 08:40:40 +02:00
Gunnar Skjold
a0f53a0c52 Partial fix of dead gauges 2021-10-21 21:17:03 +02:00
Gunnar Skjold
13bbc81b7f Clear debug config on setup if not already set 2021-09-23 20:56:24 +02:00
Gunnar Skjold
7412ba2697 Removed temp sensor update if no sensors were found 2021-09-23 20:30:22 +02:00
Gunnar Skjold
dce7b7e64b Support for long MQTT username and password 2021-09-23 19:28:55 +02:00
Gunnar Skjold
7627d6c369 Updated readme 2021-09-23 18:48:58 +02:00
Gunnar Skjold
9fc9adea1c Fixed crash when enabling substitute values in meter config 2021-09-23 18:30:15 +02:00
Gunnar Skjold
46df07fe40 Fixed build error on ESP32 2021-09-23 18:20:54 +02:00
Gunnar Skjold
0aeb5555e7 Fixed long web passwords 2021-09-23 18:10:16 +02:00
Gunnar Skjold
181fe3c909 Removed meter from setup 2021-09-23 17:36:43 +02:00
Gunnar Skjold
867d6a4ef8 Added profiles for Pow board 2021-09-23 17:34:21 +02:00
Gunnar Skjold
d52216a73e Moved hardware page to Wiki 2021-09-23 17:28:51 +02:00
Gunnar Skjold
d70b41a454 Added images for Pow 2021-09-23 17:25:52 +02:00
Gunnar Skjold
332c366561 Renamed kamstrup meters 2021-09-22 11:44:59 +02:00
Gunnar Skjold
b572ad97f8 Added Kamstrup document 2021-09-22 11:42:22 +02:00
Gunnar Skjold
aa307f8690 Fixed incorrect value for accumulated export 2021-09-22 11:40:48 +02:00
Gunnar Skjold
19223312b5 Fixed danish KAmstrup 2021-09-13 10:14:47 +02:00
Gunnar Skjold
74bc5aa7a0 Fixed build 2021-09-13 10:14:28 +02:00
Gunnar Skjold
e66e8a96ff Added some frames as documentation 2021-09-13 08:10:39 +02:00
Gunnar Skjold
ce95360a64 Minor fix 2021-09-13 08:08:39 +02:00
Gunnar Skjold
7802aeaab1 Removed unnecessary config for arduinojson 2021-09-13 08:07:38 +02:00
Gunnar Skjold
37ce3566bf Fixed config clear 2021-09-13 08:07:03 +02:00
Gunnar Skjold
606bac100a Cleanup 2021-03-24 09:37:46 +01:00
90 changed files with 4404 additions and 3568 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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
View 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]

View File

@@ -41,7 +41,7 @@ jobs:
python -m pip install --upgrade pip
pip install -U platformio css_html_js_minify
- name: Configure build targets
run: echo "[platformio]\ndefault_envs = hw1esp12e, esp12e, esp32" > platformio-user.ini
run: echo "[platformio]\ndefault_envs = esp8266, esp32" > platformio-user.ini
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ platformio-user.ini
/src/AmsToMqttBridge.ino.cpp
/test
/web/test.html
/sdkconfig

View File

@@ -1,17 +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">
## Hardware options
Look in [hardware section](/hardware) for more details about supported hardware
## Setting up your device
Go to the [WiKi](https://github.com/gskjold/AmsToMqttBridge/wiki) for information on how to set up 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.
@@ -22,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

Binary file not shown.

18
doc/Aidon_OBIS.txt Normal file
View 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
View 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>

View 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

View 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>

Binary file not shown.

30
frames/Aidon-Sweden.raw Normal file
View 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

31
frames/Aidon-TN-3p.raw Normal file
View File

@@ -0,0 +1,31 @@
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 2A 41 08 83 13 04 13 E6 E7 00 0F 40 00 00 00 00 01 01 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 64 02 02 0F 00 16 1B E1
7E A1 1E 41 08 83 13 EE EE E6 E7 00 0F 40 00 00 00 00 01 0D 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 08 6C 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 09 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 41 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 13 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F2 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 D1 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 E8 02 02 0F FF 16 23 8B
7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00 01 12 02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31 02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37 02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34 02 03 09 06 01 00 01 07 00 FF 06 00 00 03 9A 02 02 0F 00 16 1B 02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B 02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D 02 03 09 06 01 00 04 07 00 FF 06 00 00 02 0E 02 02 0F 00 16 1D 02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21 02 03 09 06 01 00 33 07 00 FF 10 00 10 02 02 0F FF 16 21 02 03 09 06 01 00 47 07 00 FF 10 00 0E 02 02 0F FF 16 21 02 03 09 06 01 00 20 07 00 FF 12 08 F4 02 02 0F FF 16 23 02 03 09 06 01 00 34 07 00 FF 12 08 CD 02 02 0F FF 16 23 02 03 09 06 01 00 48 07 00 FF 12 08 DC 02 02 0F FF 16 23 02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 03 18 03 08 00 00 FF 00 00 00 02 03 09 06 01 00 01 08 00 FF 06 00 47 F0 34 02 02 0F 01 16 1E 02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E 02 03 09 06 01 00 03 08 00 FF 06 00 00 21 9E 02 02 0F 01 16 20 02 03 09 06 01 00 04 08 00 FF 06 00 08 E0 21 02 02 0F 01 16 20 57
7E A1 8A 41 08 83 13 EB FD E6 E7 00 0F 40 00 00 00 00
01 12
02 02 09 06 01 01 00 02 81 FF 0A 0B 41 49 44 4F 4E 5F 56 30 30 30 31
02 02 09 06 00 00 60 01 00 FF 0A 10 37 33 35 39 39 39 32 38 39 30 34 39 37 39 39 37
02 02 09 06 00 00 60 01 07 FF 0A 04 36 35 33 34
02 03 09 06 01 00 01 07 00 FF 06 00 00 09 6D 02 02 0F 00 16 1B
02 03 09 06 01 00 02 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1B
02 03 09 06 01 00 03 07 00 FF 06 00 00 00 00 02 02 0F 00 16 1D
02 03 09 06 01 00 04 07 00 FF 06 00 00 02 5B 02 02 0F 00 16 1D
Object with three values Value Object with two values
| Obis code | | Scaling
| | | | | Unit
02 03 09 06 01 00 1F 07 00 FF 10 00 11 02 02 0F FF 16 21
02 03 09 06 01 00 33 07 00 FF 10 00 03 02 02 0F FF 16 21
02 03 09 06 01 00 47 07 00 FF 10 00 5A 02 02 0F FF 16 21
02 03 09 06 01 00 20 07 00 FF 12 09 04 02 02 0F FF 16 23
02 03 09 06 01 00 34 07 00 FF 12 09 02 02 02 0F FF 16 23
02 03 09 06 01 00 48 07 00 FF 12 08 EC 02 02 0F FF 16 23
02 02 09 06 00 00 01 00 00 FF 09 0C 07 E5 0A 1F 00 14 00 00 FF 00 00 00
02 03 09 06 01 00 01 08 00 FF 06 00 56 9F 52 02 02 0F 01 16 1E
02 03 09 06 01 00 02 08 00 FF 06 00 00 00 00 02 02 0F 01 16 1E
02 03 09 06 01 00 03 08 00 FF 06 00 00 22 D0 02 02 0F 01 16 20
02 03 09 06 01 00 04 08 00 FF 06 00 0A F5 EC 02 02 0F 01 16 20
51 D7
7E

34
frames/Kaifa-TN-3p.raw Normal file
View File

@@ -0,0 +1,34 @@
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 27 01 02 01 10 5A 87 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1A 3A FF 80 00 00
02 01 // Frame type and size
06 00 00 0B F3 // Active power
5B 05 7E // CRC and end tag
T FF FF DA SA SA C HC HC LD LS LQ AT AI AI AI AI AD
7E A0 78 01 02 01 10 C4 98 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 1B 00 FF 80 00 00
02 0D // Frame type and size
09 07 4B 46 4D 5F 30 30 31 // List version
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33 // Meter ID
09 07 4D 41 33 30 34 48 34 // Meter type
06 00 00 0C 21 // Active import
06 00 00 00 00 // Active export
06 00 00 00 00 // Reactive import
06 00 00 01 9F // Reactive export
06 00 00 0B F3 // I1
06 00 00 05 0B // I2
06 00 00 25 11 // I3
06 00 00 09 44 // U1
06 00 00 09 49 // U2
06 00 00 09 39 // U3
C9 95 7E // CRC and end tag
7E A0 9A 01 02 01 10 AA A5 E6 E7 00 0F 40 00 00 00 09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00
02 12 09 07 4B 46 4D 5F 30 30 31
09 10 XX XX XX XX XX XX XX XX XX XX 35 33 34 34 39 33
09 07 4D 41 33 30 34 48 34
06 00 00 09 99
06 00 00 00 00 06 00 00 00 00 06 00 00 01 67 06 00 00 03 BF 06 00 00 05 05
06 00 00 24 34 06 00 00 09 45 06 00 00 09 4F 06 00 00 09 3B
09 0C 07 E5 03 17 02 13 00 0A FF 80 00 00 06 01 34 3B 5D 06 00 00 00 00 06 00 00 09 36 06 00 3C 7A 98 DA 15 7E
7E A0 79 01 02 01 10 80 93 E6 E7 00 0F 40 00 00 00 09 0C 07 E1 09 0E 04 15 1F 14 FF 80 00 00 02 0D 09 07 4B 46 4D 5F 30 30 31 09 10 36 39 37 30 36 33 31 34 30 31 37 35 33 39 38 35 09 08 4D 41 33 30 34 48 33 45 06 00 00 04 0C 06 00 00 00 00 06 00 00 00 00 06 00 00 00 4E 06 00 00 07 C1 06 00 00 0C 9E 06 00 00 0D 7E 06 00 00 09 5F 06 00 00 00 00 06 00 00 09 66 87 96 7E

View 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
View 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
View 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

32
frames/decoding.txt Normal file
View File

@@ -0,0 +1,32 @@
T = Tag
FF = Frame format (4 bit type, 1 bit segmentation, 11 bit frame length)
DA = Destination Address (1-4 bytes, LSB=1 terminates)
SA = Source address (1-4 bytes, LSB=1 terminates)
C = Control (1 byte)
HC = HCS (2 bytes)
LD = LLC Destination
LS = LLC Remote
LQ = LLC Quality
AT = Tag (0x0F = unencrypted, 0xDB = encrypted) Not really documented that well...
AI = Invoke ID and priority (4 bytes)
AD = Date and time
AS = System title
CT = Cipher frame tag ? Undocumented
CL = Lenght of Cipher frame. Length of payload will be this number - 5 (control and counter in start) - 12 (GCM tag appended after payload)
CC = Security control
CO = Invocation counter
Security control bits
01234567
00110000 (0x30)
0 = read acces
1 = write access
2 = Authenticated req
3 = Encrypted req
4 = Digitally signed req
5 = Authenticated res
6 = Encrypted res
7 = Digitally signed res

View File

@@ -1,45 +1 @@
# Hardware options
There are currently two possible hardware options for this project, both in need of external power supply. A self powered board is under development by a member of the community and is currently being tested.
## Hardware v1 by [@roarfred](https://github.com/roarfred)
Composed from a ESP12E (or F) chip, this ESP8266 based board is designed specifically for this project with an on board M-bus chip.
Building this project will require some skills in ordering and assembling electronic circuits as well as programming. No detailed instructions are available.
![The HAN Reader Hardware](v1/img/HanReaderInEnclosure.PNG)
*The completed board mounted in a [3D printed enclosure](/Enclosure)*
## HAN reader 2.0 by [@dakarym](https://github.com/dakarym)
A board that does not require external power source. This have only been successfully tested on Aidon as far as I know. It draws too much power for Kamstrup, but it may work with Kaifa. The design is almost
completely built with SMD components, so advanced soldering skills are required to make this one.
[View his design here](https://github.com/dakarym/AmsToMqttBridge/tree/master/PCB)
## Assembly of readily available modules
You can also use a ESP based development board and combine this with a M-Bus module. Here are a few boards that have been tested, each one has a dedicated firmware file in the releases section.
### ESP8266 based boards
[Wemos D1 mini](https://docs.wemos.cc/en/latest/d1/d1_mini.html)
- M-Bus connected to GPIO5 (D1)
- Jump GPIO4 (D2) to GND to force AP mode during boot
- Dallas temp sensor connected to GPIO14 (D5)
### ESP32 based boards
[Wemos Lolin D32](https://docs.wemos.cc/en/latest/d32/d32.html)
- M-Bus connected to GPIO16
- Jump GPIO4 to GND to force AP mode during boot
- Dallas temp sensor connected to GPIO14
[Adafruit HUZZAH32](https://www.adafruit.com/product/3405)
- M-Bus connected to GPIO16
Combine one of above board with an M-Bus module. Connect 3.3v and GND together between the boards and connect the TX pin from the M-Bus board to the dedicated M-Bus pin on the ESP board.
[TSS721 M-BUS module board](https://www.aliexpress.com/item/TSS721/32751482255.html)
![FeatherMbus](img/feather_3010-00_mbus_slave.jpg)
[See Hardware page in Wiki](https://github.com/gskjold/AmsToMqttBridge/wiki)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -1,216 +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);
}
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("");
}

View File

@@ -1,45 +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();
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

View File

@@ -1,421 +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)) {
debugPrint(buffer, 0, bytesRead);
}
}
/*
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) {
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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -7,4 +7,4 @@ paragraph=The primary aim of the Timezone library is to convert Universal Coordi
category=Timing
url=https://github.com/JChristensen/Timezone
architectures=*
depends=Time
depends=Time (=1.6.0)

View File

@@ -1,27 +1,32 @@
[platformio]
extra_configs = platformio-user.ini
[common]
framework = arduino
lib_deps = file://lib/HanReader, file://lib/Timezone, MQTT@2.4.8, DallasTemperature@3.9.1, EspSoftwareSerial@6.9.0, RemoteDebug@3.0.5, Time@1.6
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 = ${common.framework}
framework = arduino
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
build_flags =
-D WEBSOCKET_DISABLED=1
[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 = ${common.framework}
framework = arduino
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
build_flags =
-D WEBSOCKET_DISABLED=1

View File

@@ -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() {
@@ -220,6 +226,7 @@ bool AmsConfiguration::getDebugConfig(DebugConfig& config) {
EEPROM.end();
return true;
} else {
clearDebug(config);
return false;
}
}
@@ -232,6 +239,12 @@ bool AmsConfiguration::setDebugConfig(DebugConfig& config) {
return ret;
}
void AmsConfiguration::clearDebug(DebugConfig& config) {
config.level = 5;
config.telnet = false;
config.serial = false;
}
bool AmsConfiguration::getDomoticzConfig(DomoticzConfig& config) {
if(hasConfig()) {
EEPROM.begin(EEPROM_SIZE);
@@ -375,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) {
@@ -496,11 +511,11 @@ void AmsConfiguration::clear() {
NtpConfig ntp;
clearNtp(ntp);
EEPROM.put(CONFIG_NTP_START, domo);
EEPROM.put(CONFIG_NTP_START, ntp);
EntsoeConfig entsoe;
clearEntsoe(entsoe);
EEPROM.put(CONFIG_ENTSOE_START, domo);
EEPROM.put(CONFIG_ENTSOE_START, entsoe);
EEPROM.put(EEPROM_CONFIG_ADDRESS, -1);
EEPROM.commit();
@@ -514,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)) {
@@ -534,12 +539,53 @@ bool AmsConfiguration::hasConfig() {
return false;
}
break;
case 86:
configVersion = -1; // Prevent loop
if(relocateConfig86()) {
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;
}
case EEPROM_CHECK_SUM:
return true;
default:
configVersion = 0;
return false;
}
return configVersion == EEPROM_CHECK_SUM;
}
int AmsConfiguration::getConfigVersion() {
@@ -552,7 +598,7 @@ void AmsConfiguration::loadTempSensors() {
int address = EEPROM_TEMP_CONFIG_ADDRESS;
int c = 0;
int storedCount = EEPROM.read(address++);
Serial.print("SEnsors: ");
Serial.print("Sensors: ");
Serial.println(storedCount);
if(storedCount > 0 && storedCount <= 32) {
for(int i = 0; i < storedCount; i++) {
@@ -584,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);
@@ -725,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);
@@ -737,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);
@@ -792,6 +740,116 @@ bool AmsConfiguration::loadConfig83(int address) {
return ret;
}
bool AmsConfiguration::relocateConfig86() {
MqttConfig86 mqtt86;
MqttConfig mqtt;
EEPROM.begin(EEPROM_SIZE);
EEPROM.get(CONFIG_MQTT_START_86, mqtt86);
strcpy(mqtt.host, mqtt86.host);
mqtt.port = mqtt86.port;
strcpy(mqtt.clientId, mqtt86.clientId);
strcpy(mqtt.publishTopic, mqtt86.publishTopic);
strcpy(mqtt.subscribeTopic, mqtt86.subscribeTopic);
strcpy(mqtt.username, mqtt86.username);
strcpy(mqtt.password, mqtt86.password);
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;
}
bool AmsConfiguration::save() {
EEPROM.begin(EEPROM_SIZE);
EEPROM.put(EEPROM_CONFIG_ADDRESS, EEPROM_CHECK_SUM);
@@ -948,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();
@@ -972,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();

View File

@@ -3,21 +3,28 @@
#include <EEPROM.h>
#include "Arduino.h"
#define EEPROM_SIZE 1024 * 3
#define EEPROM_CHECK_SUM 86 // 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_MQTT_START 224
#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 {
uint8_t boardType;
@@ -35,7 +42,7 @@ struct WiFiConfig {
bool mdns;
}; // 204
struct MqttConfig {
struct MqttConfig86 {
char host[128];
uint16_t port;
char clientId[32];
@@ -47,6 +54,18 @@ struct MqttConfig {
bool ssl;
}; // 420
struct MqttConfig {
char host[128];
uint16_t port;
char clientId[32];
char publishTopic[64];
char subscribeTopic[64];
char username[128];
char password[256];
uint8_t payloadFormat;
bool ssl;
}; // 676
struct WebConfig {
uint8_t security;
char username[64];
@@ -54,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;
@@ -84,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 {
@@ -102,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];
@@ -175,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];
@@ -271,6 +273,7 @@ public:
bool getDebugConfig(DebugConfig&);
bool setDebugConfig(DebugConfig&);
void clearDebug(DebugConfig&);
bool pinUsed(uint8_t, GpioConfig&);
@@ -318,8 +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);

View File

@@ -1,317 +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, substituteMissing);
break;
case METER_TYPE_KAMSTRUP:
extractFromKamstrup(hanReader, listSize, substituteMissing);
break;
case METER_TYPE_OMNIPOWER:
extractFromOmnipower(hanReader, listSize);
break;
}
}
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, bool substituteMissing) {
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;
if(substituteMissing) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
}
break;
}
}
}
void AmsData::extractFromKamstrup(HanReader& hanReader, uint8_t listSize, bool substituteMissing) {
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);
if(substituteMissing) {
l2current = (((activeImportPower - activeExportPower) * sqrt(3)) - (l1voltage * l1current) - (l3voltage * l3current)) / l2voltage;
}
break;
}
}
void AmsData::extractFromOmnipower(HanReader& hanReader, uint8_t listSize) {
switch(listSize) {
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();
@@ -321,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();
@@ -332,6 +59,7 @@ void AmsData::apply(AmsData& other) {
this->l2voltage = other.getL2Voltage();
this->l3voltage = other.getL3Voltage();
this->threePhase = other.isThreePhase();
this->twoPhase = other.isTwoPhase();
case 1:
this->activeImportPower = other.getActiveImportPower();
}
@@ -357,6 +85,10 @@ String AmsData::getMeterId() {
return this->meterId;
}
uint8_t AmsData::getMeterType() {
return this->meterType;
}
String AmsData::getMeterModel() {
return this->meterModel;
}
@@ -405,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;
}
@@ -424,3 +172,7 @@ float AmsData::getReactiveExportCounter() {
bool AmsData::isThreePhase() {
return this->threePhase;
}
bool AmsData::isTwoPhase() {
return this->twoPhase;
}

View File

@@ -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,28 +48,31 @@ public:
float getL2Current();
float getL3Current();
float getPowerFactor();
float getL1PowerFactor();
float getL2PowerFactor();
float getL3PowerFactor();
float getActiveImportCounter();
float getReactiveImportCounter();
float getActiveExportCounter();
float getReactiveExportCounter();
bool isThreePhase();
bool isTwoPhase();
private:
protected:
unsigned long lastUpdateMillis = 0;
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, counterEstimated = false;
void extractFromKaifa(HanReader& hanReader, uint8_t listSize);
void extractFromAidon(HanReader& hanReader, uint8_t listSize, bool substituteMissing);
void extractFromKamstrup(HanReader& hanReader, uint8_t listSize, bool substituteMissing);
void extractFromOmnipower(HanReader& hanReader, uint8_t listSize);
bool threePhase = false, twoPhase = false, counterEstimated = false;
};
#endif

315
src/AmsDataStorage.cpp Normal file
View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -10,45 +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"
#define ARDUINOJSON_POSITIVE_EXPONENTIATION_THRESHOLD 1e9
#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;
@@ -63,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);
@@ -141,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) {
@@ -175,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
@@ -189,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");
@@ -255,6 +276,7 @@ void setup() {
return;
}
}
LittleFS.end();
delay(1);
if(config.hasConfig()) {
@@ -270,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");
@@ -278,7 +303,7 @@ void setup() {
swapWifiMode();
}
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &mqtt);
ws.setup(&config, &gpioConfig, &meterConfig, &meterState, &ds);
}
int buttonTimer = 0;
@@ -289,7 +314,6 @@ bool longPressActive = false;
bool wifiConnected = false;
unsigned long lastTemperatureRead = 0;
unsigned long lastSuccessfulRead = 0;
unsigned long lastErrorBlink = 0;
int lastError = 0;
@@ -324,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);
@@ -335,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;
@@ -401,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);
}
}
@@ -431,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) {
@@ -443,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;
@@ -480,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();
@@ -529,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;
}
@@ -587,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, &timestamp);
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() {
@@ -698,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();
@@ -706,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;
@@ -722,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) {
@@ -731,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;
@@ -747,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;
@@ -772,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();

View File

@@ -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;
@@ -129,15 +255,17 @@ bool HwTools::updateTemperatures() {
delay(10);
}
} else {
sensorApi->requestTemperatures();
if(sensorCount > 0) {
sensorApi->requestTemperatures();
for(int x = 0; x < sensorCount; x++) {
TempSensorData *data = tempSensors[x];
float t = sensorApi->getTempC(data->address);
data->lastRead = t;
if(t > -85) {
data->changed = data->lastValidRead != t;
data->lastValidRead = t;
for(int x = 0; x < sensorCount; x++) {
TempSensorData *data = tempSensors[x];
float t = sensorApi->getTempC(data->address);
data->lastRead = t;
if(t > -85) {
data->changed = data->lastValidRead != t;
data->lastValidRead = t;
}
}
}
}
@@ -215,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) {

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -6,15 +6,15 @@ float DnbCurrParser::getValue() {
}
int DnbCurrParser::available() {
return 0;
}
int DnbCurrParser::read() {
return 0;
}
int DnbCurrParser::peek() {
return 0;
}
void DnbCurrParser::flush() {

View File

@@ -18,15 +18,15 @@ float EntsoeA44Parser::getPoint(uint8_t position) {
}
int EntsoeA44Parser::available() {
return 0;
}
int EntsoeA44Parser::read() {
return 0;
}
int EntsoeA44Parser::peek() {
return 0;
}
void EntsoeA44Parser::flush() {

View File

@@ -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");

View File

@@ -18,6 +18,7 @@ public:
bool loop();
char* getToken();
char* getCurrency();
float getValueForHour(uint8_t);
float getValueForHour(time_t, uint8_t);

View File

@@ -23,55 +23,47 @@ bool DomoticzMqttHandler::publish(AmsData* data, AmsData* previousState) {
return ret;
if (config.vl1idx > 0){
if (data->getL1Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL1Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
char val[16];
snprintf(val, 16, "%.2f", data->getL1Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.vl2idx > 0){
if (data->getL2Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL2Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl2idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
char val[16];
snprintf(val, 16, "%.2f", data->getL2Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl2idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.vl3idx > 0){
if (data->getL3Voltage() > 0.1){
char val[16];
snprintf(val, 16, "%.2f", data->getL3Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl3idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
char val[16];
snprintf(val, 16, "%.2f", data->getL3Voltage());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.vl3idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
if (config.cl1idx > 0){
if(data->getL1Current() > 0.0) {
char val[16];
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.cl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
char val[16];
snprintf(val, 16, "%.1f;%.1f;%.1f", data->getL1Current(), data->getL2Current(), data->getL3Current());
char json[192];
snprintf_P(json, sizeof(json), DOMOTICZ_JSON,
config.cl1idx,
val
);
ret |= mqtt->publish("domoticz/in", json);
}
return ret;
}

View File

@@ -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);

View File

@@ -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()) {

View File

@@ -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;
}
@@ -117,9 +136,11 @@ void AmsWebServer::loop() {
server.handleClient();
if(maxPwr == 0 && meterState->getListType() > 1 && meterConfig->mainFuse > 0 && meterConfig->distributionSystem > 0) {
int voltage = meterConfig->distributionSystem == 2 ? 400 : 230;
if(meterState->isThreePhase()) {
int voltage = meterConfig->distributionSystem == 2 ? 400 : 230;
maxPwr = meterConfig->mainFuse * sqrt(3) * voltage;
} else if(meterState->isTwoPhase()) {
maxPwr = meterConfig->mainFuse * voltage;
} else {
maxPwr = meterConfig->mainFuse * 230;
}
@@ -134,7 +155,15 @@ bool AmsWebServer::checkSecurity(byte level) {
String providedPwd = server.header("Authorization");
providedPwd.replace("Basic ", "");
#if defined(ESP8266)
String expectedBase64 = base64::encode(expectedAuth, false);
#elif defined(ESP32)
String expectedBase64 = base64::encode(expectedAuth);
#endif
debugger->printf("Expected auth: %s\n", expectedBase64.c_str());
debugger->printf("Provided auth: %s\n", providedPwd.c_str());
access = providedPwd.equals(expectedBase64);
}
@@ -181,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);
}
}
@@ -325,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");
@@ -352,7 +383,9 @@ void AmsWebServer::indexHtml() {
html.replace("{temp}", temp > 0 ? String(temp, 1) : "");
int rssi = hw->getWifiRssi();
html.replace("{rssi}", vcc > 0 ? String(rssi) : "");
html.replace("{rssi}", String(rssi));
html.replace("{mem}", String(ESP.getFreeHeap()/1000, 1));
html.replace("{cs}", String((uint32_t)(millis64()/1000), 10));
@@ -381,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" : "");
@@ -395,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);
@@ -408,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);
@@ -480,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");
@@ -558,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);
@@ -606,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...");
@@ -655,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;
@@ -677,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[280];
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,
@@ -695,7 +757,7 @@ void AmsWebServer::dataJson() {
meterState->getReactiveImportPower(),
meterState->getReactiveExportPower(),
meterState->getActiveImportCounter(),
meterState->getReactiveExportCounter(),
meterState->getActiveExportCounter(),
meterState->getReactiveImportCounter(),
meterState->getReactiveExportCounter(),
meterState->getL1Voltage(),
@@ -704,6 +766,10 @@ void AmsWebServer::dataJson() {
meterState->getL1Current(),
meterState->getL2Current(),
meterState->getL3Current(),
meterState->getPowerFactor(),
meterState->getL1PowerFactor(),
meterState->getL2PowerFactor(),
meterState->getL3PowerFactor(),
vcc,
rssi,
hw->getTemperature(),
@@ -713,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");
@@ -733,10 +949,10 @@ void AmsWebServer::handleSetup() {
} else {
SystemConfig sys { server.arg("board").toInt() };
config->clear();
DebugConfig debugConfig;
config->getDebugConfig(debugConfig);
config->clearGpio(*gpioConfig);
config->clearMeter(*meterConfig);
config->clear();
switch(sys.boardType) {
case 0: // roarfred
@@ -751,7 +967,6 @@ void AmsWebServer::handleSetup() {
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
gpioConfig->tempSensorPin = 5;
gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14;
gpioConfig->ledRgbInverted = true;
@@ -764,12 +979,29 @@ void AmsWebServer::handleSetup() {
gpioConfig->tempSensorPin = 5;
gpioConfig->vccBootLimit = 33;
break;
case 3: // Pow UART0
gpioConfig->hanPin = 3;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14;
gpioConfig->ledRgbInverted = true;
break;
case 4: // Pow GPIO12
gpioConfig->hanPin = 12;
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
gpioConfig->ledPinRed = 13;
gpioConfig->ledPinGreen = 14;
gpioConfig->ledRgbInverted = true;
break;
case 101: // D1
gpioConfig->hanPin = 5;
gpioConfig->apPin = 4;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = true;
gpioConfig->tempSensorPin = 14;
gpioConfig->vccMultiplier = 1100;
break;
case 100: // ESP8266
@@ -782,13 +1014,11 @@ void AmsWebServer::handleSetup() {
gpioConfig->apPin = 4;
gpioConfig->ledPin = 5;
gpioConfig->ledInverted = true;
gpioConfig->tempSensorPin = 14;
break;
case 202: // Feather
gpioConfig->hanPin = 16;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = false;
gpioConfig->tempSensorPin = 14;
break;
case 203: // DevKitC
gpioConfig->hanPin = 16;
@@ -800,7 +1030,6 @@ void AmsWebServer::handleSetup() {
gpioConfig->apPin = 0;
gpioConfig->ledPin = 2;
gpioConfig->ledInverted = false;
gpioConfig->tempSensorPin = 14;
break;
}
@@ -848,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;
@@ -861,13 +1086,15 @@ void AmsWebServer::handleSetup() {
success = false;
}
config->setDebugConfig(debugConfig);
if(success && config->save()) {
performRestart = true;
server.sendHeader("Location","/restart-wait");
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);
}
}
@@ -882,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();
@@ -898,7 +1127,6 @@ void AmsWebServer::handleSave() {
authenticationKeyHex.replace("0x", "");
fromHex(meterConfig->authenticationKey, authenticationKeyHex, 16);
}
meterConfig->substituteMissing = server.hasArg("s") && server.arg("substituteMissing") == "true";
config->setMeterConfig(*meterConfig);
}
@@ -989,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);
}
@@ -1040,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()) {
@@ -1055,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);
}
}
@@ -1124,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");
@@ -1163,26 +1396,31 @@ void AmsWebServer::configDebugHtml() {
String AmsWebServer::getSerialSelectOptions(int selected) {
String gpioOptions;
if(selected == 3) {
gpioOptions += "<option value=\"3\" selected>UART0</option>";
gpioOptions += "<option value=\"3\" selected>UART0 (GPIO3)</option>";
} else {
gpioOptions += "<option value=\"3\">UART0</option>";
gpioOptions += "<option value=\"3\">UART0 (GPIO3)</option>";
}
#if defined(ESP32)
int numGpio = 24;
int gpios[] = {4,5,6,7,8,10,11,12,13,14,15,17,18,19,21,22,23,25,32,33,34,35,36,39};
if(selected == 9) {
gpioOptions += "<option value=\"9\" selected>UART1</option>";
gpioOptions += "<option value=\"9\" selected>UART1 (GPIO9)</option>";
} else {
gpioOptions += "<option value=\"9\">UART1</option>";
gpioOptions += "<option value=\"9\">UART1 (GPIO9)</option>";
}
if(selected == 16) {
gpioOptions += "<option value=\"16\" selected>UART2</option>";
gpioOptions += "<option value=\"16\" selected>UART2 (GPIO16)</option>";
} else {
gpioOptions += "<option value=\"16\">UART2</option>";
gpioOptions += "<option value=\"16\">UART2 (GPIO16)</option>";
}
#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++) {
@@ -1207,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");
}
@@ -1239,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();
}
}
@@ -1251,7 +1533,21 @@ void AmsWebServer::firmwareHtml() {
if(!checkSecurity(1))
return;
uploadHtml("CA file", "/firmware", "mqtt");
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() {
@@ -1287,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);
}
@@ -1337,6 +1632,7 @@ void AmsWebServer::firmwareDownload() {
server.send(303);
}
https.end();
client.stop();
} else {
printI("No firmware version specified...");
server.sendHeader("Location","/");
@@ -1398,7 +1694,9 @@ void AmsWebServer::restartWaitHtml() {
yield();
if(performRestart) {
SPIFFS.end();
if(ds != NULL) {
ds->save();
}
printI("Rebooting");
delay(1000);
#if defined(ESP8266)
@@ -1441,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);
@@ -1495,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);
@@ -1548,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);
@@ -1613,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");

View File

@@ -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();

View File

@@ -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() {
@@ -147,14 +247,16 @@ $(function() {
case '/gpio':
case '/debugging':
case '/firmware':
$('#firmware-warn').show();
case '/reset':
$('#system-link').addClass('active');
break;
}
// 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]);
@@ -163,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');
@@ -219,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');
@@ -238,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({
@@ -249,8 +525,7 @@ var fetch = function() {
$(".SimpleMeter").hide();
im.show();
em.show();
vm.show();
am.show();
}
for(var id in json) {
@@ -271,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";
@@ -285,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";
@@ -300,38 +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 && json.u1) {
var v = parseFloat(json.u1);
if(json.u2) {
v = (v+parseFloat(json.u2)+parseFloat(json.u3)) / 3;
if(vp) {
var c = 0;
var t = 0;
var r = 1;
var arr = [['Phase', 'Voltage', { role: 'style' }, { role: 'annotation' }]];
if(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) {
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) {
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;
if(v > 0) {
va = google.visualization.arrayToDataTable(arr);
vp.draw(va, vo);
}
var pct = (Math.max(v-207, 1)*100/46);
vm.gaugeMeter({
percent: pct,
text: v.toFixed(1)
});
}
if(am && am.gaugeMeter && json.i1 && json.mf) {
var v = parseFloat(json.i1);
if(ap && json.mf) {
var a = 0;
var r = 1;
var arr = [['Phase', 'Amperage', { role: 'style' }, { role: 'annotation' }]];
if(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) {
v = Math.max(v, 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) {
v = Math.max(v, 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 = (v*100)/parseInt(json.mf);
am.gaugeMeter({
percent: pct,
text: v.toFixed(1)
});
}
if(json.me) {
@@ -345,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);
@@ -361,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;
}
}

View File

@@ -6,24 +6,29 @@
"e" : %d,
"ri" : %d,
"re" : %d,
"ic" : %.1f,
"ec" : %.1f,
"ric" : %.1f,
"rec" : %.1f,
"u1" : %.1f,
"u2" : %.1f,
"u3" : %.1f,
"ic" : %.2f,
"ec" : %.2f,
"ric" : %.2f,
"rec" : %.2f,
"u1" : %.2f,
"u2" : %.2f,
"u3" : %.2f,
"i1" : %.2f,
"i2" : %.2f,
"i3" : %.2f,
"v" : %.2f,
"f" : %.2f,
"f1" : %.2f,
"f2" : %.2f,
"f3" : %.2f,
"v" : %.3f,
"r" : %d,
"t" : %.1f,
"t" : %.2f,
"u" : %lu,
"m" : %lu,
"em" : %d,
"hm" : %d,
"wm" : %d,
"mm" : %d,
"me" : %d
"me" : %d,
"p" : %s
}

26
web/dayplot.json Normal file
View 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
View 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
}

View File

@@ -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
View 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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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&ohm;</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&ohm;</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">

View File

@@ -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
View 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>

View File

@@ -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>&deg;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>

View File

@@ -20,10 +20,10 @@
"U1" : %.2f,
"U2" : %.2f,
"U3" : %.2f,
"tPI" : %.1f,
"tPO" : %.1f,
"tQI" : %.1f,
"tQO" : %.1f,
"tPI" : %.2f,
"tPO" : %.2f,
"tQI" : %.2f,
"tQO" : %.2f,
"rtc" : %lu
}
}

View File

@@ -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</option>
<option value="4" {m4}>Kamstrup (Omnipower)</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
View 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
}

View File

@@ -9,7 +9,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Host</span>
</div>
<input type="text" class="form-control mc" name="h" value="{h}" maxlength="128"/>
<input type="text" class="form-control mc" name="h" value="{h}" maxlength="127"/>
</div>
</div>
<div class="col-xl-2 col-lg-3 col-md-3 col-sm-4">
@@ -25,7 +25,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Client ID</span>
</div>
<input type="text" class="form-control mc" name="i" value="{i}" maxlength="32"/>
<input type="text" class="form-control mc" name="i" value="{i}" maxlength="31"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6">
@@ -33,7 +33,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Publish topic</span>
</div>
<input type="text" class="form-control mc" name="t" value="{t}" maxlength="64"/>
<input type="text" class="form-control mc" name="t" value="{t}" maxlength="63"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-6">
@@ -41,7 +41,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Username</span>
</div>
<input type="text" class="form-control mc" name="u" value="{u}" maxlength="64"/>
<input type="text" class="form-control mc" name="u" value="{u}" maxlength="127"/>
</div>
</div>
<div class="col-xl-3 col-lg-4 col-md-6 col-sm-6">
@@ -49,7 +49,7 @@
<div class="input-group-prepend">
<span class="input-group-text">Password</span>
</div>
<input type="password" class="form-control mc" name="pw" value="{pw}" maxlength="64"/>
<input type="password" class="form-control mc" name="pw" value="{pw}" maxlength="255"/>
</div>
</div>
<div class="col-lg-3 col-md-4 col-sm-6">

View File

@@ -25,9 +25,11 @@
<h5>Hardware</h5>
<select name="board" class="form-control">
<optgroup label="Custom hardware">
<option value="0" ${config.boardType0}>Custom hardware by Roar Fredriksen</option>
<option value="1" ${config.boardType1}>Kamstrup module by Egil Opsahl</option>
<option value="4" ${config.boardType2}>Pow-U or Pow-K from amsleser.no (GPIO12)</option>
<option value="3" ${config.boardType2}>Pow-U or Pow-K from amsleser.no (UART0)</option>
<option value="2" ${config.boardType2}>HAN Reader 2.0 by Max Spencer</option>
<option value="1" ${config.boardType1}>Kamstrup module by Egil Opsahl</option>
<option value="0" ${config.boardType0}>Custom hardware by Roar Fredriksen</option>
</optgroup>
<optgroup label="ESP8266">
<option value="101" ${config.boardType101}>Wemos D1</option>
@@ -41,16 +43,6 @@
</optgroup>
</select>
</div>
<div class="col-xl-2 col-md-3">
<h5>Meter</h5>
<select name="meterType" class="form-control">
<option value="0" ${config.meterType0}>Autodetect</option>
<option value="1" ${config.meterType1}>Kaifa</option>
<option value="2" ${config.meterType2}>Aidon</option>
<option value="3" ${config.meterType3}>Kamstrup</option>
<option value="4" ${config.meterType4}>Kamstrup (Omnipower)</option>
</select>
</div>
</div>
</div>
<div class="my-3 p-3 bg-white rounded shadow">

View File

@@ -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">
@@ -13,6 +13,12 @@
</div>
</div>
</div>
<div id="firmware-warn" style="display: none;">
<br/>
<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>
</div>
<hr/>
<div class="row form-group">

BIN
webui.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

BIN
webui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB