Compare commits

..

43 Commits

Author SHA1 Message Date
Gunnar Skjold
4b32af69a7 Fixed typo 2020-02-18 07:37:42 +01:00
Gunnar Skjold
fc42fb22d8 Merge pull request #33 from gskjold/dev-v1.0.1
Dev v1.0.1
2020-02-16 14:49:36 +01:00
Gunnar Skjold
4e24e2949a Updated featheresp32 config to use Serial2 for Mbus 2020-02-15 18:01:10 +01:00
Gunnar Skjold
b2174dd521 If debug mode and HW_ROARFRED, use software serial to set upp RX for HAN with 2400 and TX for debugging with 115200 2020-02-14 20:14:30 +01:00
Gunnar Skjold
fe79a96827 Added change to allow auto modem sleep 2020-02-14 19:52:15 +01:00
Gunnar Skjold
554cc7c023 Added date and time from meter as rtc in json 2020-02-14 16:03:52 +01:00
Gunnar Skjold
d2f93c07bd Not sure if this does anything, but increased json and mqtt limits to see if Kamstrup list 3 is received 2020-02-13 20:50:13 +01:00
Gunnar Skjold
b6efae656f Expose export power in JSON 2020-02-12 16:40:51 +01:00
Gunnar Skjold
fa0f93cefe Switched to reactive import for Q 2020-02-12 13:25:36 +01:00
Gunnar Skjold
058dc658c1 Switched to reactive import for Q 2020-02-12 13:16:49 +01:00
Gunnar Skjold
4b9cb27e58 Disable web security if AP mode is triggered 2020-02-11 17:32:18 +01:00
Gunnar Skjold
5f14517258 Minor changes after testing MQTT reconnect 2020-02-11 17:25:52 +01:00
Gunnar Skjold
ad820fff01 Allow other services to get resources while trying to connect to mqtt 2020-02-11 11:04:37 +01:00
Gunnar Skjold
99a23b3bf7 Cropped webui image 2020-02-09 13:19:48 +01:00
Gunnar Skjold
15e861ce70 Added AP mode pin for both Wemos D1 and D32 2020-02-09 12:18:08 +01:00
Gunnar Skjold
fdfadb9bf9 Updated Aidon parser with support for 3 phase IT/TT system 2020-02-08 18:23:56 +01:00
Gunnar Skjold
388d35c390 Updated documentation 2020-02-07 23:12:27 +01:00
Gunnar Skjold
e466a45b5c Fixed missing check in checkbox when configuration opened in firefox 2020-02-07 22:33:55 +01:00
Gunnar Skjold
2e7b9e8e43 Fixed missing check in checkbox when configuration opened in firefox 2020-02-07 22:26:17 +01:00
Gunnar Skjold
deb2148e9a Software serial for development boards. Added a few extra boards 2020-02-07 17:27:58 +01:00
Gunnar Skjold
3a7f6078f4 Use builtin LED for Lolin D32 2020-02-06 19:02:05 +01:00
Gunnar Skjold
dbc551a41d Moved configuration object to main file and load the config before start. Fixed issue where partiy is switched for Kamstrup half way when in debug mode 2020-02-05 20:43:37 +01:00
Gunnar Skjold
9bb596aab1 Reverted what was intended as support for earlier configuration versions. 2020-02-02 08:15:49 +01:00
Gunnar Skjold
4a3d22e75c Fixed script to generate webroot header files 2020-02-01 09:57:43 +01:00
Gunnar Skjold
d68ffc827d Trigger build on changes in scripts and web 2020-02-01 09:56:14 +01:00
Gunnar Skjold
20c62a63cf Fixed script to generate webroot header files 2020-02-01 09:54:58 +01:00
Gunnar Skjold
8f85b43fc3 Made MQTT optional 2020-02-01 09:52:37 +01:00
Gunnar Skjold
57d8603790 Added option for electric distribution system (230/400) 2020-01-31 21:30:20 +01:00
Gunnar Skjold
fc1f1554d8 Extracted webroot into usable files 2020-01-31 21:08:27 +01:00
Gunnar Skjold
ced6f125fd Trigger build on changing ini file 2020-01-29 20:58:09 +01:00
Gunnar Skjold
f76cacf835 Trigger build on changing ini file 2020-01-29 20:56:31 +01:00
Gunnar Skjold
a8205edae9 Fixed build after merge 2020-01-29 20:53:52 +01:00
Gunnar Skjold
e9ed6d825a Merge pull request #14 from gskjold/issue-9
New web interface that is always available
2020-01-29 20:50:26 +01:00
Gunnar Skjold
378dbbcc99 Only show phases with power, and always deliver volts and amps to frontend 2020-01-29 20:38:53 +01:00
Gunnar Skjold
36b37eb2a6 Calculate max power when receiving json from reader 2020-01-28 21:18:48 +01:00
Gunnar Skjold
7e4047096b Stripped away bootstrap css and created a smaller boot.css as replacement. Load real bootstrap from CDN. Added HTML only (non-js) view of data 2020-01-28 21:10:54 +01:00
Gunnar Skjold
7fdedd8bcf Forgot some changes 2020-01-28 19:20:50 +01:00
Gunnar Skjold
f693869bd4 Merge branch 'master' into issue-9 2020-01-28 19:18:13 +01:00
Gunnar Skjold
32d58fdd71 Stripped jquery and full bootstrap from image 2020-01-23 19:45:29 +01:00
Gunnar Skjold
0fa3e3585a Finalized live update of meter data in web gui 2020-01-19 21:37:56 +01:00
Gunnar Skjold
a88291c0f0 Implemented new user interface and prepared for live view 2020-01-17 21:49:36 +01:00
Gunnar Skjold
ac1609bfac Merge branch 'issue-6' into issue-9 2020-01-17 19:17:44 +01:00
Gunnar Skjold
22d5839d60 Added HTML, CSS and JS for new user interface 2020-01-17 14:41:02 +01:00
31 changed files with 2197 additions and 680 deletions

View File

@@ -5,6 +5,9 @@ on:
paths:
- src/**
- lib/**
- scripts/**
- web/**
- platformio.ini
branches:
- master
tags:
@@ -37,6 +40,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -U platformio
- name: Configure build targets
run: echo "[platformio]\ndefault_envs = hw1esp12e, esp12e, esp32" > platformio-user.ini
- name: PlatformIO lib install
run: pio lib install
- name: PlatformIO run

View File

@@ -51,6 +51,15 @@ jobs:
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload hw1esp12e binary to release
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/hw1esp12e/firmware.bin
asset_name: ams2mqtt-hw1esp12e-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp12e binary to release
uses: actions/upload-release-asset@v1.0.1
env:
@@ -60,14 +69,32 @@ jobs:
asset_path: .pio/build/esp12e/firmware.bin
asset_name: ams2mqtt-esp12e-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload hw1esp12e binary to release
- name: Upload d1mini binary to release
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/hw1esp12e/firmware.bin
asset_name: ams2mqtt-hw1esp12e-${{ steps.release_tag.outputs.tag }}.bin
asset_path: .pio/build/d1mini/firmware.bin
asset_name: ams2mqtt-d1mini-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload esp32 binary to release
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/esp32/firmware.bin
asset_name: ams2mqtt-esp32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload lolind32 binary to release
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: .pio/build/lolind32/firmware.bin
asset_name: ams2mqtt-lolind32-${{ steps.release_tag.outputs.tag }}.bin
asset_content_type: application/octet-stream
- name: Upload featheresp32 binary to release
uses: actions/upload-release-asset@v1.0.1

4
.gitignore vendored
View File

@@ -7,4 +7,6 @@
.vscode
.pio
platformio-user.ini
src/version.h
/src/version.h
/src/web/root
/src/AmsToMqttBridge.ino.cpp

View File

@@ -1,14 +1,21 @@
# 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)
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 modules. It reads data from the HAN port of the meter and sends this to a configured MQTT bus.
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.
There is also a web interface available on runtime, showing meter data in real time.
<img src="webui.jpg" width="480">
## Release binaries
In the [Release section](https://github.com/gskjold/AmsToMqttBridge/releases) of this repository, you will find precompiled binaries for some common boards.
- _esp12e_ :: General ESP8266 board with 12E or 12F chip, ex NodeMCU board.
- _hw1esp12e_ :: First version hardware with ESP 12E of 12F chip.
- _hw1esp12e_ :: First version custom hardware with ESP 12E of 12F chip
- _esp12e_ :: General ESP8266 board with 12E or 12F chip
- _d1mini_ :: Wemos D1 mini
- _esp32_ :: General ESP32 board
- _lolind32_ :: Wemos D32
- _featheresp32_ :: Adafruit ESP32 feather
### Flashing binaries with [esptool.py](https://github.com/espressif/esptool)
@@ -18,3 +25,15 @@ Linux:
Windows:
```esptool.py --port COM1 write_flash 0x0 binary-file.bin```
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.
It is recommended to use Visual Studio Code with the PlatformIO plugin for development.
[Visual Studio Code](https://code.visualstudio.com/download)
[PlatformIO vscode plugin](https://platformio.org/install/ide?install=vscode)
Copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile. If you are using the original board design by [@roarfred](https://github.com/roarfred) use build flag -D HW_ROARFRED=1

View File

@@ -12,9 +12,25 @@ Building this project will require some skills in ordering and assembling electr
*The completed board mounted in a [3D printed enclosure](/Enclosure)*
## Assembly of readily available modules
Use a ESP based developmentboard and a M-bus module.
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.
[Adafruit Feather M0 WiFi w/ATWINC1500](https://www.adafruit.com/product/3010)
### 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
### ESP32 based boards
[Wemos D32](https://docs.wemos.cc/en/latest/d32/d32.html)
- M-Bus connected to GPIO21
- Jump GPIO4 to GND to force AP mode during boot
[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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,26 +1,18 @@
#include "HanConfigAp.h"
#include "config_html.h"
#include "style_css.h"
#include "Base64.h"
#if defined(ESP8266)
ESP8266WebServer HanConfigAp::server(80);
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
WebServer HanConfigAp::server(80);
#endif
Stream* HanConfigAp::debugger;
bool HanConfigAp::hasConfig() {
return config.hasConfig();
return config->hasConfig();
}
void HanConfigAp::setup(int accessPointButtonPin, Stream* debugger)
void HanConfigAp::setup(int accessPointButtonPin, configuration* config, Stream* debugger)
{
this->debugger = debugger;
this->config = config;
// Test if we're missing configuration
if (!config.hasConfig())
if (!config->hasConfig())
{
print("No config. We're booting as AP. Look for SSID ");
println(this->AP_SSID);
@@ -29,8 +21,7 @@ void HanConfigAp::setup(int accessPointButtonPin, Stream* debugger)
else
{
// Load the configuration
config.load();
if (this->debugger) config.print(this->debugger);
if (this->debugger) config->print(this->debugger);
if (accessPointButtonPin != INVALID_BUTTON_PIN)
{
@@ -73,213 +64,15 @@ void HanConfigAp::setup(int accessPointButtonPin, Stream* debugger)
}
}
void HanConfigAp::enableWeb() {
server.on("/", handleRoot);
server.on("/style.css", handleStyle);
server.on("/save", handleSave);
server.begin(); // Web server start
print("Web server is ready for config at http://");
if(isActivated) {
print(WiFi.softAPIP());
} else {
print(WiFi.localIP());
}
println("/");
}
bool HanConfigAp::loop() {
if(isActivated) {
//DNS
dnsServer.processNextRequest();
}
//HTTP
server.handleClient();
return isActivated;
}
/** Handle root or redirect to captive portal */
void HanConfigAp::handleRoot() {
println("Serving / over http...");
configuration *config = new configuration();
config->load();
String html = String((const __FlashStringHelper*) CONFIG_HTML);
if(config->hasConfig()) {
bool access = !config->isAuth();
if(config->isAuth() && server.hasHeader("Authorization")) {
String expectedAuth = String(config->authUser) + ":" + String(config->authPass);
String providedPwd = server.header("Authorization");
providedPwd.replace("Basic ", "");
char inputString[providedPwd.length()];
providedPwd.toCharArray(inputString, providedPwd.length()+1);
int inputStringLength = sizeof(inputString);
int decodedLength = Base64.decodedLength(inputString, inputStringLength);
char decodedString[decodedLength];
Base64.decode(decodedString, inputString, inputStringLength);
print("Received auth: ");
println(decodedString);
access = String(decodedString).equals(expectedAuth);
}
if(!access) {
server.sendHeader("WWW-Authenticate", "Basic realm=\"Secure Area\"");
server.send(401, "text/html", "");
} else {
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
html.replace("${config.ssid}", config->ssid);
html.replace("${config.ssidPassword}", config->ssidPassword);
switch (config->meterType) {
case 1:
html.replace("${config.meterType0}", "");
html.replace("${config.meterType1}", "selected");
html.replace("${config.meterType2}", "");
html.replace("${config.meterType3}", "");
break;
case 2:
html.replace("${config.meterType0}", "");
html.replace("${config.meterType1}", "");
html.replace("${config.meterType2}", "selected");
html.replace("${config.meterType3}", "");
break;
case 3:
html.replace("${config.meterType0}", "");
html.replace("${config.meterType1}", "");
html.replace("${config.meterType2}", "");
html.replace("${config.meterType3}", "selected");
break;
default:
html.replace("${config.meterType0}", "selected");
html.replace("${config.meterType1}", "");
html.replace("${config.meterType2}", "");
html.replace("${config.meterType3}", "");
}
html.replace("${config.mqtt}", config->mqtt);
html.replace("${config.mqttPort}", String(config->mqttPort));
html.replace("${config.mqttClientID}", config->mqttClientID);
html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic);
html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic);
html.replace("${config.mqttUser}", config->mqttUser);
html.replace("${config.mqttPass}", config->mqttPass);
html.replace("${config.authUser}", config->authUser);
html.replace("${config.authPass}", config->authPass);
}
} else {
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
html.replace("${config.ssid}", "");
html.replace("${config.ssidPassword}", "");
html.replace("${config.meterType0}", "selected");
html.replace("${config.meterType1}", "");
html.replace("${config.meterType2}", "");
html.replace("${config.meterType3}", "");
html.replace("${config.mqtt}", "");
html.replace("${config.mqttPort}", "1883");
html.replace("${config.mqttClientID}", "");
html.replace("${config.mqttPublishTopic}", "");
html.replace("${config.mqttSubscribeTopic}", "");
html.replace("${config.mqttUser}", "");
html.replace("${config.mqttPass}", "");
html.replace("${config.authUser}", "");
html.replace("${config.authPass}", "");
}
server.send(200, "text/html", html);
}
void HanConfigAp::handleStyle() {
println("Serving /style.css over http...");
String css = String((const __FlashStringHelper*) STYLE_CSS);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "text/css", css);
}
void HanConfigAp::handleSave() {
configuration *config = new configuration();
String temp;
temp = server.arg("ssid");
config->ssid = new char[temp.length() + 1];
temp.toCharArray(config->ssid, temp.length() + 1, 0);
temp = server.arg("ssidPassword");
config->ssidPassword = new char[temp.length() + 1];
temp.toCharArray(config->ssidPassword, temp.length() + 1, 0);
config->meterType = (byte)server.arg("meterType").toInt();
temp = server.arg("mqtt");
config->mqtt = new char[temp.length() + 1];
temp.toCharArray(config->mqtt, temp.length() + 1, 0);
config->mqttPort = (int)server.arg("mqttPort").toInt();
temp = server.arg("mqttClientID");
config->mqttClientID = new char[temp.length() + 1];
temp.toCharArray(config->mqttClientID, temp.length() + 1, 0);
temp = server.arg("mqttPublishTopic");
config->mqttPublishTopic = new char[temp.length() + 1];
temp.toCharArray(config->mqttPublishTopic, temp.length() + 1, 0);
temp = server.arg("mqttSubscribeTopic");
config->mqttSubscribeTopic = new char[temp.length() + 1];
temp.toCharArray(config->mqttSubscribeTopic, temp.length() + 1, 0);
temp = server.arg("mqttUser");
config->mqttUser = new char[temp.length() + 1];
temp.toCharArray(config->mqttUser, temp.length() + 1, 0);
temp = server.arg("mqttPass");
config->mqttPass = new char[temp.length() + 1];
temp.toCharArray(config->mqttPass, temp.length() + 1, 0);
temp = server.arg("authUser");
config->authUser = new char[temp.length() + 1];
temp.toCharArray(config->authUser, temp.length() + 1, 0);
temp = server.arg("authPass");
config->authPass = new char[temp.length() + 1];
temp.toCharArray(config->authPass, temp.length() + 1, 0);
println("Saving configuration now...");
if (HanConfigAp::debugger) config->print(HanConfigAp::debugger);
if (config->save())
{
println("Successfully saved. Will reboot now.");
String html = "<html><body><h1>Successfully Saved!</h1><h3>Device is restarting now...</h3></form>";
server.send(200, "text/html", html);
#if defined(ESP8266)
ESP.reset();
#elif defined(ESP32)
ESP.restart();
#endif
}
else
{
println("Error saving configuration");
String html = "<html><body><h1>Error saving configuration!</h1></form>";
server.send(500, "text/html", html);
}
}
size_t HanConfigAp::print(const char* text)
{
if (debugger) debugger->print(text);

View File

@@ -11,10 +11,8 @@
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <WebServer.h>
#else
#warning "Unsupported board type"
#endif
@@ -26,16 +24,16 @@
class HanConfigAp {
public:
void setup(int accessPointButtonPin, Stream* debugger);
void enableWeb();
void setup(int accessPointButtonPin, configuration* config, Stream* debugger);
bool loop();
bool hasConfig();
configuration config;
bool isActivated = false;
private:
const char* AP_SSID = "AMS2MQTT";
configuration* config;
// DNS server
const byte DNS_PORT = 53;
DNSServer dnsServer;
@@ -45,16 +43,6 @@ private:
static size_t print(const Printable& data);
static size_t println(const Printable& data);
// Web server
static void handleRoot();
static void handleStyle();
static void handleSave();
#if defined(ESP8266)
static ESP8266WebServer server;
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
static WebServer server;
#endif
static Stream* debugger;
};

View File

@@ -1,85 +0,0 @@
const char CONFIG_HTML[] PROGMEM = R"=="==(
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="/style.css">
<title>AMS2MQTT - configuration</title>
</head>
<body>
<form method='post' action='/save'>
<div class="wrapper">
<div class="inner-wrapper">
<div>
<h2>WiFi</h2>
</div>
<div>
<input type='text' name='ssid' value="${config.ssid}" placeholder="SSID">
</div>
<div>
<input type='password' name='ssidPassword' value="${config.ssidPassword}" placeholder="Password">
</div>
</div>
<div class="inner-wrapper">
<div>
<h2>Meter Type</h2>
</div>
<div class="select-style">
<select name="meterType">
<option value="0" ${config.meterType0} disabled class="disabled-option"> SELECT TYPE </option>
<option value="1" ${config.meterType1}>Kaifa</option>
<option value="2" ${config.meterType2}>Aidon</option>
<option value="3" ${config.meterType3}>Kamstrup</option>
</select>
</div>
</div>
<div class="inner-wrapper">
<div>
<h2>MQTT</h2>
</div>
<div>
<label>Server & port:</label>
<input type='text' name='mqtt' value="${config.mqtt}" placeholder="server">
<input type='number' name='mqttPort' value="${config.mqttPort}" placeholder="port">
</div>
<div>
<label>Client ID:</label>
<input type='text' name='mqttClientID' value="${config.mqttClientID}" placeholder="client id">
</div>
<div>
<label>Publish topic: </label>
<input type='text' name='mqttPublishTopic' value="${config.mqttPublishTopic}" placeholder="topic">
</div>
<div>
<label>Username:</label>
<input type='text' name='mqttUser' value="${config.mqttUser}" placeholder="Blank for insecure">
</div>
<div>
<label>Password:</label>
<input type='password' name='mqttPass' value="${config.mqttPass}" placeholder="Blank for insecure">
</div>
<div>
<input class="submit-button" type='submit' value='save'>
</div>
</div>
<div class="inner-wrapper">
<div>
<h2>Webserver</h2>
</div>
<div>
<label>Username:</label>
<input type='text' name='authUser' value="${config.authUser}" placeholder="Blank for insecure">
</div>
<div>
<label>Password:</label>
<input type='password' name='authPass' value="${config.authPass}" placeholder="Blank for insecure">
</div>
</div>
</div>
</form>
<body>
</html>
)=="==";

View File

@@ -24,11 +24,18 @@ bool configuration::save()
address += saveString(address, ssid);
address += saveString(address, ssidPassword);
address += saveByte(address, meterType);
address += saveString(address, mqtt);
address += saveInt(address, mqttPort);
address += saveString(address, mqttClientID);
address += saveString(address, mqttPublishTopic);
address += saveString(address, mqttSubscribeTopic);
if(mqttHost) {
address += saveBool(address, true);
address += saveString(address, mqttHost);
address += saveInt(address, mqttPort);
address += saveString(address, mqttClientID);
address += saveString(address, mqttPublishTopic);
address += saveString(address, mqttSubscribeTopic);
} else {
address += saveBool(address, false);
}
if (isSecure()) {
address += saveBool(address, true);
@@ -39,12 +46,15 @@ bool configuration::save()
address += saveBool(address, false);
address += saveBool(address, isAuth());
if (isAuth()) {
address += saveByte(address, authSecurity);
if (authSecurity > 0) {
address += saveString(address, authUser);
address += saveString(address, authPass);
}
address += saveInt(address, fuseSize);
address += saveInt(address, distSys);
bool success = EEPROM.commit();
EEPROM.end();
@@ -57,37 +67,44 @@ bool configuration::load()
int address = EEPROM_CONFIG_ADDRESS;
bool success = false;
ssid = (char*)String("").c_str();
ssidPassword = (char*)String("").c_str();
ssid = 0;
ssidPassword = 0;
meterType = (byte)0;
mqtt = (char*)String("").c_str();
mqttClientID = (char*)String("").c_str();
mqttPublishTopic = (char*)String("").c_str();
mqttSubscribeTopic = (char*)String("").c_str();
mqttHost = 0;
mqttClientID = 0;
mqttPublishTopic = 0;
mqttSubscribeTopic = 0;
mqttUser = 0;
mqttPass = 0;
mqttPort = 1883;
authSecurity = 0;
authUser = 0;
authPass = 0;
fuseSize = 0;
distSys = 0;
EEPROM.begin(EEPROM_SIZE);
int cs = EEPROM.read(address);
if (cs >= 71)
if (cs == EEPROM_CHECK_SUM)
{
address++;
address += readString(address, &ssid);
address += readString(address, &ssidPassword);
address += readByte(address, &meterType);
address += readString(address, &mqtt);
address += readInt(address, &mqttPort);
address += readString(address, &mqttClientID);
address += readString(address, &mqttPublishTopic);
address += readString(address, &mqttSubscribeTopic);
bool mqtt = false;
address += readBool(address, &mqtt);
if(mqtt) {
address += readString(address, &mqttHost);
address += readInt(address, &mqttPort);
address += readString(address, &mqttClientID);
address += readString(address, &mqttPublishTopic);
address += readString(address, &mqttSubscribeTopic);
}
bool secure = false;
address += readBool(address, &secure);
if (secure)
{
address += readString(address, &mqttUser);
@@ -99,12 +116,8 @@ bool configuration::load()
mqttPass = 0;
}
success = true;
}
if(cs >= 72) {
bool auth = false;
address += readBool(address, &auth);
if (auth) {
address += readByte(address, &authSecurity);
if (authSecurity > 0) {
address += readString(address, &authUser);
address += readString(address, &authPass);
} else {
@@ -112,6 +125,9 @@ bool configuration::load()
authPass = 0;
}
address += readInt(address, &fuseSize);
address += readByte(address, &distSys);
success = true;
}
EEPROM.end();
@@ -123,10 +139,6 @@ bool configuration::isSecure()
return (mqttUser != 0) && (String(mqttUser).length() > 0);
}
bool configuration::isAuth() {
return (authUser != 0) && (String(authUser).length() > 0);
}
int configuration::readInt(int address, int *value)
{
int lower = EEPROM.read(address);
@@ -177,11 +189,13 @@ void configuration::print(Stream* debugger)
debugger->printf("ssid: %s\r\n", this->ssid);
debugger->printf("ssidPassword: %s\r\n", this->ssidPassword);
debugger->printf("meterType: %i\r\n", this->meterType);
debugger->printf("mqtt: %s\r\n", this->mqtt);
debugger->printf("mqttPort: %i\r\n", this->mqttPort);
debugger->printf("mqttClientID: %s\r\n", this->mqttClientID);
debugger->printf("mqttPublishTopic: %s\r\n", this->mqttPublishTopic);
debugger->printf("mqttSubscribeTopic: %s\r\n", this->mqttSubscribeTopic);
if(this->mqttHost) {
debugger->printf("mqttHost: %s\r\n", this->mqttHost);
debugger->printf("mqttPort: %i\r\n", this->mqttPort);
debugger->printf("mqttClientID: %s\r\n", this->mqttClientID);
debugger->printf("mqttPublishTopic: %s\r\n", this->mqttPublishTopic);
debugger->printf("mqttSubscribeTopic: %s\r\n", this->mqttSubscribeTopic);
}
if (this->isSecure())
{
@@ -190,11 +204,14 @@ void configuration::print(Stream* debugger)
debugger->printf("mqttPass: %s\r\n", this->mqttPass);
}
if (this->isAuth()) {
if (this->authSecurity > 0) {
debugger->printf("WEB AUTH:\r\n");
debugger->printf("authSecurity: %i\r\n", this->authSecurity);
debugger->printf("authUser: %s\r\n", this->authUser);
debugger->printf("authPass: %s\r\n", this->authPass);
}
debugger->printf("fuseSize: %i\r\n", this->fuseSize);
debugger->printf("distSys: %i\r\n", this->distSys);
debugger->println("-----------------------------------------------");
}
@@ -234,7 +251,7 @@ int configuration::readString(int pAddress, char* pString[])
int configuration::saveString(int pAddress, char* pString)
{
int address = 0;
int length = strlen(pString) + 1;
int length = pString ? strlen(pString) + 1 : 0;
EEPROM.put(pAddress + address, length);
address++;

View File

@@ -16,7 +16,7 @@ class configuration {
public:
char* ssid;
char* ssidPassword;
char* mqtt;
char* mqttHost;
int mqttPort;
char* mqttClientID;
char* mqttPublishTopic;
@@ -25,12 +25,15 @@ public:
char* mqttPass;
byte meterType;
byte authSecurity;
char* authUser;
char* authPass;
int fuseSize;
byte distSys;
bool hasConfig();
bool isSecure();
bool isAuth();
bool save();
bool load();
@@ -39,7 +42,7 @@ protected:
private:
const int EEPROM_SIZE = 512;
const byte EEPROM_CHECK_SUM = 72; // Used to check if config is stored. Change if structure changes
const byte EEPROM_CHECK_SUM = 75; // Used to check if config is stored. Change if structure changes
const int EEPROM_CONFIG_ADDRESS = 0;
int saveString(int pAddress, char* pString);

View File

@@ -1,135 +0,0 @@
const char STYLE_CSS[] PROGMEM = R"=="==(
body,div,input {
font-family: "Roboto", Arial, Lucida Grande;
}
.wrapper {
width: 500px;
position: absolute;
padding: 30px;
background-color: #FFF;
border-radius: 1px;
color: #333;
border-color: rgba(0, 0, 0, 0.03);
box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12);
margin-left: 20px;
margin-top: 20px;
}
div {
padding-bottom: 5px;
}
label {
font-family: "Roboto", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 16px;
width: 100px;
display: inline-block;
}
input {
font-family: "Roboto", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 16px;
bottom: 30px;
border: none;
border-bottom: 1px solid #d4d4d4;
padding: 10px;
background: transparent;
transition: all .25s ease;
}
input[type=number] {
width: 70px;
margin-left: 5px;
}
input:focus {
outline: none;
border-bottom: 1px solid #3f51b5;
}
h2 {
text-align: left;
font-size: 20px;
font-weight: bold;
letter-spacing: 3px;
line-height: 28px;
}
.submit-button {
position: absolute;
text-align: right;
border-radius: 20px;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
background-color: #3f51b5;
color: #FFF;
padding: 12px 25px;
display: inline-block;
font-size: 12px;
font-weight: bold;
letter-spacing: 2px;
right: 0px;
bottom: 10px;
cursor: pointer;
transition: all .25s ease;
box-shadow: 0 2px 2px rgba(0, 0, 0, .24), 0 0 2px rgba(0, 0, 0, .12);
width: 100px;
}
.select-style {
border-top: 10px solid white;
border-bottom: 1px solid #d4d4d4;
color: #ffffff;
cursor: pointer;
display: block;
font-family: Roboto, "Helvetica Neue", sans-serif;
font-size: 14px;
font-weight: 400;
height: 16px;
line-height: 14px;
min-width: 200px;
padding-bottom: 7px;
padding-left: 0px;
padding-right: 0px;
position: relative;
text-align: left;
width: 80%;
-webkit-box-direction: normal;
overflow: hidden;
background: #ffffff url("data:image/png;base64,R0lGODlhDwAUAIABAAAAAP///yH5BAEAAAEALAAAAAAPABQAAAIXjI+py+0Po5wH2HsXzmw//lHiSJZmUAAAOw==") no-repeat 98% 50%;
}
.disabled-option {
color: #d4d4d4;
}
.select-style select {
padding: 5px 8px;
width: 100%;
border: none;
box-shadow: none;
background: transparent;
background-image: none;
-webkit-appearance: none;
}
.select-style select:focus {
outline: none;
border: none;
}
@media only screen and (max-width: 1000px) {
.wrapper {
width: 80%;
}
}
@media only screen and (max-width: 300px) {
.wrapper {
width: 75%;
}
}
@media only screen and (max-width: 600px) {
.wrapper {
width: 80%;
margin-left: 0px;
margin-top: 0px;
}
.submit-button {
bottom: 0px;
width: 70px;
}
input {
width: 100%;
}
}
)=="==";

View File

@@ -8,9 +8,11 @@ enum class Aidon
{
List1 = 0x01,
List1PhaseShort = 0x09,
List1PhaseLong = 0xff, // TODO: Need sample
List1PhaseLong = 0x0E,
List3PhaseShort = 0x0D,
List3PhaseLong = 0x12
List3PhaseLong = 0x12,
List3PhaseITShort = 0x0C,
List3PhaseITLong = 0x11,
};
enum class Aidon_List1
@@ -73,6 +75,33 @@ enum class Aidon_List1Phase
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
@@ -176,6 +205,101 @@ enum class Aidon_List3Phase
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

@@ -5,7 +5,7 @@ HanReader::HanReader()
}
void HanReader::setup(HardwareSerial *hanPort, Stream *debugPort)
void HanReader::setup(Stream *hanPort, Stream *debugPort)
{
han = hanPort;
bytesRead = 0;
@@ -13,7 +13,7 @@ void HanReader::setup(HardwareSerial *hanPort, Stream *debugPort)
if (debug) debug->println("MBUS serial setup complete");
}
void HanReader::setup(HardwareSerial *hanPort)
void HanReader::setup(Stream *hanPort)
{
setup(hanPort, NULL);
}

View File

@@ -18,8 +18,8 @@ public:
bool compensateFor09HeaderBug = false;
HanReader();
void setup(HardwareSerial *hanPort);
void setup(HardwareSerial *hanPort, Stream *debugPort);
void setup(Stream *hanPort);
void setup(Stream *hanPort, Stream *debugPort);
bool read();
bool read(byte data);
int getListSize();
@@ -30,7 +30,7 @@ public:
private:
Stream *debug;
HardwareSerial *han;
Stream *han;
byte buffer[512];
int bytesRead;
DlmsReader reader;

View File

@@ -10,9 +10,11 @@ static void hanToJsonKaifa3phase(int listSize, JsonObject& data, HanReader& hanR
{
data["lv"] = hanReader.getString( (int)Kaifa_List3Phase::ListVersionIdentifier);
data["id"] = hanReader.getString( (int)Kaifa_List3Phase::MeterID);
data["type"] = hanReader.getString( (int)Kaifa_List3Phase::MeterType);
data["type"] = hanReader.getString( (int)Kaifa_List3Phase::MeterType);
data["P"] = hanReader.getInt( (int)Kaifa_List3Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Kaifa_List3Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Kaifa_List3Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL1)) / 1000;
data["I2"] = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL2)) / 1000;
data["I3"] = ((double) hanReader.getInt((int)Kaifa_List3Phase::CurrentL3)) / 1000;
@@ -23,6 +25,7 @@ static void hanToJsonKaifa3phase(int listSize, JsonObject& data, HanReader& hanR
if (listSize >= (int)Kaifa::List3PhaseLong)
{
data["rtc"] = hanReader.getTime( (int)Kaifa_List3Phase::MeterClock);
data["tPI"] = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Kaifa_List3Phase::CumulativeReactiveImportEnergy);
@@ -39,12 +42,15 @@ static void hanToJsonKaifa1phase(int listSize, JsonObject& data, HanReader& hanR
data["type"] = hanReader.getString( (int)Kaifa_List1Phase::MeterType);
data["P"] = hanReader.getInt( (int)Kaifa_List1Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Kaifa_List1Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Kaifa_List1Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt((int)Kaifa_List1Phase::CurrentL1)) / 1000;
data["U1"] = ((double) hanReader.getInt((int)Kaifa_List1Phase::VoltageL1)) / 10;
}
if (listSize >= (int)Kaifa::List1PhaseLong)
{
data["rtc"] = hanReader.getTime( (int)Kaifa_List1Phase::MeterClock);
data["tPI"] = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Kaifa_List1Phase::CumulativeReactiveImportEnergy);
@@ -77,15 +83,15 @@ static void hanToJsonKaifa(JsonObject& data, HanReader& hanReader, Stream *debug
}
static void hanToJsonAidon3phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger)
{
if (listSize >= (int)Aidon::List3PhaseShort)
{
static void hanToJsonAidon3phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger) {
if (listSize >= (int)Aidon::List3PhaseShort) {
data["lv"] = hanReader.getString( (int)Aidon_List3Phase::ListVersionIdentifier);
data["id"] = hanReader.getString( (int)Aidon_List3Phase::MeterID);
data["type"] = hanReader.getString( (int)Aidon_List3Phase::MeterType);
data["P"] = hanReader.getInt( (int)Aidon_List3Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Aidon_List3Phase::ReactiveExportPower);
data["Q"] = hanReader.getInt( (int)Aidon_List3Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Aidon_List3Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Aidon_List3Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL1)) / 10;
data["I2"] = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL2)) / 10;
data["I3"] = ((double) hanReader.getInt( (int)Aidon_List3Phase::CurrentL3)) / 10;
@@ -94,15 +100,13 @@ static void hanToJsonAidon3phase(int listSize, JsonObject& data, HanReader& hanR
data["U3"] = ((double) hanReader.getInt( (int)Aidon_List3Phase::VoltageL3)) / 10;
}
if (listSize >= (int)Aidon::List3PhaseLong)
{
if (listSize >= (int)Aidon::List3PhaseLong) {
data["rtc"] = hanReader.getTime( (int)Aidon_List3Phase::Timestamp);
data["tPI"] = hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Aidon_List3Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveImportEnergy);
data["tQO"] = hanReader.getInt( (int)Aidon_List3Phase::CumulativeReactiveExportEnergy);
}
// TODO: Do not divide Aidon values by 10!?
}
static void hanToJsonAidon1phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger)
@@ -113,12 +117,50 @@ static void hanToJsonAidon1phase(int listSize, JsonObject& data, HanReader& hanR
data["id"] = hanReader.getString( (int)Aidon_List1Phase::MeterID);
data["type"] = hanReader.getString( (int)Aidon_List1Phase::MeterType);
data["P"] = hanReader.getInt( (int)Aidon_List1Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Aidon_List1Phase::ReactiveExportPower);
data["Q"] = hanReader.getInt( (int)Aidon_List1Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Aidon_List1Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Aidon_List1Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt( (int)Aidon_List1Phase::CurrentL1)) / 10;
data["U1"] = ((double) hanReader.getInt( (int)Aidon_List1Phase::VoltageL1)) / 10;
}
// TODO Aidon::List1PhaseLong
if (listSize >= (int)Aidon::List1PhaseLong)
{
data["rtc"] = hanReader.getTime( (int)Aidon_List1Phase::Timestamp);
data["tPI"] = hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Aidon_List1Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveImportEnergy);
data["tQO"] = hanReader.getInt( (int)Aidon_List1Phase::CumulativeReactiveExportEnergy);
}
}
static void hanToJsonAidon3phaseIT(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger)
{
if (listSize >= (int)Aidon::List3PhaseITShort)
{
data["lv"] = hanReader.getString( (int)Aidon_List3PhaseIT::ListVersionIdentifier);
data["id"] = hanReader.getString( (int)Aidon_List3PhaseIT::MeterID);
data["type"] = hanReader.getString( (int)Aidon_List3PhaseIT::MeterType);
data["P"] = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Aidon_List3PhaseIT::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Aidon_List3PhaseIT::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL1)) / 10;
data["I2"] = 0;
data["I3"] = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::CurrentL3)) / 10;
data["U1"] = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL1)) / 10;
data["U2"] = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL2)) / 10;
data["U3"] = ((double) hanReader.getInt( (int)Aidon_List3PhaseIT::VoltageL3)) / 10;
}
if (listSize >= (int)Aidon::List3PhaseITLong)
{
data["rtc"] = hanReader.getTime( (int)Aidon_List3PhaseIT::Timestamp);
data["tPI"] = hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveImportEnergy);
data["tQO"] = hanReader.getInt( (int)Aidon_List3PhaseIT::CumulativeReactiveExportEnergy);
}
}
static void hanToJsonAidon(JsonObject& data, HanReader& hanReader, Stream *debugger)
@@ -141,21 +183,24 @@ static void hanToJsonAidon(JsonObject& data, HanReader& hanReader, Stream *debug
case (int)Aidon::List1PhaseShort:
case (int)Aidon::List1PhaseLong:
return hanToJsonAidon1phase(listSize, data, hanReader, debugger);
case (int)Aidon::List3PhaseITShort:
case (int)Aidon::List3PhaseITLong:
return hanToJsonAidon3phaseIT(listSize, data, hanReader, debugger);
default:
if (debugger) debugger->printf("Warning: Unknown listSize %d\n", listSize);
return;
}
}
static void hanToJsonKamstrup3phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger)
{
if (listSize >= (int)Kamstrup::List3PhaseShort)
{
static void hanToJsonKamstrup3phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger) {
if (listSize >= (int)Kamstrup::List3PhaseShort) {
data["lv"] = hanReader.getString( (int)Kamstrup_List3Phase::ListVersionIdentifier);
data["id"] = hanReader.getString( (int)Kamstrup_List3Phase::MeterID);
data["type"] = hanReader.getString( (int)Kamstrup_List3Phase::MeterType);
data["P"] = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Kamstrup_List3Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Kamstrup_List3Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL1)) / 100;
data["I2"] = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL2)) / 100;
data["I3"] = ((double) hanReader.getInt((int)Kamstrup_List3Phase::CurrentL3)) / 100;
@@ -164,8 +209,8 @@ static void hanToJsonKamstrup3phase(int listSize, JsonObject& data, HanReader& h
data["U3"] = hanReader.getInt( (int)Kamstrup_List3Phase::VoltageL3);
}
if (listSize >= (int)Kamstrup::List3PhaseLong)
{
if (listSize >= (int)Kamstrup::List3PhaseLong) {
data["rtc"] = hanReader.getTime( (int)Kamstrup_List3Phase::MeterClock);
data["tPI"] = hanReader.getInt( (int)Kamstrup_List3Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Kamstrup_List3Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Kamstrup_List3Phase::CumulativeReactiveImportEnergy);
@@ -173,21 +218,21 @@ static void hanToJsonKamstrup3phase(int listSize, JsonObject& data, HanReader& h
}
}
static void hanToJsonKamstrup1phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger)
{
if (listSize >= (int)Kamstrup::List1PhaseShort)
{
static void hanToJsonKamstrup1phase(int listSize, JsonObject& data, HanReader& hanReader, Stream *debugger) {
if (listSize >= (int)Kamstrup::List1PhaseShort) {
data["lv"] = hanReader.getString( (int)Kamstrup_List1Phase::ListVersionIdentifier);
data["id"] = hanReader.getString( (int)Kamstrup_List1Phase::MeterID);
data["type"] = hanReader.getString( (int)Kamstrup_List1Phase::MeterType);
data["P"] = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveImportPower);
data["Q"] = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveImportPower);
data["PO"] = hanReader.getInt( (int)Kamstrup_List1Phase::ActiveExportPower);
data["QO"] = hanReader.getInt( (int)Kamstrup_List1Phase::ReactiveExportPower);
data["I1"] = ((double) hanReader.getInt((int)Kamstrup_List1Phase::CurrentL1)) / 100;
data["U1"] = hanReader.getInt( (int)Kamstrup_List1Phase::VoltageL1);
}
if (listSize >= (int)Kamstrup::List1PhaseLong)
{
if (listSize >= (int)Kamstrup::List1PhaseLong) {
data["rtc"] = hanReader.getTime( (int)Kamstrup_List1Phase::MeterClock);
data["tPI"] = hanReader.getInt( (int)Kamstrup_List1Phase::CumulativeActiveImportEnergy);
data["tPO"] = hanReader.getInt( (int)Kamstrup_List1Phase::CumulativeActiveExportEnergy);
data["tQI"] = hanReader.getInt( (int)Kamstrup_List1Phase::CumulativeReactiveImportEnergy);
@@ -195,8 +240,7 @@ static void hanToJsonKamstrup1phase(int listSize, JsonObject& data, HanReader& h
}
}
static void hanToJsonKamstrup(JsonObject& data, HanReader& hanReader, Stream *debugger)
{
static void hanToJsonKamstrup(JsonObject& data, HanReader& hanReader, Stream *debugger) {
int listSize = hanReader.getListSize();
switch (listSize) {

View File

@@ -7,7 +7,9 @@ board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
build_flags =
-D HAS_DALLAS_TEMP_SENSOR=0
-D IS_CUSTOM_AMS_BOARD=0
-D HW_ROARFRED=1
-D DEBUG_MODE=1
monitor_speed = 2400
monitor_flags = --parity E
monitor_flags =
--parity
E

View File

@@ -1,18 +1,10 @@
[platformio]
extra_configs = platformio-user.ini
[common]
framework = arduino
lib_deps = HanConfigAp@1.0.0, HanReader@1.0.0, HanToJson@1.0.0, ArduinoJson@^6.0.0, MQTT@^2.4.0, DallasTemperature@^3.8.0
[env:esp12e]
platform = espressif8266
board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
build_flags =
-D HAS_DALLAS_TEMP_SENSOR=0
-D IS_CUSTOM_AMS_BOARD=0
lib_deps = HanConfigAp@1.0.0, HanReader@1.0.0, HanToJson@1.0.0, ArduinoJson@^6.0.0, MQTT@^2.4.0, DallasTemperature@^3.8.0, EspSoftwareSerial@^6.7.1
[env:hw1esp12e]
platform = espressif8266
@@ -20,14 +12,52 @@ board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
build_flags =
-D HAS_DALLAS_TEMP_SENSOR=1
-D IS_CUSTOM_AMS_BOARD=1
-D HW_ROARFRED=1
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp12e]
platform = espressif8266
board = esp12e
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:d1mini]
platform = espressif8266
board = d1_mini
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:esp32]
platform = espressif32
board = esp32dev
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:lolind32]
platform = espressif32
board = lolin_d32
framework = ${common.framework}
lib_deps = ${common.lib_deps}
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py
[env:featheresp32]
platform = espressif32
board = featheresp32
framework = ${common.framework}
lib_deps = ${common.lib_deps}
build_flags =
-D HAS_DALLAS_TEMP_SENSOR=0
-D IS_CUSTOM_AMS_BOARD=0
extra_scripts =
pre:scripts/addversion.py
scripts/makeweb.py

16
scripts/addversion.py Normal file
View File

@@ -0,0 +1,16 @@
import os
FILENAME_VERSION_H = 'src/version.h'
version = os.environ.get('GITHUB_REF')
if version == None:
version = "SNAPSHOT"
import datetime
hf = """
#ifndef VERSION
#define VERSION "{}"
#endif
""".format(version)
with open(FILENAME_VERSION_H, 'w+') as f:
f.write(hf)

30
scripts/makeweb.py Normal file
View File

@@ -0,0 +1,30 @@
import os
import re
import shutil
webroot = "web"
srcroot = "src/web/root"
if os.path.exists(srcroot):
shutil.rmtree(srcroot)
os.mkdir(srcroot)
else:
os.mkdir(srcroot)
for filename in os.listdir(webroot):
basename = re.sub("[^0-9a-zA-Z]+", "_", filename)
srcfile = webroot + "/" + filename
dstfile = srcroot + "/" + basename + ".h"
varname = basename.upper()
with open(dstfile, "w") as dst:
dst.write("const char ")
dst.write(varname)
dst.write("[] PROGMEM = R\"==\"==(\n")
with open(srcfile, "r") as src:
for line in src.readlines():
dst.write(line)
dst.write("\n)==\"==\";\n")

70
src/AmsToMqttBridge.h Normal file
View File

@@ -0,0 +1,70 @@
#define WIFI_CONNECTION_TIMEOUT 30000;
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
// Build settings for custom hardware by Roar Fredriksen
#if HW_ROARFRED
#define LED_PIN 2 // The blue on-board LED of the ESP8266 custom AMS board
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN 0
#include <DallasTemperature.h>
#include <OneWire.h>
#define TEMP_SENSOR_PIN 5 // Temperature sensor connected to GPIO5
#if DEBUG_MODE
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(3);
#else
HardwareSerial *hanSerial = &Serial;
#endif
// Build settings for Wemos Lolin D32
#elif defined(ARDUINO_LOLIN_D32)
#define LED_PIN 5
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN 4
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(GPIO_NUM_21);
// Build settings for Wemos D1 mini
#elif defined(ARDUINO_ESP8266_WEMOS_D1MINI)
#define LED_PIN D4
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN D2
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(D1);
// Build settings for Adafruit Feather ESP32
#elif defined(ARDUINO_FEATHER_ESP32)
#define LED_PIN LED_BUILTIN
#define LED_ACTIVE_HIGH 1
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
HardwareSerial *hanSerial = &Serial2;
// Default build settings
#else
#define LED_PIN 2
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
#define SOFTWARE_SERIAL 1
#include <SoftwareSerial.h>
SoftwareSerial *hanSerial = new SoftwareSerial(5);
#endif
#if defined TEMP_SENSOR_PIN
OneWire oneWire(TEMP_SENSOR_PIN);
DallasTemperature tempSensor(&oneWire);
#endif

View File

@@ -4,72 +4,53 @@
Author: roarf
*/
//#define HAS_DALLAS_TEMP_SENSOR 1 // Set to zero if Dallas one wire temp sensor is not present
//#define IS_CUSTOM_AMS_BOARD 1 // Set to zero if using NodeMCU or board not designed by Roar Fredriksen
#include "AmsToMqttBridge.h"
#include <ArduinoJson.h>
#include <MQTT.h>
#if HAS_DALLAS_TEMP_SENSOR
#include <DallasTemperature.h>
#include <OneWire.h>
#endif
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include "web/AmsWebServer.h"
#include "HanConfigAp.h"
#include "HanReader.h"
#include "HanToJson.h"
#define WIFI_CONNECTION_TIMEOUT 30000;
#if IS_CUSTOM_AMS_BOARD
#define LED_PIN 2 // The blue on-board LED of the ESP8266 custom AMS board
#define LED_ACTIVE_HIGH 0
#define AP_BUTTON_PIN 0
#else
#define LED_PIN LED_BUILTIN
#define LED_ACTIVE_HIGH 1
#define AP_BUTTON_PIN INVALID_BUTTON_PIN
#endif
#if HAS_DALLAS_TEMP_SENSOR
#define TEMP_SENSOR_PIN 5 // Temperature sensor connected to GPIO5
OneWire oneWire(TEMP_SENSOR_PIN);
DallasTemperature tempSensor(&oneWire);
#endif
// Configuration
configuration config;
// Object used to boot as Access Point
HanConfigAp ap;
// Web server
AmsWebServer ws;
// WiFi client and MQTT client
WiFiClient *client;
MQTTClient mqtt(384);
MQTTClient mqtt(512);
// Object used for debugging
HardwareSerial* debugger = NULL;
Stream* debugger = NULL;
// The HAN Port reader, used to read serial data and decode DLMS
HanReader hanReader;
// the setup function runs once when you press reset or power the board
void setup() {
if(config.hasConfig()) {
config.load();
}
#if DEBUG_MODE
debugger = &Serial;
#if HW_ROARFRED
SoftwareSerial *ser = new SoftwareSerial(-1, 1);
ser->begin(115200, SWSERIAL_8N1);
debugger = ser;
#else
HardwareSerial *ser = &Serial;
ser->begin(115200, SERIAL_8N1);
#endif
debugger = ser;
#endif
if (debugger) {
// Setup serial port for debugging
debugger->begin(2400, SERIAL_8E1);
//debugger->begin(115200);
while (!debugger);
debugger->println("");
debugger->println("Started...");
}
@@ -81,28 +62,40 @@ void setup() {
delay(1000);
// Initialize the AP
ap.setup(AP_BUTTON_PIN, debugger);
ap.setup(AP_BUTTON_PIN, &config, debugger);
led_off();
if (!ap.isActivated)
{
setupWiFi();
// Configure uart for AMS data
if(ap.config.meterType == 3) {
Serial.begin(2400, SERIAL_8N1);
} else {
Serial.begin(2400, SERIAL_8E1);
}
while (!Serial);
hanReader.setup(&Serial, debugger);
// Configure uart for AMS data
#if defined SOFTWARE_SERIAL
if(config.meterType == 3) {
hanSerial->begin(2400, SWSERIAL_8N1);
} else {
hanSerial->begin(2400, SWSERIAL_8E1);
}
#else
if(config.meterType == 3) {
hanSerial->begin(2400, SERIAL_8N1);
} else {
hanSerial->begin(2400, SERIAL_8E1);
}
#if defined UART2
hanSerial->swap();
#endif
#endif
while (!&hanSerial);
hanReader.setup(hanSerial, debugger);
// Compensate for the known Kaifa bug
hanReader.compensateFor09HeaderBug = (ap.config.meterType == 1);
hanReader.compensateFor09HeaderBug = (config.meterType == 1);
}
ap.enableWeb();
ws.setup(&config, debugger);
}
// the loop function runs over and over again until power down or reset
@@ -114,19 +107,20 @@ void loop()
// Turn off the LED
led_off();
// allow the MQTT client some resources
mqtt.loop();
delay(10); // <- fixes some issues with WiFi stability
// Reconnect to WiFi and MQTT as needed
if (!mqtt.connected()) {
MQTT_connect();
}
else
{
// Read data from the HAN port
readHanPort();
if (WiFi.status() != WL_CONNECTED) {
WiFi_connect();
} else {
if (config.mqttHost) {
mqtt.loop();
yield();
if(!mqtt.connected()) {
MQTT_connect();
}
}
}
readHanPort();
}
else
{
@@ -134,6 +128,8 @@ void loop()
if (millis() / 1000 % 2 == 0) led_on();
else led_off();
}
ws.loop();
delay(1); // Needed for auto modem sleep
}
@@ -164,7 +160,7 @@ void setupWiFi()
// Connect to WiFi
WiFi.mode(WIFI_STA);
WiFi.begin(ap.config.ssid, ap.config.ssidPassword);
WiFi.begin(config.ssid, config.ssidPassword);
// Wait for WiFi connection
if (debugger) debugger->print("\nWaiting for WiFi to connect...");
@@ -175,16 +171,6 @@ void setupWiFi()
if (debugger) debugger->println(" connected");
client = new WiFiClient();
mqtt.begin(ap.config.mqtt, *client);
// Direct incoming MQTT messages
if (ap.config.mqttSubscribeTopic != 0 && strlen(ap.config.mqttSubscribeTopic) > 0) {
mqtt.subscribe(ap.config.mqttSubscribeTopic);
mqtt.onMessage(mqttMessageReceived);
}
// Notify everyone we're here!
sendMqttData("Connected!");
}
void mqttMessageReceived(String &topic, String &payload)
@@ -204,7 +190,7 @@ void mqttMessageReceived(String &topic, String &payload)
void readHanPort()
{
if (hanReader.read())
if (hanReader.read() && config.hasConfig())
{
// Flash LED on, this shows us that data is received
led_on();
@@ -215,7 +201,7 @@ void readHanPort()
if (debugger) debugger->println(time);
// Define a json object to keep the data
StaticJsonDocument<500> json;
StaticJsonDocument<1024> json;
// Any generic useful info here
json["id"] = WiFi.macAddress();
@@ -226,56 +212,51 @@ void readHanPort()
// to keep the data from the meter itself
JsonObject data = json.createNestedObject("data");
#if HAS_DALLAS_TEMP_SENSOR
#if defined TEMP_SENSOR_PIN
// Get the temperature too
tempSensor.requestTemperatures();
data["temp"] = tempSensor.getTempCByIndex(0);
#endif
hanToJson(data, ap.config.meterType, hanReader);
hanToJson(data, config.meterType, hanReader);
// Write the json to the debug port
if (debugger) {
debugger->print("Sending data to MQTT: ");
serializeJsonPretty(json, *debugger);
debugger->println();
}
if(config.mqttHost != 0 && strlen(config.mqttHost) != 0 && config.mqttPublishTopic != 0 && strlen(config.mqttPublishTopic) != 0) {
// Write the json to the debug port
if (debugger) {
debugger->print("Sending data to MQTT: ");
serializeJsonPretty(json, *debugger);
debugger->println();
}
// Make sure we have configured a publish topic
if (! ap.config.mqttPublishTopic == 0 || strlen(ap.config.mqttPublishTopic) == 0)
{
// Publish the json to the MQTT server
String msg;
serializeJson(json, msg);
mqtt.publish(ap.config.mqttPublishTopic, msg.c_str());
mqtt.publish(config.mqttPublishTopic, msg.c_str());
mqtt.loop();
}
ws.setJson(json);
// Flash LED off
led_off();
}
}
// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
void MQTT_connect()
{
void WiFi_connect() {
// Connect to WiFi access point.
if (debugger)
{
debugger->println();
debugger->println();
debugger->print("Connecting to WiFi network ");
debugger->println(ap.config.ssid);
debugger->println(config.ssid);
}
if (WiFi.status() != WL_CONNECTED)
{
// Make one first attempt at connect, this seems to considerably speed up the first connection
WiFi.disconnect();
WiFi.begin(ap.config.ssid, ap.config.ssidPassword);
WiFi.begin(config.ssid, config.ssidPassword);
delay(1000);
}
@@ -294,7 +275,7 @@ void MQTT_connect()
debugger->println(WiFi.status());
}
WiFi.disconnect();
WiFi.begin(ap.config.ssid, ap.config.ssidPassword);
WiFi.begin(config.ssid, config.ssidPassword);
vTimeout = millis() + WIFI_CONNECTION_TIMEOUT;
}
yield();
@@ -305,61 +286,63 @@ void MQTT_connect()
debugger->println("WiFi connected");
debugger->println("IP address: ");
debugger->println(WiFi.localIP());
debugger->print("\nconnecting to MQTT: ");
debugger->print(ap.config.mqtt);
}
}
// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care if connecting.
unsigned long lastMqttRetry = -10000;
void MQTT_connect() {
if(!config.mqttHost) {
if(debugger) debugger->println("No MQTT config");
return;
}
if(millis() - lastMqttRetry < 5000) {
yield();
return;
}
lastMqttRetry = millis();
if(debugger) {
debugger->print("Connecting to MQTT: ");
debugger->print(config.mqttHost);
debugger->print(", port: ");
debugger->print(ap.config.mqttPort);
debugger->print(config.mqttPort);
debugger->println();
}
// Wait for the MQTT connection to complete
while (!mqtt.connected()) {
// Connect to a unsecure or secure MQTT server
if ((ap.config.mqttUser == 0 && mqtt.connect(ap.config.mqttClientID)) ||
(ap.config.mqttUser != 0 && mqtt.connect(ap.config.mqttClientID, ap.config.mqttUser, ap.config.mqttPass)))
{
if (debugger) debugger->println("\nSuccessfully connected to MQTT!");
mqtt.disconnect();
// Subscribe to the chosen MQTT topic, if set in configuration
if (ap.config.mqttSubscribeTopic != 0 && strlen(ap.config.mqttSubscribeTopic) > 0)
{
mqtt.subscribe(ap.config.mqttSubscribeTopic);
if (debugger) debugger->printf(" Subscribing to [%s]\r\n", ap.config.mqttSubscribeTopic);
}
}
else
{
if (debugger)
{
debugger->print(".");
debugger->print("failed, ");
debugger->println(" trying again in 5 seconds");
}
mqtt.begin(config.mqttHost, config.mqttPort, *client);
// Wait 2 seconds before retrying
mqtt.disconnect();
// Connect to a unsecure or secure MQTT server
if ((config.mqttUser == 0 && mqtt.connect(config.mqttClientID)) ||
(config.mqttUser != 0 && mqtt.connect(config.mqttClientID, config.mqttUser, config.mqttPass))) {
if (debugger) debugger->println("\nSuccessfully connected to MQTT!");
delay(2000);
// Subscribe to the chosen MQTT topic, if set in configuration
if (config.mqttSubscribeTopic != 0 && strlen(config.mqttSubscribeTopic) > 0) {
mqtt.subscribe(config.mqttSubscribeTopic);
if (debugger) debugger->printf(" Subscribing to [%s]\r\n", config.mqttSubscribeTopic);
}
// Allow some resources for the WiFi connection
yield();
delay(2000);
sendMqttData("Connected!");
} else {
if (debugger) {
debugger->print(" failed, ");
debugger->println(" trying again in 5 seconds");
}
}
yield();
}
// Send a simple string embedded in json over MQTT
void sendMqttData(String data)
{
// Make sure we have configured a publish topic
if (ap.config.mqttPublishTopic == 0 || strlen(ap.config.mqttPublishTopic) == 0)
if (config.mqttPublishTopic == 0 || strlen(config.mqttPublishTopic) == 0)
return;
// Make sure we're connected
if (!client->connected() || !mqtt.connected()) {
MQTT_connect();
}
// Build a json with the message in a "data" attribute
StaticJsonDocument<500> json;
json["id"] = WiFi.macAddress();
@@ -371,7 +354,7 @@ void sendMqttData(String data)
serializeJson(json, msg);
// Send the json over MQTT
mqtt.publish(ap.config.mqttPublishTopic, msg.c_str());
mqtt.publish(config.mqttPublishTopic, msg.c_str());
if (debugger) debugger->print("sendMqttData: ");
if (debugger) debugger->println(data);

142
src/Base64.cpp Normal file
View File

@@ -0,0 +1,142 @@
/*
Copyright (C) 2016 Arturo Guadalupi. All right reserved.
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*/
#include "Base64.h"
#include <Arduino.h>
#if (defined(__AVR__))
#include <avr\pgmspace.h>
#else
#include <pgmspace.h>
#endif
const char PROGMEM _Base64AlphabetTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
int Base64Class::encode(char *output, char *input, int inputLength) {
int i = 0, j = 0;
int encodedLength = 0;
unsigned char A3[3];
unsigned char A4[4];
while(inputLength--) {
A3[i++] = *(input++);
if(i == 3) {
fromA3ToA4(A4, A3);
for(i = 0; i < 4; i++) {
output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[i]]);
}
i = 0;
}
}
if(i) {
for(j = i; j < 3; j++) {
A3[j] = '\0';
}
fromA3ToA4(A4, A3);
for(j = 0; j < i + 1; j++) {
output[encodedLength++] = pgm_read_byte(&_Base64AlphabetTable[A4[j]]);
}
while((i++ < 3)) {
output[encodedLength++] = '=';
}
}
output[encodedLength] = '\0';
return encodedLength;
}
int Base64Class::decode(char * output, char * input, int inputLength) {
int i = 0, j = 0;
int decodedLength = 0;
unsigned char A3[3];
unsigned char A4[4];
while (inputLength--) {
if(*input == '=') {
break;
}
A4[i++] = *(input++);
if (i == 4) {
for (i = 0; i <4; i++) {
A4[i] = lookupTable(A4[i]);
}
fromA4ToA3(A3,A4);
for (i = 0; i < 3; i++) {
output[decodedLength++] = A3[i];
}
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++) {
A4[j] = '\0';
}
for (j = 0; j <4; j++) {
A4[j] = lookupTable(A4[j]);
}
fromA4ToA3(A3,A4);
for (j = 0; j < i - 1; j++) {
output[decodedLength++] = A3[j];
}
}
output[decodedLength] = '\0';
return decodedLength;
}
int Base64Class::encodedLength(int plainLength) {
int n = plainLength;
return (n + 2 - ((n + 2) % 3)) / 3 * 4;
}
int Base64Class::decodedLength(char * input, int inputLength) {
int i = 0;
int numEq = 0;
for(i = inputLength - 1; input[i] == '='; i--) {
numEq++;
}
return ((6 * inputLength) / 8) - numEq;
}
//Private utility functions
inline void Base64Class::fromA3ToA4(unsigned char * A4, unsigned char * A3) {
A4[0] = (A3[0] & 0xfc) >> 2;
A4[1] = ((A3[0] & 0x03) << 4) + ((A3[1] & 0xf0) >> 4);
A4[2] = ((A3[1] & 0x0f) << 2) + ((A3[2] & 0xc0) >> 6);
A4[3] = (A3[2] & 0x3f);
}
inline void Base64Class::fromA4ToA3(unsigned char * A3, unsigned char * A4) {
A3[0] = (A4[0] << 2) + ((A4[1] & 0x30) >> 4);
A3[1] = ((A4[1] & 0xf) << 4) + ((A4[2] & 0x3c) >> 2);
A3[2] = ((A4[2] & 0x3) << 6) + A4[3];
}
inline unsigned char Base64Class::lookupTable(char c) {
if(c >='A' && c <='Z') return c - 'A';
if(c >='a' && c <='z') return c - 71;
if(c >='0' && c <='9') return c + 4;
if(c == '+') return 62;
if(c == '/') return 63;
return -1;
}
Base64Class Base64;

26
src/Base64.h Normal file
View File

@@ -0,0 +1,26 @@
/*
Copyright (C) 2016 Arturo Guadalupi. All right reserved.
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*/
#ifndef _BASE64_H
#define _BASE64_H
class Base64Class{
public:
int encode(char *output, char *input, int inputLength);
int decode(char * output, char * input, int inputLength);
int encodedLength(int plainLength);
int decodedLength(char * input, int inputLength);
private:
inline void fromA3ToA4(unsigned char * A4, unsigned char * A3);
inline void fromA4ToA3(unsigned char * A3, unsigned char * A4);
inline unsigned char lookupTable(char c);
};
extern Base64Class Base64;
#endif // _BASE64_H

375
src/web/AmsWebServer.cpp Normal file
View File

@@ -0,0 +1,375 @@
#include "AmsWebServer.h"
#include "version.h"
#include "root/index_html.h"
#include "root/configuration_html.h"
#include "root/boot_css.h"
#include "root/gaugemeter_js.h"
#include "Base64.h"
#if defined(ESP8266)
ESP8266WebServer server(80);
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
WebServer server(80);
#endif
void AmsWebServer::setup(configuration* config, Stream* debugger) {
this->config = config;
this->debugger = debugger;
server.on("/", std::bind(&AmsWebServer::indexHtml, this));
server.on("/configuration", std::bind(&AmsWebServer::configurationHtml, this));
server.on("/boot.css", std::bind(&AmsWebServer::bootCss, this));
server.on("/gaugemeter.js", std::bind(&AmsWebServer::gaugemeterJs, this));
server.on("/data.json", std::bind(&AmsWebServer::dataJson, this));
server.on("/save", std::bind(&AmsWebServer::handleSave, this));
server.begin(); // Web server start
print("Web server is ready for config at http://");
if(WiFi.getMode() == WIFI_AP) {
print(WiFi.softAPIP());
} else {
print(WiFi.localIP());
}
println("/");
}
void AmsWebServer::loop() {
server.handleClient();
}
void AmsWebServer::setJson(StaticJsonDocument<1024> json) {
if(!json.isNull()) {
p = json["data"]["P"].as<int>();
if(json["data"].containsKey("U1")) {
u1 = json["data"]["U1"].as<double>();
i1 = json["data"]["I1"].as<double>();
if(json["data"].containsKey("U2")) {
u2 = json["data"]["U2"].as<double>();
i2 = json["data"]["I2"].as<double>();
if(json["data"].containsKey("U3")) {
u3 = json["data"]["U3"].as<double>();
i3 = json["data"]["I3"].as<double>();
}
}
if(maxPwr == 0 && config->hasConfig() && config->fuseSize > 0 && config->distSys > 0) {
int volt = config->distSys == 2 ? 400 : 230;
if(u2 > 0) {
maxPwr = config->fuseSize * sqrt(3) * volt;
} else {
maxPwr = config->fuseSize * 230;
}
}
} else {
if(u1 > 0) {
json["data"]["U1"] = u1;
json["data"]["I1"] = i1;
}
if(u2 > 0) {
json["data"]["U2"] = u2;
json["data"]["I2"] = i2;
}
if(u3 > 0) {
json["data"]["U3"] = u3;
json["data"]["I3"] = i3;
}
}
this->json = json;
}
}
bool AmsWebServer::checkSecurity(byte level) {
bool access = WiFi.getMode() == WIFI_AP || !config->hasConfig() || config->authSecurity < level;
if(!access && config->authSecurity >= level && server.hasHeader("Authorization")) {
println(" forcing web security");
String expectedAuth = String(config->authUser) + ":" + String(config->authPass);
String providedPwd = server.header("Authorization");
providedPwd.replace("Basic ", "");
char inputString[providedPwd.length()];
providedPwd.toCharArray(inputString, providedPwd.length()+1);
int inputStringLength = sizeof(inputString);
int decodedLength = Base64.decodedLength(inputString, inputStringLength);
char decodedString[decodedLength];
Base64.decode(decodedString, inputString, inputStringLength);
print("Received auth: ");
println(decodedString);
access = String(decodedString).equals(expectedAuth);
}
if(!access) {
println(" no access, requesting user/pass");
server.sendHeader("WWW-Authenticate", "Basic realm=\"Secure Area\"");
server.send(401, "text/html", "");
}
return access;
}
void AmsWebServer::indexHtml() {
println("Serving /index.html over http...");
if(!checkSecurity(2))
return;
String html = String((const __FlashStringHelper*) INDEX_HTML);
html.replace("${version}", VERSION);
html.replace("${data.P}", String(p));
html.replace("${data.U1}", u1 > 0 ? String(u1, 1) : "");
html.replace("${data.I1}", u1 > 0 ? String(i1, 1) : "");
html.replace("${display.P1}", u1 > 0 ? "" : "none");
html.replace("${data.U2}", u2 > 0 ? String(u2, 1) : "");
html.replace("${data.I2}", u2 > 0 ? String(i2, 1) : "");
html.replace("${display.P2}", u2 > 0 ? "" : "none");
html.replace("${data.U3}", u3 > 0 ? String(u3, 1) : "");
html.replace("${data.I3}", u3 > 0 ? String(i3, 1) : "");
html.replace("${display.P3}", u3 > 0 ? "" : "none");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "text/html", html);
}
void AmsWebServer::configurationHtml() {
println("Serving /configuration.html over http...");
if(!checkSecurity(1))
return;
String html = String((const __FlashStringHelper*) CONFIGURATION_HTML);
html.replace("${version}", VERSION);
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
if(config->hasConfig()) {
html.replace("${config.ssid}", config->ssid);
html.replace("${config.ssidPassword}", config->ssidPassword);
html.replace("${config.meterType}", String(config->fuseSize));
for(int i = 0; i<4; i++) {
html.replace("${config.meterType" + String(i) + "}", config->meterType == i ? "selected" : "");
}
html.replace("${config.mqtt}", config->mqttHost == 0 ? "" : "checked");
html.replace("${config.mqttHost}", config->mqttHost);
html.replace("${config.mqttPort}", String(config->mqttPort));
html.replace("${config.mqttClientID}", config->mqttClientID);
html.replace("${config.mqttPublishTopic}", config->mqttPublishTopic);
html.replace("${config.mqttSubscribeTopic}", config->mqttSubscribeTopic);
html.replace("${config.mqttUser}", config->mqttUser);
html.replace("${config.mqttPass}", config->mqttPass);
html.replace("${config.authUser}", config->authUser);
html.replace("${config.authSecurity}", String(config->authSecurity));
for(int i = 0; i<3; i++) {
html.replace("${config.authSecurity" + String(i) + "}", config->authSecurity == i ? "selected" : "");
}
html.replace("${config.authPass}", config->authPass);
html.replace("${config.fuseSize}", String(config->fuseSize));
for(int i = 0; i<64; i++) {
html.replace("${config.fuseSize" + String(i) + "}", config->fuseSize == i ? "selected" : "");
}
for(int i = 0; i<3; i++) {
html.replace("${config.distSys" + String(i) + "}", config->distSys == i ? "selected" : "");
}
} else {
html.replace("${config.ssid}", "");
html.replace("${config.ssidPassword}", "");
html.replace("${config.meterType}", "");
for(int i = 0; i<4; i++) {
html.replace("${config.meterType" + String(i) + "}", i == 0 ? "selected" : "");
}
html.replace("${config.mqtt}", "");
html.replace("${config.mqttHost}", "");
html.replace("${config.mqttPort}", "1883");
html.replace("${config.mqttClientID}", "");
html.replace("${config.mqttPublishTopic}", "");
html.replace("${config.mqttSubscribeTopic}", "");
html.replace("${config.mqttUser}", "");
html.replace("${config.mqttPass}", "");
html.replace("${config.authSecurity}", "");
for(int i = 0; i<3; i++) {
html.replace("${config.authSecurity" + String(i) + "}", i == 0 ? "selected" : "");
}
html.replace("${config.authUser}", "");
html.replace("${config.authPass}", "");
html.replace("${config.fuseSize}", "");
for(int i = 0; i<64; i++) {
html.replace("${config.fuseSize" + String(i) + "}", i == 0 ? "selected" : "");
}
for(int i = 0; i<3; i++) {
html.replace("${config.distSys" + String(i) + "}", i == 0 ? "selected" : "");
}
}
server.send(200, "text/html", html);
}
void AmsWebServer::bootCss() {
println("Serving /boot.css over http...");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "text/css", BOOT_CSS);
}
void AmsWebServer::gaugemeterJs() {
println("Serving /gaugemeter.js over http...");
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "application/javascript", GAUGEMETER_JS);
}
void AmsWebServer::dataJson() {
println("Serving /data.json over http...");
if(!checkSecurity(2))
return;
String jsonStr;
if(!json.isNull()) {
println(" json has data");
int maxPwr = this->maxPwr;
if(maxPwr == 0) {
if(u2 > 0) {
maxPwr = 20000;
} else {
maxPwr = 10000;
}
}
json["maxPower"] = maxPwr;
json["pct"] = min(p*100/maxPwr, 100);
json["meterType"] = config->meterType;
json["currentMillis"] = millis();
serializeJson(json, jsonStr);
} else {
println(" json is empty");
jsonStr = "{}";
}
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "application/json", jsonStr);
}
void AmsWebServer::handleSave() {
String temp;
temp = server.arg("ssid");
config->ssid = new char[temp.length() + 1];
temp.toCharArray(config->ssid, temp.length() + 1, 0);
temp = server.arg("ssidPassword");
config->ssidPassword = new char[temp.length() + 1];
temp.toCharArray(config->ssidPassword, temp.length() + 1, 0);
config->meterType = (byte)server.arg("meterType").toInt();
if(server.hasArg("mqtt") && server.arg("mqtt") == "true") {
println("MQTT enabled");
temp = server.arg("mqttHost");
config->mqttHost = new char[temp.length() + 1];
temp.toCharArray(config->mqttHost, temp.length() + 1, 0);
config->mqttPort = (int)server.arg("mqttPort").toInt();
temp = server.arg("mqttClientID");
config->mqttClientID = new char[temp.length() + 1];
temp.toCharArray(config->mqttClientID, temp.length() + 1, 0);
temp = server.arg("mqttPublishTopic");
config->mqttPublishTopic = new char[temp.length() + 1];
temp.toCharArray(config->mqttPublishTopic, temp.length() + 1, 0);
temp = server.arg("mqttSubscribeTopic");
config->mqttSubscribeTopic = new char[temp.length() + 1];
temp.toCharArray(config->mqttSubscribeTopic, temp.length() + 1, 0);
temp = server.arg("mqttUser");
config->mqttUser = new char[temp.length() + 1];
temp.toCharArray(config->mqttUser, temp.length() + 1, 0);
temp = server.arg("mqttPass");
config->mqttPass = new char[temp.length() + 1];
temp.toCharArray(config->mqttPass, temp.length() + 1, 0);
} else {
println("MQTT disabled");
config->mqttHost = NULL;
config->mqttUser = NULL;
config->mqttPass = NULL;
}
config->authSecurity = (byte)server.arg("authSecurity").toInt();
if(config->authSecurity > 0) {
temp = server.arg("authUser");
config->authUser = new char[temp.length() + 1];
temp.toCharArray(config->authUser, temp.length() + 1, 0);
temp = server.arg("authPass");
config->authPass = new char[temp.length() + 1];
temp.toCharArray(config->authPass, temp.length() + 1, 0);
}
config->fuseSize = (int)server.arg("fuseSize").toInt();
config->distSys = (byte)server.arg("distSys").toInt();
println("Saving configuration now...");
if (debugger) config->print(debugger);
if (config->save())
{
println("Successfully saved. Will reboot now.");
String html = "<html><body><h1>Successfully Saved!</h1><h3>Device is restarting now...</h3><a href=\"/\">Go to index</a></form>";
server.send(200, "text/html", html);
yield();
delay(1000);
#if defined(ESP8266)
ESP.reset();
#elif defined(ESP32)
ESP.restart();
#endif
}
else
{
println("Error saving configuration");
String html = "<html><body><h1>Error saving configuration!</h1></form>";
server.send(500, "text/html", html);
}
}
size_t AmsWebServer::print(const char* text)
{
if (debugger) debugger->print(text);
}
size_t AmsWebServer::println(const char* text)
{
if (debugger) debugger->println(text);
}
size_t AmsWebServer::print(const Printable& data)
{
if (debugger) debugger->print(data);
}
size_t AmsWebServer::println(const Printable& data)
{
if (debugger) debugger->println(data);
}

60
src/web/AmsWebServer.h Normal file
View File

@@ -0,0 +1,60 @@
#ifndef _AMSWEBSERVER_h
#define _AMSWEBSERVER_h
#include <ArduinoJson.h>
#include "configuration.h"
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
#include <WiFi.h>
#include <WebServer.h>
#else
#warning "Unsupported board type"
#endif
class AmsWebServer {
public:
void setup(configuration* config, Stream* debugger);
void loop();
void setJson(StaticJsonDocument<1024> json);
private:
configuration* config;
Stream* debugger;
StaticJsonDocument<1024> json;
int maxPwr;
int p;
double u1, u2, u3, i1, i2, i3;
#if defined(ESP8266)
ESP8266WebServer server;
#elif defined(ESP32) // ARDUINO_ARCH_ESP32
WebServer server;
#endif
bool checkSecurity(byte level);
void indexHtml();
void configurationHtml();
void bootCss();
void gaugemeterJs();
void dataJson();
void handleSave();
size_t print(const char* text);
size_t println(const char* text);
size_t print(const Printable& data);
size_t println(const Printable& data);
};
#endif

307
web/boot.css Normal file
View File

@@ -0,0 +1,307 @@
/* Ripped necessary style from bootstrap 4.4.1 to make the page look good without internet access. Meant to be overridden by CSS from CDN */
:root {
--blue: #007bff;
--indigo: #6610f2;
--purple: #6f42c1;
--pink: #e83e8c;
--red: #dc3545;
--orange: #fd7e14;
--yellow: #ffc107;
--green: #28a745;
--teal: #20c997;
--cyan: #17a2b8;
--white: #fff;
--gray: #6c757d;
--gray-dark: #343a40;
--primary: #007bff;
--secondary: #6c757d;
--success: #28a745;
--info: #17a2b8;
--warning: #ffc107;
--danger: #dc3545;
--light: #f8f9fa;
--dark: #343a40;
--breakpoint-xs: 0;
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--font-family-monospace: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
color: -internal-root-color;
}
body {
display: block;
margin: 8px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
.bg-white {
background-color: #fff!important;
}
.bg-light {
background-color: #f8f9fa!important;
}
.bg-purple {
background-color: var(--purple);
}
.text-white-50 {
color: rgba(255,255,255,.5)!important;
}
.mb-0, .my-0 {
margin-bottom: 0!important;
}
.mb-2, .my-2 {
margin-bottom: .5rem!important;
}
.mt-2, .my-2 {
margin-top: .5rem!important;
}
.pb-2, .py-2 {
padding-bottom: .5rem!important;
}
.p-3 {
padding: 1rem!important;
}
.mb-3, .my-3 {
margin-bottom: 1rem!important;
}
.mt-3, .my-3 {
margin-top: 1rem!important;
}
.mb-4, .my-4 {
margin-bottom: 1.5rem!important;
}
.shadow {
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
}
.align-items-center {
-ms-flex-align: center!important;
align-items: center!important;
}
.border-bottom {
border-bottom: 1px solid #dee2e6!important;
}.rounded {
border-radius: .25rem!important;
}
div {
display: block;
}
.container {
width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
}
article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
main {
display: block;
}
.text-white {
color: #fff!important;
}
.text-right {
text-align: right!important;
}
.text-center {
text-align: center!important;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
margin-bottom: .5rem;
font-weight: 500;
line-height: 1.2;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
.h6, h6 {
font-size: 1rem;
}
.row {
display: -ms-flexbox;
display: flex;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.d-flex {
display: -ms-flexbox!important;
display: flex!important;
}
.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col-2 {
-ms-flex: 0 0 16.666667%;
flex: 0 0 16.666667%;
max-width: 16.666667%;
}
.col-3 {
-ms-flex: 0 0 25%;
flex: 0 0 25%;
max-width: 25%;
}
.col-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-5 {
-ms-flex: 0 0 41.666667%;
flex: 0 0 41.666667%;
max-width: 41.666667%;
}
.col-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
.col-8 {
-ms-flex: 0 0 66.666667%;
flex: 0 0 66.666667%;
max-width: 66.666667%;
}
.col-9 {
-ms-flex: 0 0 75%;
flex: 0 0 75%;
max-width: 75%;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
}
.btn {
display: inline-block;
font-weight: 400;
color: #212529;
text-align: center;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: .25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.form-group {
margin-bottom: 1rem;
}
.form-control {
display: block;
width: 100%;
height: calc(1.5em + .75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.form-control:disabled, .form-control[readonly] {
background-color: #e9ecef;
opacity: 1;
}
input:not([type="image" i]) {
box-sizing: border-box;
}
button, input {
overflow: visible;
}
button, input, optgroup, select, textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
input[type="radio"], input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
border: 0;
border-top: 1px solid rgba(0,0,0,.1);
box-sizing: content-box;
height: 0;
overflow: visible;
}
@media (min-width: 576px) {
.container, .container-sm {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container, .container-md, .container-sm {
max-width: 720px;
}
.col-md-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.col-md-6 {
-ms-flex: 0 0 50%;
flex: 0 0 50%;
max-width: 50%;
}
}
@media (min-width: 992px) {
.container, .container-lg, .container-md, .container-sm {
max-width: 960px;
}
.col-lg-4 {
-ms-flex: 0 0 33.333333%;
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
}
@media (min-width: 1200px) {
.container, .container-lg, .container-md, .container-sm, .container-xl {
max-width: 1140px;
}
}
*, ::after, ::before {
box-sizing: border-box;
}

184
web/configuration.html Normal file
View File

@@ -0,0 +1,184 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMS reader - configuration</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="stylesheet" type="text/css" href="boot.css"/>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<style>
.bg-purple {
background-color: var(--purple);
}
</style>
</head>
<body class="bg-light">
<main role="main" class="container">
<div class="d-flex align-items-center p-3 my-2 text-white-50 bg-purple rounded shadow">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">AMS reader - configuration</h6>
<small>${version}</small>
</div>
</div>
<form method="post" action="/save">
<div class="row">
<div class="col-md-6 col-lg-4">
<div class="my-2 p-3 bg-white rounded shadow">
<h6 class="border-bottom border-gray pb-2 mb-4">WiFi</h6>
<div class="row form-group">
<label class="col-3">SSID</label>
<div class="col-9">
<input type="text" class="form-control" name="ssid" value="${config.ssid}"/>
</div>
</div>
<div class="row form-group">
<label class="col-3">Password</label>
<div class="col-9">
<input type="password" class="form-control" name="ssidPassword" value="${config.ssidPassword}"/>
</div>
</div>
</div>
<div class="my-3 p-3 bg-white rounded shadow">
<h6 class="border-bottom border-gray pb-2 mb-4">AMS meter</h6>
<div class="row form-group">
<label class="col-6">Distribution system</label>
<div class="col-6">
<select class="form-control" name="distSys">
<option value="" ${config.distSys0}></option>
<option value="1" ${config.distSys1}>IT (230V)</option>
<option value="2" ${config.distSys2}>TN (400V)</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-6">Meter type</label>
<div class="col-6">
<select class="form-control" name="meterType">
<option value="0" ${config.meterType0} disabled></option>
<option value="1" ${config.meterType1}>Kaifa</option>
<option value="2" ${config.meterType2}>Aidon</option>
<option value="3" ${config.meterType3}>Kamstrup</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-6">Main fuse</label>
<div class="col-6">
<select class="form-control" name="fuseSize">
<option value="" ${config.fuseSize0}></option>
<option value="25" ${config.fuseSize25}>25A</option>
<option value="32" ${config.fuseSize32}>32A</option>
<option value="35" ${config.fuseSize32}>35A</option>
<option value="40" ${config.fuseSize40}>40A</option>
<option value="50" ${config.fuseSize50}>50A</option>
<option value="63" ${config.fuseSize63}>63A</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="my-2 p-3 bg-white rounded shadow">
<h6 class="border-bottom border-gray pb-2 mb-4">MQTT</h6>
<div class="row form-group">
<label class="col-4">Enable</label>
<div class="col-8">
<input id="mqttEnable" type="checkbox" name="mqtt" value="true" ${config.mqtt}/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Hostname</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttHost" value="${config.mqttHost}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Port</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttPort" value="${config.mqttPort}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Client ID</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttClientID" value="${config.mqttClientID}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Topic</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttPublishTopic" value="${config.mqttPublishTopic}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Username</label>
<div class="col-8">
<input type="text" class="form-control mqtt-config" name="mqttUser" value="${config.mqttUser}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Password</label>
<div class="col-8">
<input type="password" class="form-control mqtt-config" name="mqttPass" value="${config.mqttPass}"/>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="my-2 p-3 bg-white rounded shadow">
<h6 class="border-bottom border-gray pb-2 mb-4">Web server</h6>
<div class="row form-group">
<label class="col-4">Security</label>
<div class="col-8">
<select id="authSecurity" class="form-control" name="authSecurity">
<option value="0" ${config.authSecurity0}>None</option>
<option value="1" ${config.authSecurity1}>Only configuration</option>
<option value="2" ${config.authSecurity2}>Everything</option>
</select>
</div>
</div>
<div class="row form-group">
<label class="col-4">Username</label>
<div class="col-8">
<input type="text" class="form-control auth-config" name="authUser" value="${config.authUser}"/>
</div>
</div>
<div class="row form-group">
<label class="col-4">Password</label>
<div class="col-8">
<input type="password" class="form-control auth-config" name="authPass" value="${config.authPass}"/>
</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="/" class="btn btn-outline-secondary">Back</a>
</div>
<div class="col-6 text-right">
<button class="btn btn-primary">Save</button>
</div>
</div>
</form>
</main>
<script>
$('#mqttEnable').on('change', function() {
var inputs = $('.mqtt-config');
inputs.prop('disabled', !$(this).is(':checked'));
});
$('#authSecurity').on('change', function() {
var inputs = $('.auth-config');
inputs.prop('disabled', $(this).val() == 0);
});
$(function() {
$('#mqttEnable').trigger('change');
$('#authSecurity').trigger('change');
});
</script>
</body>
</html>

275
web/gaugemeter.js Normal file
View File

@@ -0,0 +1,275 @@
/*
* 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")),
"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);

199
web/index.html Normal file
View File

@@ -0,0 +1,199 @@
<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" type="text/css" href="boot.css"/>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"/>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="gaugemeter.js"></script>
<style>
.bg-purple {
background-color: var(--purple);
}
.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;
}
</style>
</head>
<body class="bg-light">
<main role="main" class="container">
<div class="d-flex align-items-center p-3 my-2 text-white-50 bg-purple rounded shadow">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">AMS reader</h6>
<small>${version}</small>
</div>
</div>
<div class="my-3 p-3 bg-white rounded shadow">
<h6 class="border-bottom border-gray pb-2 mb-4">Current meter values</h6>
<div class="row">
<div class="col-md-4">
<div class="text-center">
<div id="P" class="SimpleMeter">
${data.P} W
</div>
<div class="GaugeMeter rounded"
style="display: none;"
data-size="200px"
data-text_size="0.11"
data-width="25"
data-style="Arch"
data-theme="Green-Gold-Red"
data-animationstep="0"
data-animate_gauge_colors="1"
data-percent="0"
data-text="-"
data-label="Consumption"
data-append="W"
></div>
</div>
</div>
<div class="col-md-4">
<div id="P1" class="row" style="display: ${display.P1}">
<div class="col-2">P1</div>
<div class="col-5 text-right"><span id="U1">${data.U1}</span> V</div>
<div class="col-5 text-right"><span id="I1">${data.I1}</span> A</div>
</div>
<div id="P2" class="row" style="display: ${display.P2}">
<div class="col-2">P2</div>
<div class="col-5 text-right"><span id="U2">${data.U2}</span> V</div>
<div class="col-5 text-right"><span id="I2">${data.I2}</span> A</div>
</div>
<div id="P3" class="row" style="display: ${display.P3}">
<div class="col-2">P3</div>
<div class="col-5 text-right"><span id="U3">${data.U3}</span> V</div>
<div class="col-5 text-right"><span id="I3">${data.I3}</span> A</div>
</div>
</div>
</div>
</div>
<hr/>
<div class="row form-group">
<div class="col-6">
<a href="https://github.com/gskjold/AmsToMqttBridge/releases" class="btn btn-outline-secondary">Release notes</a>
</div>
<div class="col-6 text-right">
<a href="configuration" class="btn btn-primary">Configuration</a>
</div>
</div>
</main>
<script>
$(".GaugeMeter").gaugeMeter();
var wait = 500;
var nextrefresh = wait;
var fetch = function() {
$.ajax({
url: '/data.json',
dataType: 'json',
}).done(function(json) {
$(".SimpleMeter").hide();
var el = $(".GaugeMeter");
el.show();
var rate = 2500;
if(json.data) {
el.data('percent', json.pct);
if(json.data.P) {
var num = parseFloat(json.data.P);
if(num > 1000) {
num = num / 1000;
el.data('text', num.toFixed(1));
el.data('append','kW');
} else {
el.data('text', num);
el.data('append','W');
}
}
el.gaugeMeter();
for(var id in json.data) {
var str = json.data[id];
if(isNaN(str)) {
$('#'+id).html(str);
} else {
var num = parseFloat(str);
$('#'+id).html(num.toFixed(1));
}
}
if(json.data.U1 > 0) {
$('#P1').show();
}
if(json.data.U2 > 0) {
$('#P2').show();
}
if(json.data.U3 > 0) {
$('#P3').show();
}
if(json.meterType == 3) {
rate = 10000;
}
if(json.currentMillis && json.up) {
nextrefresh = rate - ((json.currentMillis - json.up) % rate) + wait;
} else {
nextrefresh = 2500;
}
} else {
el.data('percent', 0);
el.data('text', '-');
el.gaugeMeter();
nextrefresh = 2500;
}
if(!nextrefresh || nextrefresh < 500) {
nextrefresh = 2500;
}
setTimeout(fetch, nextrefresh);
}).fail(function() {
el.data('percent', 0);
el.data('text', '-');
el.gaugeMeter();
nextrefresh = 10000;
setTimeout(fetch, nextrefresh);
});
}
setTimeout(fetch, nextrefresh);
</script>
</body>
</html>

BIN
webui.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB