mirror of
https://github.com/UtilitechAS/amsreader-firmware.git
synced 2026-04-19 09:29:39 +00:00
More v2.2
This commit is contained in:
@@ -70,6 +70,9 @@ public:
|
||||
bool isThreePhase();
|
||||
bool isTwoPhase();
|
||||
|
||||
int8_t getLastError();
|
||||
void setLastError(int8_t);
|
||||
|
||||
protected:
|
||||
unsigned long lastUpdateMillis = 0;
|
||||
unsigned long lastList2 = 0;
|
||||
@@ -84,6 +87,8 @@ protected:
|
||||
float powerFactor = 0, l1PowerFactor = 0, l2PowerFactor = 0, l3PowerFactor = 0;
|
||||
double activeImportCounter = 0, reactiveImportCounter = 0, activeExportCounter = 0, reactiveExportCounter = 0;
|
||||
bool threePhase = false, twoPhase = false, counterEstimated = false;
|
||||
|
||||
int8_t lastError = 0x00;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -214,3 +214,11 @@ bool AmsData::isThreePhase() {
|
||||
bool AmsData::isTwoPhase() {
|
||||
return this->twoPhase;
|
||||
}
|
||||
|
||||
int8_t AmsData::getLastError() {
|
||||
return lastError;
|
||||
}
|
||||
|
||||
void AmsData::setLastError(int8_t lastError) {
|
||||
this->lastError = lastError;
|
||||
}
|
||||
@@ -40,17 +40,20 @@
|
||||
"h" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
},
|
||||
"d" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
},
|
||||
"m" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
}
|
||||
},
|
||||
"c" : %u
|
||||
|
||||
@@ -776,12 +776,15 @@ void AmsWebServer::dataJson() {
|
||||
ea->getUseThisHour(),
|
||||
ea->getCostThisHour(),
|
||||
ea->getProducedThisHour(),
|
||||
ea->getIncomeThisHour(),
|
||||
ea->getUseToday(),
|
||||
ea->getCostToday(),
|
||||
ea->getProducedToday(),
|
||||
ea->getIncomeToday(),
|
||||
ea->getUseThisMonth(),
|
||||
ea->getCostThisMonth(),
|
||||
ea->getProducedThisMonth(),
|
||||
ea->getIncomeThisMonth(),
|
||||
(uint32_t) time(nullptr)
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,18 @@ struct EnergyAccountingPeak {
|
||||
};
|
||||
|
||||
struct EnergyAccountingData {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t costYesterday;
|
||||
uint16_t costThisMonth;
|
||||
uint16_t costLastMonth;
|
||||
uint16_t incomeYesterday;
|
||||
uint16_t incomeThisMonth;
|
||||
uint16_t incomeLastMonth;
|
||||
EnergyAccountingPeak peaks[5];
|
||||
};
|
||||
|
||||
struct EnergyAccountingData4 {
|
||||
uint8_t version;
|
||||
uint8_t month;
|
||||
uint16_t costYesterday;
|
||||
@@ -55,6 +67,12 @@ public:
|
||||
double getCostThisMonth();
|
||||
uint16_t getCostLastMonth();
|
||||
|
||||
double getIncomeThisHour();
|
||||
double getIncomeToday();
|
||||
double getIncomeYesterday();
|
||||
double getIncomeThisMonth();
|
||||
uint16_t getIncomeLastMonth();
|
||||
|
||||
float getMonthMax();
|
||||
uint8_t getCurrentThreshold();
|
||||
EnergyAccountingPeak getPeak(uint8_t);
|
||||
@@ -72,7 +90,7 @@ private:
|
||||
Timezone *tz = NULL;
|
||||
uint8_t currentHour = 0, currentDay = 0, currentThresholdIdx = 0;
|
||||
double use, costHour, costDay;
|
||||
double produce;
|
||||
double produce, incomeHour, incomeDay;
|
||||
EnergyAccountingData data = { 0, 0, 0, 0, 0, 0 };
|
||||
|
||||
void calcDayCost();
|
||||
|
||||
@@ -45,8 +45,9 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Initializing data at %lld\n", (int64_t) now);
|
||||
if(!load()) {
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) Unable to load existing data\n");
|
||||
data = { 4, local.Month,
|
||||
0, 0, 0,
|
||||
data = { 5, local.Month,
|
||||
0, 0, 0, // Cost
|
||||
0, 0, 0, // Income
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
@@ -58,6 +59,7 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
debugger->printf("(EnergyAccounting) Peak hour from day %d: %d\n", data.peaks[i].day, data.peaks[i].value*10);
|
||||
}
|
||||
debugger->printf("(EnergyAccounting) Loaded cost yesterday: %.2f, this month: %d, last month: %d\n", data.costYesterday / 10.0, data.costThisMonth, data.costLastMonth);
|
||||
debugger->printf("(EnergyAccounting) Loaded income yesterday: %.2f, this month: %d, last month: %d\n", data.incomeYesterday / 10.0, data.incomeThisMonth, data.incomeLastMonth);
|
||||
}
|
||||
init = true;
|
||||
}
|
||||
@@ -70,11 +72,14 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
if(local.Hour != currentHour && (amsData->getListType() >= 3 || local.Minute == 1)) {
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New local hour %d\n", local.Hour);
|
||||
|
||||
tmElements_t oneHrAgo;
|
||||
tmElements_t oneHrAgo, oneHrAgoLocal;
|
||||
breakTime(now-3600, oneHrAgo);
|
||||
uint16_t val = ds->getHourImport(oneHrAgo.Hour) / 10;
|
||||
ret |= updateMax(val, local.Day);
|
||||
|
||||
breakTime(tz->toLocal(now-3600), oneHrAgoLocal);
|
||||
ret |= updateMax(val, oneHrAgoLocal.Day);
|
||||
|
||||
currentHour = local.Hour; // Need to be defined here so that day cost is correctly calculated
|
||||
if(local.Hour > 0) {
|
||||
calcDayCost();
|
||||
}
|
||||
@@ -82,13 +87,18 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
use = 0;
|
||||
produce = 0;
|
||||
costHour = 0;
|
||||
currentHour = local.Hour;
|
||||
incomeHour = 0;
|
||||
|
||||
if(local.Day != currentDay) {
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New day %d\n", local.Day);
|
||||
data.costYesterday = costDay * 10;
|
||||
data.costThisMonth += costDay;
|
||||
costDay = 0;
|
||||
|
||||
data.incomeYesterday = incomeDay * 10;
|
||||
data.incomeThisMonth += incomeDay;
|
||||
incomeDay = 0;
|
||||
|
||||
currentDay = local.Day;
|
||||
ret = true;
|
||||
}
|
||||
@@ -97,6 +107,8 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
if(debugger->isActive(RemoteDebug::INFO)) debugger->printf("(EnergyAccounting) New month %d\n", local.Month);
|
||||
data.costLastMonth = data.costThisMonth;
|
||||
data.costThisMonth = 0;
|
||||
data.incomeLastMonth = data.incomeThisMonth;
|
||||
data.incomeThisMonth = 0;
|
||||
for(uint8_t i = 0; i < 5; i++) {
|
||||
data.peaks[i] = { 0, 0 };
|
||||
}
|
||||
@@ -124,6 +136,13 @@ bool EnergyAccounting::update(AmsData* amsData) {
|
||||
if(kwhe > 0) {
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) Adding %.4f kWh export\n", kwhe);
|
||||
produce += kwhe;
|
||||
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
|
||||
float price = eapi->getValueForHour(0);
|
||||
float income = price * kwhe;
|
||||
if(debugger->isActive(RemoteDebug::VERBOSE)) debugger->printf("(EnergyAccounting) and %.4f %s\n", income / 100.0, eapi->getCurrency());
|
||||
incomeHour += income;
|
||||
incomeDay += income;
|
||||
}
|
||||
}
|
||||
|
||||
if(config != NULL) {
|
||||
@@ -141,13 +160,20 @@ void EnergyAccounting::calcDayCost() {
|
||||
breakTime(tz->toLocal(now), local);
|
||||
|
||||
if(eapi != NULL && eapi->getValueForHour(0) != ENTSOE_NO_VALUE) {
|
||||
if(initPrice) costDay = 0;
|
||||
if(initPrice) {
|
||||
costDay = 0;
|
||||
incomeDay = 0;
|
||||
}
|
||||
for(int i = 0; i < currentHour; i++) {
|
||||
float price = eapi->getValueForHour(i - currentHour);
|
||||
float price = eapi->getValueForHour(i - local.Hour);
|
||||
if(price == ENTSOE_NO_VALUE) break;
|
||||
breakTime(now - ((currentHour - i) * 3600), utc);
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
|
||||
int16_t wh = ds->getHourImport(utc.Hour);
|
||||
costDay += price * (wh / 1000.0);
|
||||
|
||||
wh = ds->getHourExport(utc.Hour);
|
||||
incomeDay += price * (wh / 1000.0);
|
||||
}
|
||||
initPrice = true;
|
||||
}
|
||||
@@ -161,9 +187,10 @@ double EnergyAccounting::getUseToday() {
|
||||
float ret = 0.0;
|
||||
time_t now = time(nullptr);
|
||||
if(now < BUILD_EPOCH) return 0;
|
||||
tmElements_t utc;
|
||||
tmElements_t utc, local;
|
||||
breakTime(tz->toLocal(now), local);
|
||||
for(int i = 0; i < currentHour; i++) {
|
||||
breakTime(now - ((currentHour - i) * 3600), utc);
|
||||
breakTime(now - ((local.Hour - i) * 3600), utc);
|
||||
ret += ds->getHourImport(utc.Hour) / 1000.0;
|
||||
}
|
||||
return ret + getUseThisHour();
|
||||
@@ -226,6 +253,26 @@ uint16_t EnergyAccounting::getCostLastMonth() {
|
||||
return data.costLastMonth;
|
||||
}
|
||||
|
||||
double EnergyAccounting::getIncomeThisHour() {
|
||||
return incomeHour;
|
||||
}
|
||||
|
||||
double EnergyAccounting::getIncomeToday() {
|
||||
return incomeDay;
|
||||
}
|
||||
|
||||
double EnergyAccounting::getIncomeYesterday() {
|
||||
return data.incomeYesterday / 10.0;
|
||||
}
|
||||
|
||||
double EnergyAccounting::getIncomeThisMonth() {
|
||||
return data.incomeThisMonth + getIncomeToday();
|
||||
}
|
||||
|
||||
uint16_t EnergyAccounting::getIncomeLastMonth() {
|
||||
return data.incomeLastMonth;
|
||||
}
|
||||
|
||||
uint8_t EnergyAccounting::getCurrentThreshold() {
|
||||
if(config == NULL)
|
||||
return 0;
|
||||
@@ -309,14 +356,27 @@ bool EnergyAccounting::load() {
|
||||
file.readBytes(buf, file.size());
|
||||
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("(EnergyAccounting) Data version %d\n", buf[0]);
|
||||
if(buf[0] == 4) {
|
||||
if(buf[0] == 5) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
memcpy(&this->data, data, sizeof(this->data));
|
||||
ret = true;
|
||||
} else if(buf[0] == 4) {
|
||||
EnergyAccountingData4* data = (EnergyAccountingData4*) buf;
|
||||
this->data = { 5, data->month,
|
||||
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
|
||||
0,0,0, // Income from production
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
data->peaks[3].day, data->peaks[3].value,
|
||||
data->peaks[4].day, data->peaks[4].value
|
||||
};
|
||||
ret = true;
|
||||
} else if(buf[0] == 3) {
|
||||
EnergyAccountingData* data = (EnergyAccountingData*) buf;
|
||||
this->data = { 4, data->month,
|
||||
this->data = { 5, data->month,
|
||||
(uint16_t) (data->costYesterday / 10), (uint16_t) (data->costThisMonth / 100), (uint16_t) (data->costLastMonth / 100),
|
||||
0,0,0, // Income from production
|
||||
data->peaks[0].day, data->peaks[0].value,
|
||||
data->peaks[1].day, data->peaks[1].value,
|
||||
data->peaks[2].day, data->peaks[2].value,
|
||||
@@ -325,8 +385,9 @@ bool EnergyAccounting::load() {
|
||||
};
|
||||
ret = true;
|
||||
} else {
|
||||
data = { 4, 0,
|
||||
0, 0, 0,
|
||||
data = { 5, 0,
|
||||
0, 0, 0, // Cost
|
||||
0,0,0, // Income from production
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<div class="container mx-auto m-3">
|
||||
<Router>
|
||||
<Header data={data} sysinfo={sysinfo}/>
|
||||
<Header data={data}/>
|
||||
<Route path="/">
|
||||
<Dashboard data={data}/>
|
||||
</Route>
|
||||
|
||||
@@ -25,15 +25,15 @@
|
||||
{#if hasExport}
|
||||
<div class="flex">
|
||||
<div>Hour</div>
|
||||
<div class="flex-auto text-right">{data.h.p ? data.h.p.toFixed(2) : '-'} kWh {#if currency}/ {data.h.pc ? data.h.pc.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
<div class="flex-auto text-right">{data.h.p ? data.h.p.toFixed(2) : '-'} kWh {#if currency}/ {data.h.i ? data.h.i.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div>Day</div>
|
||||
<div class="flex-auto text-right">{data.d.p ? data.d.p.toFixed(1) : '-'} kWh {#if currency}/ {data.d.pc ? data.d.pc.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
<div class="flex-auto text-right">{data.d.p ? data.d.p.toFixed(1) : '-'} kWh {#if currency}/ {data.d.i ? data.d.i.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div>Month</div>
|
||||
<div class="flex-auto text-right">{data.m.p ? data.m.p.toFixed(0) : '-'} kWh {#if currency}/ {data.m.pc ? data.m.pc.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
<div class="flex-auto text-right">{data.m.p ? data.m.p.toFixed(0) : '-'} kWh {#if currency}/ {data.m.i ? data.m.i.toFixed(2) : '-'} {currency}{/if}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="cnt">
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<div>
|
||||
Below are some stuff we need to know
|
||||
Various permissions we need to do stuff:
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="my-3">
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script>
|
||||
import Mask from "./Mask.svelte";
|
||||
|
||||
export let action;
|
||||
export let title;
|
||||
|
||||
let uploading = false;
|
||||
</script>
|
||||
|
||||
<div class="grid xl:grid-cols-4 lg:grid-cols-2 md:grid-cols-2">
|
||||
<div class="cnt">
|
||||
<strong>Upload {title}</strong>
|
||||
<p class="mb-4">Select a suitable file and click upload</p>
|
||||
<form action="{action}" enctype="multipart/form-data" method="post">
|
||||
<form action="{action}" enctype="multipart/form-data" method="post" on:submit={() => uploading=true}>
|
||||
<input name="file" type="file">
|
||||
<div class="w-full text-right mt-4">
|
||||
<button type="submit" class="btn-pri">Upload</button>
|
||||
@@ -16,3 +19,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Mask active={uploading} message="Uploading file, please wait"/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Link } from "svelte-navigator";
|
||||
import { sysinfoStore, getGitHubReleases, gitHubReleaseStore } from './DataStores.js';
|
||||
import { upgrade, getNextVersion } from './UpgradeHelper';
|
||||
import { boardtype } from './Helpers.js';
|
||||
import { boardtype, hanError, mqttError } from './Helpers.js';
|
||||
import GitHubLogo from './../assets/github.svg';
|
||||
import Uptime from "./Uptime.svelte";
|
||||
import Badge from './Badge.svelte';
|
||||
@@ -13,7 +13,7 @@
|
||||
import DownloadIcon from "./DownloadIcon.svelte";
|
||||
|
||||
export let data = {}
|
||||
export let sysinfo = {}
|
||||
let sysinfo = {}
|
||||
|
||||
let nextVersion = {};
|
||||
|
||||
@@ -28,11 +28,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
sysinfoStore.subscribe(update => {
|
||||
sysinfo = update;
|
||||
if(update.fwconsent === 1) {
|
||||
getGitHubReleases();
|
||||
}
|
||||
});
|
||||
|
||||
gitHubReleaseStore.subscribe(releases => {
|
||||
nextVersion = getNextVersion(sysinfo.version, releases);
|
||||
});
|
||||
getGitHubReleases();
|
||||
</script>
|
||||
|
||||
<nav class="bg-violet-600 p-1 rounded-md mx-2">
|
||||
@@ -47,13 +52,19 @@
|
||||
{/if}
|
||||
<div class="flex-none my-auto">Free mem: {data.m ? (data.m/1000).toFixed(1) : '-'}kb</div>
|
||||
</div>
|
||||
<div class="flex-auto my-auto justify-center p-2">
|
||||
<div class="flex-auto flex-wrap my-auto justify-center p-2">
|
||||
<Badge title="ESP" text={sysinfo.booting ? 'Booting' : data.v > 2.0 ? data.v.toFixed(2)+"V" : "ESP"} color={sysinfo.booting ? 'yellow' : data.em === 1 ? 'green' : data.em === 2 ? 'yellow' : data.em === 3 ? 'red' : 'gray'}/>
|
||||
<Badge title="HAN" text="HAN" color={sysinfo.booting ? 'gray' : data.hm === 1 ? 'green' : data.hm === 2 ? 'yellow' : data.hm === 3 ? 'red' : 'gray'}/>
|
||||
<Badge title="WiFi" text={data.r ? data.r.toFixed(0)+"dBm" : "WiFi"} color={sysinfo.booting ? 'gray' : data.wm === 1 ? 'green' : data.wm === 2 ? 'yellow' : data.wm === 3 ? 'red' : 'gray'}/>
|
||||
<Badge title="MQTT" text="MQTT" color={sysinfo.booting ? 'gray' : data.mm === 1 ? 'green' : data.mm === 2 ? 'yellow' : data.mm === 3 ? 'red' : 'gray'}/>
|
||||
</div>
|
||||
<div class="flex-auto p-2 flex flex-row-reverse flex-wrap">
|
||||
{#if data.he < 0}
|
||||
<div class="bd-red">{ 'HAN error: ' + hanError(data.he) }</div>
|
||||
{/if}
|
||||
{#if data.me < 0}
|
||||
<div class="bd-red">{ 'MQTT error: ' + mqttError(data.me) }</div>
|
||||
{/if}
|
||||
<div class="flex-auto p-2 flex flex-row-reverse flex-wrap">
|
||||
<div class="flex-none">
|
||||
<a class="float-right" href='https://github.com/gskjold/AmsToMqttBridge' target='_blank' rel="noreferrer" aria-label="GitHub"><img class="gh-logo" src={GitHubLogo} alt="GitHub repo"/></a>
|
||||
</div>
|
||||
|
||||
@@ -91,3 +91,33 @@ export function boardtype(c, b) {
|
||||
return "Generic ESP8266";
|
||||
}
|
||||
}
|
||||
|
||||
export function hanError(err) {
|
||||
switch(err) {
|
||||
case -1: return "Parse error";
|
||||
case -2: return "Incomplete data received";
|
||||
case -3: return "Payload boundry flag missing";
|
||||
case -4: return "Header checksum error";
|
||||
case -5: return "Footer checksum error";
|
||||
case -9: return "Unknown data received, check meter config";
|
||||
case -41: return "Frame length not equal";
|
||||
case -51: return "Authentication failed";
|
||||
case -52: return "Decryption failed";
|
||||
case -53: return "Encryption key invalid";
|
||||
}
|
||||
if(err < 0) return "Unspecified error "+err;
|
||||
return "";
|
||||
}
|
||||
|
||||
export function mqttError(err) {
|
||||
switch(err) {
|
||||
case -3: return "Connection failed";
|
||||
case -4: return "Network timeout";
|
||||
case -10: return "Connection denied";
|
||||
case -11: return "Failed to subscribe";
|
||||
case -13: return "Connection lost";
|
||||
}
|
||||
|
||||
if(err < 0) return "Unspecified error "+err;
|
||||
return "";
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
import { upgrade, getNextVersion } from './UpgradeHelper';
|
||||
import DownloadIcon from './DownloadIcon.svelte';
|
||||
import { Link } from 'svelte-navigator';
|
||||
import Mask from './Mask.svelte';
|
||||
|
||||
export let sysinfo;
|
||||
|
||||
@@ -45,6 +46,8 @@
|
||||
}
|
||||
|
||||
let fileinput;
|
||||
let files = [];
|
||||
let uploading = false;
|
||||
getSysinfo();
|
||||
</script>
|
||||
|
||||
@@ -125,15 +128,16 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="my-2 flex">
|
||||
<form action="/firmware" enctype="multipart/form-data" method="post">
|
||||
<input style="display:none" name="file" type="file" accept=".bin" bind:this={fileinput}>
|
||||
{#if fileinput && fileinput.files.length == 0}
|
||||
<form action="/firmware" enctype="multipart/form-data" method="post" on:submit={() => uploading=true}>
|
||||
<input style="display:none" name="file" type="file" accept=".bin" bind:this={fileinput} bind:files={files}>
|
||||
{#if files.length == 0}
|
||||
<button type="button" on:click={()=>{fileinput.click();}} class="text-xs py-1 px-2 rounded bg-blue-500 text-white float-right mr-3">Select firmware file for upgrade</button>
|
||||
{:else if fileinput}
|
||||
{fileinput.files[0].name}
|
||||
{:else}
|
||||
{files[0].name}
|
||||
<button type="submit" class="ml-2 text-xs py-1 px-2 rounded bg-blue-500 text-white float-right mr-3">Upload</button>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Mask active={uploading} message="Uploading firmware, please wait"/>
|
||||
|
||||
@@ -17,18 +17,17 @@ export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
server: {
|
||||
proxy: {
|
||||
"/data.json": "http://192.168.233.244",
|
||||
"/energyprice.json": "http://192.168.233.244",
|
||||
"/dayplot.json": "http://192.168.233.244",
|
||||
"/monthplot.json": "http://192.168.233.244",
|
||||
"/temperature.json": "http://192.168.233.244",
|
||||
"/sysinfo.json": "http://192.168.233.244",
|
||||
"/configuration.json": "http://192.168.233.244",
|
||||
"/tariff.json": "http://192.168.233.244",
|
||||
"/save": "http://192.168.233.244",
|
||||
"/reboot": "http://192.168.233.244",
|
||||
"/firmware": "http://192.168.233.244",
|
||||
"/upgrade": "http://192.168.233.244"
|
||||
"/data.json": "http://192.168.233.229",
|
||||
"/energyprice.json": "http://192.168.233.229",
|
||||
"/dayplot.json": "http://192.168.233.229",
|
||||
"/monthplot.json": "http://192.168.233.229",
|
||||
"/temperature.json": "http://192.168.233.229",
|
||||
"/sysinfo.json": "http://192.168.233.229",
|
||||
"/configuration.json": "http://192.168.233.229",
|
||||
"/tariff.json": "http://192.168.233.229",
|
||||
"/save": "http://192.168.233.229",
|
||||
"/reboot": "http://192.168.233.229",
|
||||
"/upgrade": "http://192.168.233.229"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -83,7 +83,6 @@ private:
|
||||
void monthplotJson();
|
||||
void energyPriceJson();
|
||||
void temperatureJson();
|
||||
void wifiScanJson();
|
||||
void tariffJson();
|
||||
|
||||
void configurationJson();
|
||||
|
||||
5
lib/SvelteUi/json/conf_debug.json
Normal file
5
lib/SvelteUi/json/conf_debug.json
Normal file
@@ -0,0 +1,5 @@
|
||||
"d": {
|
||||
"s": %s,
|
||||
"t": %s,
|
||||
"l": %d
|
||||
},
|
||||
7
lib/SvelteUi/json/conf_general.json
Normal file
7
lib/SvelteUi/json/conf_general.json
Normal file
@@ -0,0 +1,7 @@
|
||||
"g": {
|
||||
"t": "%s",
|
||||
"h": "%s",
|
||||
"s": %d,
|
||||
"u": "%s",
|
||||
"p": "%s"
|
||||
},
|
||||
28
lib/SvelteUi/json/conf_gpio.json
Normal file
28
lib/SvelteUi/json/conf_gpio.json
Normal file
@@ -0,0 +1,28 @@
|
||||
"i": {
|
||||
"h": %s,
|
||||
"a": %s,
|
||||
"l": {
|
||||
"p": %s,
|
||||
"i": %s
|
||||
},
|
||||
"r": {
|
||||
"r": %s,
|
||||
"g": %s,
|
||||
"b": %s,
|
||||
"i": %s
|
||||
},
|
||||
"t": {
|
||||
"d": %s,
|
||||
"a": %s
|
||||
},
|
||||
"v": {
|
||||
"p": %s,
|
||||
"o": %.2f,
|
||||
"m": %.3f,
|
||||
"d": {
|
||||
"v": %d,
|
||||
"g": %d
|
||||
},
|
||||
"b": %.1f
|
||||
}
|
||||
}
|
||||
20
lib/SvelteUi/json/conf_meter.json
Normal file
20
lib/SvelteUi/json/conf_meter.json
Normal file
@@ -0,0 +1,20 @@
|
||||
"m": {
|
||||
"b": %d,
|
||||
"p": %d,
|
||||
"i": %s,
|
||||
"d": %d,
|
||||
"f": %d,
|
||||
"r": %d,
|
||||
"e": {
|
||||
"e": %s,
|
||||
"k": "%s",
|
||||
"a": "%s"
|
||||
},
|
||||
"m": {
|
||||
"e": %s,
|
||||
"w": %.3f,
|
||||
"v": %.3f,
|
||||
"a": %.3f,
|
||||
"c": %.3f
|
||||
}
|
||||
},
|
||||
15
lib/SvelteUi/json/conf_mqtt.json
Normal file
15
lib/SvelteUi/json/conf_mqtt.json
Normal file
@@ -0,0 +1,15 @@
|
||||
"q": {
|
||||
"h": "%s",
|
||||
"p": %d,
|
||||
"u": "%s",
|
||||
"a": "%s",
|
||||
"c": "%s",
|
||||
"b": "%s",
|
||||
"m": %d,
|
||||
"s": {
|
||||
"e": %s,
|
||||
"c": %s,
|
||||
"r": %s,
|
||||
"k": %s
|
||||
}
|
||||
},
|
||||
11
lib/SvelteUi/json/conf_net.json
Normal file
11
lib/SvelteUi/json/conf_net.json
Normal file
@@ -0,0 +1,11 @@
|
||||
"n": {
|
||||
"m": "%s",
|
||||
"i": "%s",
|
||||
"s": "%s",
|
||||
"g": "%s",
|
||||
"d1": "%s",
|
||||
"d2": "%s",
|
||||
"d": %s,
|
||||
"n1": "%s",
|
||||
"h": %s
|
||||
},
|
||||
7
lib/SvelteUi/json/conf_price.json
Normal file
7
lib/SvelteUi/json/conf_price.json
Normal file
@@ -0,0 +1,7 @@
|
||||
"p": {
|
||||
"e": %s,
|
||||
"t": "%s",
|
||||
"r": "%s",
|
||||
"c": "%s",
|
||||
"m": %.3f
|
||||
},
|
||||
15
lib/SvelteUi/json/conf_thresholds.json
Normal file
15
lib/SvelteUi/json/conf_thresholds.json
Normal file
@@ -0,0 +1,15 @@
|
||||
"t": {
|
||||
"t": [
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d
|
||||
],
|
||||
"h": %d
|
||||
},
|
||||
6
lib/SvelteUi/json/conf_wifi.json
Normal file
6
lib/SvelteUi/json/conf_wifi.json
Normal file
@@ -0,0 +1,6 @@
|
||||
"w": {
|
||||
"s": "%s",
|
||||
"p": "%s",
|
||||
"w": %.1f,
|
||||
"z": %d
|
||||
},
|
||||
@@ -40,19 +40,23 @@
|
||||
"h" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
},
|
||||
"d" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
},
|
||||
"m" : {
|
||||
"u" : %.2f,
|
||||
"c" : %.2f,
|
||||
"p" : %.2f
|
||||
"p" : %.2f,
|
||||
"i" : %.2f
|
||||
}
|
||||
},
|
||||
"pr" : "%s",
|
||||
"he" : %d,
|
||||
"c" : %lu
|
||||
}
|
||||
4
lib/SvelteUi/json/peak.json
Normal file
4
lib/SvelteUi/json/peak.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"d": %d,
|
||||
"v": %.2f
|
||||
}
|
||||
25
lib/SvelteUi/json/sysinfo.json
Normal file
25
lib/SvelteUi/json/sysinfo.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"version": "%s",
|
||||
"chip": "%s",
|
||||
"chipId": "%s",
|
||||
"mac": "%s",
|
||||
"board": %d,
|
||||
"vndcfg": %s,
|
||||
"usrcfg": %s,
|
||||
"fwconsent": %d,
|
||||
"hostname": "%s",
|
||||
"booting": %s,
|
||||
"upgrading": %s,
|
||||
"net": {
|
||||
"ip": "%s",
|
||||
"mask": "%s",
|
||||
"gw": "%s",
|
||||
"dns1": "%s",
|
||||
"dns2": "%s"
|
||||
},
|
||||
"meter": {
|
||||
"mfg": %d,
|
||||
"model": "%s",
|
||||
"id": "%s"
|
||||
}
|
||||
}
|
||||
17
lib/SvelteUi/json/tariff.json
Normal file
17
lib/SvelteUi/json/tariff.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"t": [
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d,
|
||||
%d
|
||||
],
|
||||
"p": [ %s ],
|
||||
"c": %d,
|
||||
"m": %.2f
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
#include "base64.h"
|
||||
#include "hexutils.h"
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#include "html/index_html.h"
|
||||
#include "html/index_css.h"
|
||||
#include "html/index_js.h"
|
||||
@@ -15,6 +13,18 @@
|
||||
#include "html/energyprice_json.h"
|
||||
#include "html/tempsensor_json.h"
|
||||
#include "html/response_json.h"
|
||||
#include "html/sysinfo_json.h"
|
||||
#include "html/tariff_json.h"
|
||||
#include "html/peak_json.h"
|
||||
#include "html/conf_general_json.h"
|
||||
#include "html/conf_meter_json.h"
|
||||
#include "html/conf_wifi_json.h"
|
||||
#include "html/conf_net_json.h"
|
||||
#include "html/conf_mqtt_json.h"
|
||||
#include "html/conf_price_json.h"
|
||||
#include "html/conf_thresholds_json.h"
|
||||
#include "html/conf_debug_json.h"
|
||||
#include "html/conf_gpio_json.h"
|
||||
|
||||
#include "version.h"
|
||||
|
||||
@@ -60,8 +70,6 @@ void AmsWebServer::setup(AmsConfiguration* config, GpioConfig* gpioConfig, Meter
|
||||
server.on(F("/temperature.json"), HTTP_GET, std::bind(&AmsWebServer::temperatureJson, this));
|
||||
server.on(F("/tariff.json"), HTTP_GET, std::bind(&AmsWebServer::tariffJson, this));
|
||||
|
||||
server.on(F("/wifiscan.json"), HTTP_GET, std::bind(&AmsWebServer::wifiScanJson, this));
|
||||
|
||||
server.on(F("/configuration.json"), HTTP_GET, std::bind(&AmsWebServer::configurationJson, this));
|
||||
server.on(F("/save"), HTTP_POST, std::bind(&AmsWebServer::handleSave, this));
|
||||
server.on(F("/reboot"), HTTP_POST, std::bind(&AmsWebServer::reboot, this));
|
||||
@@ -169,17 +177,8 @@ void AmsWebServer::faviconIco() {
|
||||
void AmsWebServer::sysinfoJson() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /sysinfo.json over http...\n");
|
||||
|
||||
DynamicJsonDocument doc(1024);
|
||||
doc[F("version")] = VERSION;
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
doc[F("chip")] = "esp32s2";
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
doc[F("chip")] = "esp32c3";
|
||||
#elif defined(ESP32)
|
||||
doc[F("chip")] = "esp32";
|
||||
#elif defined(ESP8266)
|
||||
doc[F("chip")] = "esp8266";
|
||||
#endif
|
||||
SystemConfig sys;
|
||||
config->getSystemConfig(sys);
|
||||
|
||||
uint32_t chipId;
|
||||
#if defined(ESP32)
|
||||
@@ -188,39 +187,54 @@ void AmsWebServer::sysinfoJson() {
|
||||
chipId = ESP.getChipId();
|
||||
#endif
|
||||
String chipIdStr = String(chipId, HEX);
|
||||
doc[F("chipId")] = chipIdStr;
|
||||
doc[F("mac")] = WiFi.macAddress();
|
||||
|
||||
SystemConfig sys;
|
||||
config->getSystemConfig(sys);
|
||||
doc[F("board")] = sys.boardType;
|
||||
doc[F("vndcfg")] = sys.vendorConfigured;
|
||||
doc[F("usrcfg")] = sys.userConfigured;
|
||||
doc[F("fwconsent")] = sys.dataCollectionConsent;
|
||||
doc[F("country")] = sys.country;
|
||||
|
||||
String hostname;
|
||||
if(sys.userConfigured) {
|
||||
WiFiConfig wifiConfig;
|
||||
config->getWiFiConfig(wifiConfig);
|
||||
doc[F("hostname")] = wifiConfig.hostname;
|
||||
hostname = String(wifiConfig.hostname);
|
||||
} else {
|
||||
doc[F("hostname")] = "ams-"+chipIdStr;
|
||||
hostname = "ams-"+chipIdStr;
|
||||
}
|
||||
|
||||
doc[F("booting")] = performRestart;
|
||||
doc[F("upgrading")] = rebootForUpgrade;
|
||||
IPAddress dns1 = WiFi.dnsIP(0);
|
||||
IPAddress dns2 = WiFi.dnsIP(1);
|
||||
|
||||
doc[F("net")][F("ip")] = WiFi.localIP().toString();
|
||||
doc[F("net")][F("mask")] = WiFi.subnetMask().toString();
|
||||
doc[F("net")][F("gw")] = WiFi.gatewayIP().toString();
|
||||
doc[F("net")][F("dns1")] = WiFi.dnsIP(0).toString();
|
||||
doc[F("net")][F("dns2")] = WiFi.dnsIP(1).toString();
|
||||
snprintf_P(buf, BufferSize, SYSINFO_JSON,
|
||||
VERSION,
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S2)
|
||||
"esp32s2",
|
||||
#elif defined(CONFIG_IDF_TARGET_ESP32C3)
|
||||
"esp32c3",
|
||||
#elif defined(ESP32)
|
||||
"esp32",
|
||||
#elif defined(ESP8266)
|
||||
"esp8266",
|
||||
#endif
|
||||
chipIdStr.c_str(),
|
||||
WiFi.macAddress().c_str(),
|
||||
sys.boardType,
|
||||
sys.vendorConfigured ? "true" : "false",
|
||||
sys.userConfigured ? "true" : "false",
|
||||
sys.dataCollectionConsent,
|
||||
hostname.c_str(),
|
||||
performRestart ? "true" : "false",
|
||||
rebootForUpgrade ? "true" : "false",
|
||||
WiFi.localIP().toString().c_str(),
|
||||
WiFi.subnetMask().toString().c_str(),
|
||||
WiFi.gatewayIP().toString().c_str(),
|
||||
dns1.isSet() ? dns1.toString().c_str() : "",
|
||||
dns2.isSet() ? dns2.toString().c_str() : "",
|
||||
meterState->getMeterType(),
|
||||
meterState->getMeterModel().c_str(),
|
||||
meterState->getMeterId().c_str()
|
||||
);
|
||||
|
||||
doc[F("meter")][F("mfg")] = meterState->getMeterType();
|
||||
doc[F("meter")][F("model")] = meterState->getMeterModel();
|
||||
doc[F("meter")][F("id")] = meterState->getMeterId();
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
serializeJson(doc, buf, BufferSize);
|
||||
server.setContentLength(strlen(buf));
|
||||
server.send(200, MIME_JSON, buf);
|
||||
|
||||
server.handleClient();
|
||||
@@ -276,7 +290,9 @@ void AmsWebServer::dataJson() {
|
||||
|
||||
|
||||
uint8_t hanStatus;
|
||||
if(meterConfig->baud == 0 || meterState->getLastUpdateMillis() == 0) {
|
||||
if(meterState->getLastError() < 0) {
|
||||
hanStatus = 3;
|
||||
} else if((meterConfig->baud == 0 || meterState->getLastUpdateMillis() == 0) && now < 15000) {
|
||||
hanStatus = 0;
|
||||
} else if(now - meterState->getLastUpdateMillis() < 15000) {
|
||||
hanStatus = 1;
|
||||
@@ -313,7 +329,7 @@ void AmsWebServer::dataJson() {
|
||||
String peaks = "";
|
||||
for(uint8_t i = 1; i <= ea->getConfig()->hours; i++) {
|
||||
if(!peaks.isEmpty()) peaks += ",";
|
||||
peaks += String(ea->getPeak(i).value);
|
||||
peaks += String(ea->getPeak(i).value / 100.0);
|
||||
}
|
||||
|
||||
snprintf_P(buf, BufferSize, DATA_JSON,
|
||||
@@ -357,13 +373,17 @@ void AmsWebServer::dataJson() {
|
||||
ea->getUseThisHour(),
|
||||
ea->getCostThisHour(),
|
||||
ea->getProducedThisHour(),
|
||||
ea->getIncomeThisHour(),
|
||||
ea->getUseToday(),
|
||||
ea->getCostToday(),
|
||||
ea->getProducedToday(),
|
||||
ea->getIncomeToday(),
|
||||
ea->getUseThisMonth(),
|
||||
ea->getCostThisMonth(),
|
||||
ea->getProducedThisMonth(),
|
||||
ea->getIncomeThisMonth(),
|
||||
eapi == NULL ? "" : eapi->getArea(),
|
||||
meterState->getLastError(),
|
||||
(uint32_t) time(nullptr)
|
||||
);
|
||||
|
||||
@@ -665,28 +685,13 @@ void AmsWebServer::indexJs() {
|
||||
void AmsWebServer::configurationJson() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /configuration.json over http...\n");
|
||||
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
DynamicJsonDocument doc(2048);
|
||||
doc[F("version")] = VERSION;
|
||||
|
||||
NtpConfig ntpConfig;
|
||||
config->getNtpConfig(ntpConfig);
|
||||
WiFiConfig wifiConfig;
|
||||
config->getWiFiConfig(wifiConfig);
|
||||
WebConfig webConfig;
|
||||
config->getWebConfig(webConfig);
|
||||
|
||||
doc[F("g")][F("t")] = ntpConfig.timezone;
|
||||
doc[F("g")][F("h")] = wifiConfig.hostname;
|
||||
doc[F("g")][F("s")] = webConfig.security;
|
||||
doc[F("g")][F("u")] = webConfig.username;
|
||||
doc[F("g")][F("p")] = strlen(webConfig.password) > 0 ? "***" : "";
|
||||
|
||||
bool encen = false;
|
||||
for(uint8_t i = 0; i < 16; i++) {
|
||||
@@ -695,166 +700,143 @@ void AmsWebServer::configurationJson() {
|
||||
}
|
||||
}
|
||||
|
||||
config->getMeterConfig(*meterConfig);
|
||||
doc[F("m")][F("b")] = meterConfig->baud;
|
||||
doc[F("m")][F("p")] = meterConfig->parity;
|
||||
doc[F("m")][F("i")] = meterConfig->invert;
|
||||
doc[F("m")][F("d")] = meterConfig->distributionSystem;
|
||||
doc[F("m")][F("f")] = meterConfig->mainFuse;
|
||||
doc[F("m")][F("r")] = meterConfig->productionCapacity;
|
||||
doc[F("m")][F("e")][F("e")] = encen;
|
||||
doc[F("m")][F("e")][F("k")] = toHex(meterConfig->encryptionKey, 16);
|
||||
doc[F("m")][F("e")][F("a")] = toHex(meterConfig->authenticationKey, 16);
|
||||
doc[F("m")][F("m")][F("e")] = meterConfig->wattageMultiplier > 1 || meterConfig->voltageMultiplier > 1 || meterConfig->amperageMultiplier > 1 || meterConfig->accumulatedMultiplier > 1;
|
||||
doc[F("m")][F("m")][F("w")] = meterConfig->wattageMultiplier / 1000.0;
|
||||
doc[F("m")][F("m")][F("v")] = meterConfig->voltageMultiplier / 1000.0;
|
||||
doc[F("m")][F("m")][F("a")] = meterConfig->amperageMultiplier / 1000.0;
|
||||
doc[F("m")][F("m")][F("c")] = meterConfig->accumulatedMultiplier / 1000.0;
|
||||
|
||||
EnergyAccountingConfig eac;
|
||||
config->getEnergyAccountingConfig(eac);
|
||||
doc[F("t")][F("t")][0] = eac.thresholds[0];
|
||||
doc[F("t")][F("t")][1] = eac.thresholds[1];
|
||||
doc[F("t")][F("t")][2] = eac.thresholds[2];
|
||||
doc[F("t")][F("t")][3] = eac.thresholds[3];
|
||||
doc[F("t")][F("t")][4] = eac.thresholds[4];
|
||||
doc[F("t")][F("t")][5] = eac.thresholds[5];
|
||||
doc[F("t")][F("t")][6] = eac.thresholds[6];
|
||||
doc[F("t")][F("t")][7] = eac.thresholds[7];
|
||||
doc[F("t")][F("t")][8] = eac.thresholds[8];
|
||||
doc[F("t")][F("t")][9] = eac.thresholds[9];
|
||||
doc[F("t")][F("h")] = eac.hours;
|
||||
|
||||
doc[F("w")][F("s")] = wifiConfig.ssid;
|
||||
doc[F("w")][F("p")] = strlen(wifiConfig.psk) > 0 ? "***" : "";
|
||||
doc[F("w")][F("w")] = wifiConfig.power / 10.0;
|
||||
doc[F("w")][F("z")] = wifiConfig.sleep;
|
||||
|
||||
doc[F("n")][F("m")] = strlen(wifiConfig.ip) > 0 ? "static" : "dhcp";
|
||||
doc[F("n")][F("i")] = wifiConfig.ip;
|
||||
doc[F("n")][F("s")] = wifiConfig.subnet;
|
||||
doc[F("n")][F("g")] = wifiConfig.gateway;
|
||||
doc[F("n")][F("d1")] = wifiConfig.dns1;
|
||||
doc[F("n")][F("d2")] = wifiConfig.dns2;
|
||||
doc[F("n")][F("d")] = wifiConfig.mdns;
|
||||
doc[F("n")][F("n1")] = ntpConfig.server;
|
||||
doc[F("n")][F("h")] = ntpConfig.dhcp;
|
||||
|
||||
EnergyAccountingConfig* eac = ea->getConfig();
|
||||
MqttConfig mqttConfig;
|
||||
config->getMqttConfig(mqttConfig);
|
||||
doc[F("q")][F("h")] = mqttConfig.host;
|
||||
doc[F("q")][F("p")] = mqttConfig.port;
|
||||
doc[F("q")][F("u")] = mqttConfig.username;
|
||||
doc[F("q")][F("a")] = strlen(mqttConfig.password) > 0 ? "***" : "";
|
||||
doc[F("q")][F("c")] = mqttConfig.clientId;
|
||||
doc[F("q")][F("b")] = mqttConfig.publishTopic;
|
||||
doc[F("q")][F("m")] = mqttConfig.payloadFormat;
|
||||
doc[F("q")][F("s")][F("e")] = mqttConfig.ssl;
|
||||
|
||||
if(LittleFS.begin()) {
|
||||
doc[F("q")][F("s")][F("c")] = LittleFS.exists(FILE_MQTT_CA);
|
||||
doc[F("q")][F("s")][F("r")] = LittleFS.exists(FILE_MQTT_CERT);
|
||||
doc[F("q")][F("s")][F("k")] = LittleFS.exists(FILE_MQTT_KEY);
|
||||
LittleFS.end();
|
||||
} else {
|
||||
doc[F("q")][F("s")][F("c")] = false;
|
||||
doc[F("q")][F("s")][F("r")] = false;
|
||||
doc[F("q")][F("s")][F("k")] = false;
|
||||
}
|
||||
|
||||
EntsoeConfig entsoe;
|
||||
config->getEntsoeConfig(entsoe);
|
||||
doc[F("p")][F("e")] = strlen(entsoe.token) > 0;
|
||||
doc[F("p")][F("t")] = entsoe.token;
|
||||
doc[F("p")][F("r")] = entsoe.area;
|
||||
doc[F("p")][F("c")] = entsoe.currency;
|
||||
doc[F("p")][F("m")] = entsoe.multiplier / 1000.0;
|
||||
|
||||
DebugConfig debugConfig;
|
||||
config->getDebugConfig(debugConfig);
|
||||
doc[F("d")][F("s")] = debugConfig.serial;
|
||||
doc[F("d")][F("t")] = debugConfig.telnet;
|
||||
doc[F("d")][F("l")] = debugConfig.level;
|
||||
|
||||
GpioConfig gpioConfig;
|
||||
config->getGpioConfig(gpioConfig);
|
||||
if(gpioConfig.hanPin == 0xff)
|
||||
doc[F("i")][F("h")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("h")] = gpioConfig.hanPin;
|
||||
|
||||
if(gpioConfig.apPin == 0xff)
|
||||
doc[F("i")][F("a")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("a")] = gpioConfig.apPin;
|
||||
|
||||
if(gpioConfig.ledPin == 0xff)
|
||||
doc[F("i")][F("l")][F("p")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("l")][F("p")] = gpioConfig.ledPin;
|
||||
|
||||
doc[F("i")][F("l")][F("i")] = gpioConfig.ledInverted;
|
||||
|
||||
if(gpioConfig.ledPinRed == 0xff)
|
||||
doc[F("i")][F("r")][F("r")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("r")][F("r")] = gpioConfig.ledPinRed;
|
||||
bool qsc = false;
|
||||
bool qsr = false;
|
||||
bool qsk = false;
|
||||
|
||||
if(gpioConfig.ledPinGreen == 0xff)
|
||||
doc[F("i")][F("r")][F("g")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("r")][F("g")] = gpioConfig.ledPinGreen;
|
||||
if(LittleFS.begin()) {
|
||||
qsc = LittleFS.exists(FILE_MQTT_CA);
|
||||
qsr = LittleFS.exists(FILE_MQTT_CERT);
|
||||
qsk = LittleFS.exists(FILE_MQTT_KEY);
|
||||
LittleFS.end();
|
||||
}
|
||||
|
||||
if(gpioConfig.ledPinBlue == 0xff)
|
||||
doc[F("i")][F("r")][F("b")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("r")][F("b")] = gpioConfig.ledPinBlue;
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
doc[F("i")][F("r")][F("i")] = gpioConfig.ledRgbInverted;
|
||||
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server.send_P(200, MIME_JSON, PSTR("{\"version\":\""));
|
||||
server.sendContent_P(VERSION);
|
||||
server.sendContent_P(PSTR("\","));
|
||||
snprintf_P(buf, BufferSize, CONF_GENERAL_JSON,
|
||||
ntpConfig.timezone,
|
||||
wifiConfig.hostname,
|
||||
webConfig.security,
|
||||
webConfig.username,
|
||||
strlen(webConfig.password) > 0 ? "***" : ""
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_METER_JSON,
|
||||
meterConfig->baud,
|
||||
meterConfig->parity,
|
||||
meterConfig->invert ? "true" : "false",
|
||||
meterConfig->distributionSystem,
|
||||
meterConfig->mainFuse,
|
||||
meterConfig->productionCapacity,
|
||||
encen ? "true" : "false",
|
||||
toHex(meterConfig->encryptionKey, 16).c_str(),
|
||||
toHex(meterConfig->authenticationKey, 16).c_str(),
|
||||
meterConfig->wattageMultiplier > 1 || meterConfig->voltageMultiplier > 1 || meterConfig->amperageMultiplier > 1 || meterConfig->accumulatedMultiplier > 1 ? "true" : "false",
|
||||
meterConfig->wattageMultiplier / 1000.0,
|
||||
meterConfig->voltageMultiplier / 1000.0,
|
||||
meterConfig->amperageMultiplier / 1000.0,
|
||||
meterConfig->accumulatedMultiplier / 1000.0
|
||||
);
|
||||
server.sendContent(buf);
|
||||
|
||||
if(gpioConfig.tempSensorPin == 0xff)
|
||||
doc[F("i")][F("t")][F("d")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("t")][F("d")] = gpioConfig.tempSensorPin;
|
||||
|
||||
if(gpioConfig.tempAnalogSensorPin == 0xff)
|
||||
doc[F("i")][F("t")][F("a")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("t")][F("a")] = gpioConfig.tempAnalogSensorPin;
|
||||
|
||||
if(gpioConfig.vccPin == 0xff)
|
||||
doc[F("i")][F("v")][F("p")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("p")] = gpioConfig.vccPin;
|
||||
|
||||
if(gpioConfig.vccOffset == 0)
|
||||
doc[F("i")][F("v")][F("o")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("o")] = gpioConfig.vccOffset / 100.0;
|
||||
|
||||
if(gpioConfig.vccMultiplier == 0)
|
||||
doc[F("i")][F("v")][F("m")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("m")] = gpioConfig.vccMultiplier / 1000.0;
|
||||
|
||||
if(gpioConfig.vccResistorVcc == 0)
|
||||
doc[F("i")][F("v")][F("d")][F("v")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("d")][F("v")] = gpioConfig.vccResistorVcc;
|
||||
|
||||
if(gpioConfig.vccResistorGnd == 0)
|
||||
doc[F("i")][F("v")][F("d")][F("g")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("d")][F("g")] = gpioConfig.vccResistorGnd;
|
||||
|
||||
if(gpioConfig.vccBootLimit == 0)
|
||||
doc[F("i")][F("v")][F("b")] = nullptr;
|
||||
else
|
||||
doc[F("i")][F("v")][F("b")] = gpioConfig.vccBootLimit / 10.0;
|
||||
|
||||
serializeJson(doc, buf, BufferSize);
|
||||
server.send(200, MIME_JSON, buf);
|
||||
snprintf_P(buf, BufferSize, CONF_THRESHOLDS_JSON,
|
||||
eac->thresholds[0],
|
||||
eac->thresholds[1],
|
||||
eac->thresholds[2],
|
||||
eac->thresholds[3],
|
||||
eac->thresholds[4],
|
||||
eac->thresholds[5],
|
||||
eac->thresholds[6],
|
||||
eac->thresholds[7],
|
||||
eac->thresholds[8],
|
||||
eac->thresholds[9],
|
||||
eac->hours
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_WIFI_JSON,
|
||||
wifiConfig.ssid,
|
||||
strlen(wifiConfig.psk) > 0 ? "***" : "",
|
||||
wifiConfig.power / 10.0,
|
||||
wifiConfig.sleep
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_NET_JSON,
|
||||
strlen(wifiConfig.ip) > 0 ? "static" : "dhcp",
|
||||
wifiConfig.ip,
|
||||
wifiConfig.subnet,
|
||||
wifiConfig.gateway,
|
||||
wifiConfig.dns1,
|
||||
wifiConfig.dns2,
|
||||
wifiConfig.mdns ? "true" : "false",
|
||||
ntpConfig.server,
|
||||
ntpConfig.dhcp ? "true" : "false"
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_MQTT_JSON,
|
||||
mqttConfig.host,
|
||||
mqttConfig.port,
|
||||
mqttConfig.username,
|
||||
strlen(mqttConfig.password) > 0 ? "***" : "",
|
||||
mqttConfig.clientId,
|
||||
mqttConfig.publishTopic,
|
||||
mqttConfig.payloadFormat,
|
||||
mqttConfig.ssl ? "true" : "false",
|
||||
qsc ? "true" : "false",
|
||||
qsr ? "true" : "false",
|
||||
qsk ? "true" : "false"
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_PRICE_JSON,
|
||||
strlen(entsoe.token) > 0 ? "true" : "false",
|
||||
entsoe.token,
|
||||
entsoe.area,
|
||||
entsoe.currency,
|
||||
entsoe.multiplier / 1000.0
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_DEBUG_JSON,
|
||||
debugConfig.serial ? "true" : "false",
|
||||
debugConfig.telnet ? "true" : "false",
|
||||
debugConfig.level
|
||||
);
|
||||
server.sendContent(buf);
|
||||
snprintf_P(buf, BufferSize, CONF_GPIO_JSON,
|
||||
gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(),
|
||||
gpioConfig->apPin == 0xff ? "null" : String(gpioConfig->apPin, 10).c_str(),
|
||||
gpioConfig->hanPin == 0xff ? "null" : String(gpioConfig->hanPin, 10).c_str(),
|
||||
gpioConfig->ledInverted ? "true" : "false",
|
||||
gpioConfig->ledPinRed == 0xff ? "null" : String(gpioConfig->ledPinRed, 10).c_str(),
|
||||
gpioConfig->ledPinGreen == 0xff ? "null" : String(gpioConfig->ledPinGreen, 10).c_str(),
|
||||
gpioConfig->ledPinBlue == 0xff ? "null" : String(gpioConfig->ledPinBlue, 10).c_str(),
|
||||
gpioConfig->ledRgbInverted ? "true" : "false",
|
||||
gpioConfig->tempSensorPin == 0xff ? "null" : String(gpioConfig->tempSensorPin, 10).c_str(),
|
||||
gpioConfig->tempAnalogSensorPin == 0xff ? "null" : String(gpioConfig->tempAnalogSensorPin, 10).c_str(),
|
||||
gpioConfig->vccPin == 0xff ? "null" : String(gpioConfig->vccPin, 10).c_str(),
|
||||
gpioConfig->vccOffset / 100.0,
|
||||
gpioConfig->vccMultiplier / 1000.0,
|
||||
gpioConfig->vccResistorVcc,
|
||||
gpioConfig->vccResistorGnd,
|
||||
gpioConfig->vccBootLimit / 10.0
|
||||
);
|
||||
server.sendContent(buf);
|
||||
server.sendContent("}");
|
||||
}
|
||||
|
||||
void AmsWebServer::handleSave() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf(PSTR("Handling save method from http"));
|
||||
if(!checkSecurity(1))
|
||||
@@ -1300,23 +1282,10 @@ void AmsWebServer::handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
void AmsWebServer::wifiScanJson() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /wifiscan.json over http...\n");
|
||||
|
||||
DynamicJsonDocument doc(512);
|
||||
|
||||
serializeJson(doc, buf, BufferSize);
|
||||
server.send(200, MIME_JSON, buf);
|
||||
}
|
||||
|
||||
void AmsWebServer::reboot() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /reboot over http...\n");
|
||||
|
||||
DynamicJsonDocument doc(128);
|
||||
doc[F("reboot")] = true;
|
||||
|
||||
serializeJson(doc, buf, BufferSize);
|
||||
server.send(200, MIME_JSON, buf);
|
||||
server.send(200, MIME_JSON, "{\"reboot\":true}");
|
||||
|
||||
server.handleClient();
|
||||
delay(250);
|
||||
@@ -1601,32 +1570,43 @@ void AmsWebServer::mqttKeyUpload() {
|
||||
void AmsWebServer::tariffJson() {
|
||||
if(debugger->isActive(RemoteDebug::DEBUG)) debugger->printf("Serving /tariff.json over http...\n");
|
||||
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
if(!checkSecurity(1))
|
||||
return;
|
||||
|
||||
EnergyAccountingConfig* eac = ea->getConfig();
|
||||
EnergyAccountingData data = ea->getData();
|
||||
|
||||
DynamicJsonDocument doc(512);
|
||||
JsonArray thresholds = doc.createNestedArray(F("t"));
|
||||
for(uint8_t x = 0;x < 10; x++) {
|
||||
thresholds.add(eac->thresholds[x]);
|
||||
}
|
||||
JsonArray peaks = doc.createNestedArray(F("p"));
|
||||
String peaks;
|
||||
for(uint8_t x = 0;x < min((uint8_t) 5, eac->hours); x++) {
|
||||
JsonObject p = peaks.createNestedObject();
|
||||
EnergyAccountingPeak peak = ea->getPeak(x+1);
|
||||
p["d"] = peak.day;
|
||||
p["v"] = peak.value / 100.0;
|
||||
int len = snprintf_P(buf, BufferSize, PEAK_JSON,
|
||||
peak.day,
|
||||
peak.value / 100.0
|
||||
);
|
||||
buf[len] = '\0';
|
||||
if(!peaks.isEmpty()) peaks += ",";
|
||||
peaks += String(buf);
|
||||
}
|
||||
doc["c"] = ea->getCurrentThreshold();
|
||||
doc["m"] = ea->getMonthMax();
|
||||
|
||||
serializeJson(doc, buf, BufferSize);
|
||||
snprintf_P(buf, BufferSize, TARIFF_JSON,
|
||||
eac->thresholds[0],
|
||||
eac->thresholds[1],
|
||||
eac->thresholds[2],
|
||||
eac->thresholds[3],
|
||||
eac->thresholds[4],
|
||||
eac->thresholds[5],
|
||||
eac->thresholds[6],
|
||||
eac->thresholds[7],
|
||||
eac->thresholds[8],
|
||||
eac->thresholds[9],
|
||||
peaks.c_str(),
|
||||
ea->getCurrentThreshold(),
|
||||
ea->getMonthMax()
|
||||
);
|
||||
|
||||
server.sendHeader(HEADER_CACHE_CONTROL, CACHE_CONTROL_NO_CACHE);
|
||||
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
|
||||
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
|
||||
|
||||
server.setContentLength(strlen(buf));
|
||||
server.send(200, MIME_JSON, buf);
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
extra_configs = platformio-user.ini
|
||||
|
||||
[common]
|
||||
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, EntsoePriceApi, EnergyAccounting, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, ArduinoJson, SvelteUi
|
||||
lib_deps = EEPROM, LittleFS, DNSServer, 256dpi/MQTT@2.5.0, OneWireNg@0.10.0, DallasTemperature@3.9.1, EspSoftwareSerial@6.14.1, https://github.com/gskjold/RemoteDebug.git, Time@1.6.1, Timezone@1.2.4, AmsConfiguration, AmsData, AmsDataStorage, HwTools, Uptime, EntsoePriceApi, EnergyAccounting, AmsMqttHandler, RawMqttHandler, JsonMqttHandler, DomoticzMqttHandler, HomeAssistantMqttHandler, SvelteUi
|
||||
lib_ignore = OneWire
|
||||
extra_scripts =
|
||||
pre:scripts/addversion.py
|
||||
|
||||
@@ -826,6 +826,7 @@ bool readHanPort() {
|
||||
if(pos == DATA_PARSE_INCOMPLETE) {
|
||||
return false;
|
||||
} else if(pos == DATA_PARSE_UNKNOWN_DATA) {
|
||||
meterState.setLastError(pos);
|
||||
debugV("Unknown data payload:");
|
||||
len = len + hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len);
|
||||
debugPrint(hanBuffer, 0, len);
|
||||
@@ -837,6 +838,7 @@ bool readHanPort() {
|
||||
len = 0;
|
||||
return false;
|
||||
} else if(pos < 0) {
|
||||
meterState.setLastError(pos);
|
||||
printHanReadError(pos);
|
||||
len += hanSerial->readBytes(hanBuffer+len, BUF_SIZE_HAN-len);
|
||||
if(mqttEnabled && mqtt != NULL && mqttHandler == NULL) {
|
||||
@@ -852,6 +854,7 @@ bool readHanPort() {
|
||||
for(int i = pos+ctx.length; i<BUF_SIZE_HAN; i++) {
|
||||
hanBuffer[i] = 0x00;
|
||||
}
|
||||
meterState.setLastError(DATA_PARSE_OK);
|
||||
|
||||
AmsData data;
|
||||
char* payload = ((char *) (hanBuffer)) + pos;
|
||||
@@ -1706,8 +1709,9 @@ void configFileParse() {
|
||||
sDs = true;
|
||||
} else if(strncmp_P(buf, PSTR("energyaccounting "), 17) == 0) {
|
||||
uint8_t i = 0;
|
||||
EnergyAccountingData ead = { 4, 0,
|
||||
0, 0, 0,
|
||||
EnergyAccountingData ead = { 0, 0,
|
||||
0, 0, 0, // Cost
|
||||
0, 0, 0, // Income
|
||||
0, 0, // Peak 1
|
||||
0, 0, // Peak 2
|
||||
0, 0, // Peak 3
|
||||
@@ -1716,33 +1720,73 @@ void configFileParse() {
|
||||
};
|
||||
char * pch = strtok (buf+17," ");
|
||||
while (pch != NULL) {
|
||||
if(i == 0) {
|
||||
// Ignore version
|
||||
} else if(i == 1) {
|
||||
long val = String(pch).toInt();
|
||||
ead.month = val;
|
||||
} else if(i == 2) {
|
||||
double val = String(pch).toDouble();
|
||||
if(val > 0.0) {
|
||||
ead.peaks[0] = { 1, (uint16_t) (val*100) };
|
||||
}
|
||||
} else if(i == 3) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costYesterday = val * 10;
|
||||
} else if(i == 4) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costThisMonth = val;
|
||||
} else if(i == 5) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costLastMonth = val;
|
||||
} else if(i >= 6 && i < 18) {
|
||||
uint8_t hour = i-6;
|
||||
if(hour%2 == 0) {
|
||||
if(ead.version < 5) {
|
||||
if(i == 0) {
|
||||
long val = String(pch).toInt();
|
||||
ead.peaks[hour/2].day = val;
|
||||
} else {
|
||||
ead.version = val;
|
||||
} else if(i == 1) {
|
||||
long val = String(pch).toInt();
|
||||
ead.month = val;
|
||||
} else if(i == 2) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.peaks[hour/2].value = val * 100;
|
||||
if(val > 0.0) {
|
||||
ead.peaks[0] = { 1, (uint16_t) (val*100) };
|
||||
}
|
||||
} else if(i == 3) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costYesterday = val * 10;
|
||||
} else if(i == 4) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costThisMonth = val;
|
||||
} else if(i == 5) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costLastMonth = val;
|
||||
} else if(i >= 6 && i < 18) {
|
||||
uint8_t hour = i-6;
|
||||
if(hour%2 == 0) {
|
||||
long val = String(pch).toInt();
|
||||
ead.peaks[hour/2].day = val;
|
||||
} else {
|
||||
double val = String(pch).toDouble();
|
||||
ead.peaks[hour/2].value = val * 100;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(i == 1) {
|
||||
long val = String(pch).toInt();
|
||||
ead.month = val;
|
||||
} else if(i == 2) {
|
||||
double val = String(pch).toDouble();
|
||||
if(val > 0.0) {
|
||||
ead.peaks[0] = { 1, (uint16_t) (val*100) };
|
||||
}
|
||||
} else if(i == 3) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costYesterday = val * 10;
|
||||
} else if(i == 4) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costThisMonth = val;
|
||||
} else if(i == 5) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.costLastMonth = val;
|
||||
} else if(i == 6) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.incomeYesterday= val * 10;
|
||||
} else if(i == 7) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.incomeThisMonth = val;
|
||||
} else if(i == 8) {
|
||||
double val = String(pch).toDouble();
|
||||
ead.incomeLastMonth = val;
|
||||
} else if(i >= 9 && i < 21) {
|
||||
uint8_t hour = i-9;
|
||||
if(hour%2 == 0) {
|
||||
long val = String(pch).toInt();
|
||||
ead.peaks[hour/2].day = val;
|
||||
} else {
|
||||
double val = String(pch).toDouble();
|
||||
ead.peaks[hour/2].value = val * 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
pch = strtok (NULL, " ");
|
||||
|
||||
Reference in New Issue
Block a user