-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathiotsaSmartMeter.ino
442 lines (402 loc) · 12.1 KB
/
iotsaSmartMeter.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
//
// Read current electricity and gas usage from a smart meter adhering to the dutch
// P1 standard.
//
// All dutch smart meters *must* include a P1 port that allows you to read them out using
// a slightly convoluted serial protocol. The details of the P1 port can be found at
// http://files.domoticaforum.eu/uploads/Smartmetering/DSMR%20v4.0%20final%20P1.pdf
//
// Unfortunately, after some experiments, it turns out the P1 port doesn't supply enough
// power to start (while the base station hasn't been discovered the esp uses 170 mA or more).
// Project is on hold.
//
#include "iotsa.h"
#include "iotsaWifi.h"
#ifdef ESP32
#include <HardwareSerial.h>
#else
#include <SoftwareSerial.h>
#endif
// CHANGE: Add application includes and declarations here
#define WITH_OTA // Enable Over The Air updates from ArduinoIDE. Needs at least 1MB flash.
IotsaApplication application("Iotsa Smart Meter Server");
IotsaWifiMod wifiMod(application);
#ifdef WITH_OTA
#include "iotsaOta.h"
IotsaOtaMod otaMod(application);
#endif
#ifdef IOTSA_WITH_BLE
#include "iotsaBLEServer.h"
IotsaBLEServerMod bleserverMod(application);
#include "iotsaBattery.h"
IotsaBatteryMod batteryMod(application);
#endif
//
// P1 interface.
//
#ifdef ESP32
#define PIN_ENABLE 10 // GPIO13 is ENABLE, active high, with external pullup to 5V (so we use pinMode tricks to drive it)
#define PIN_DATA 20 // GPIO12 is DATA, 115200 baud serial with inverted logic, internal pullup.
HardwareSerial p1Serial(1);
#else
#define PIN_ENABLE 13 // GPIO13 is ENABLE, active high, with external pullup to 5V (so we use pinMode tricks to drive it)
#define PIN_DATA 12 // GPIO12 is DATA, 115200 baud serial with inverted logic, internal pullup.
SoftwareSerial p1Serial(PIN_DATA, -1, true);
#endif
#define DATA_BAUDRATE 115200
#define MAX_TELEGRAM_SIZE 2048 // Maximum size of a data "telegram"
// P1 telegram parser class.
class P1Parser {
public:
P1Parser(String& _telegram) : telegram(_telegram) {}
P1Parser(const char * _telegram) : telegram(_telegram) {}
bool valid();
bool more();
bool next(String& name, String& value);
private:
String telegram;
};
bool P1Parser::valid() {
String hdr("/KFM5KAIFA-METER\r\n\r\n");
if (telegram.startsWith(hdr)) {
telegram.remove(0, hdr.length());
return true;
}
return false;
}
bool P1Parser::more() {
return !telegram.startsWith("!");
}
bool P1Parser::next(String& name, String& value) {
int lParPos = telegram.indexOf('(');
int lfPos = telegram.indexOf('\n');
if (lfPos < 2 || lParPos < 0 || lParPos > lfPos) {
name = "syntaxErrorAt";
value = telegram;
telegram = "!";
return true;
}
name = telegram.substring(0, lParPos);
value = telegram.substring(lParPos, lfPos-1);
telegram.remove(0, lfPos+1);
// See if we can parse the value
if (name == "0-0:1.0.0") {
name = "timestamp";
value.remove(value.length()-1);
value.remove(0,1);
value = "20" + value.substring(0, 2) + "-" + value.substring(2, 4) + "-" + value.substring(4, 6)
+ "T" + value.substring(6, 8) + ":" + value.substring(8, 10) + ":" + value.substring(10, 12);
return true;
}
if (name == "0-0:96.1.1") {
name = "meter_id_electricity";
value.remove(value.length()-1);
value.remove(0,1);
return true;
}
if (name == "0-1:96.1.0") {
name = "meter_id_gas";
value.remove(value.length()-1);
value.remove(0,1);
return true;
}
if (name == "0-0:96.14.0") {
name = "current_tariff";
value.remove(value.length()-1);
value.remove(0,1);
return true;
}
if (name == "0-0:96.7.21") {
name = "total_power_failures";
value.remove(value.length()-1);
value.remove(0,1);
return true;
}
if (name == "0-0:96.7.9") {
name = "total_power_failures_long";
value.remove(value.length()-1);
value.remove(0,1);
return true;
}
if (name == "1-0:1.8.1") {
name = "total_kwh_tariff_1";
value.remove(11);
value.remove(0,1);
return true;
}
if (name == "1-0:1.8.2") {
name = "total_kwh_tariff_2";
value.remove(11);
value.remove(0,1);
return true;
}
if (name == "1-0:2.8.1") {
name = "total_kwh_tariff_1_returned";
value.remove(11);
value.remove(0,1);
return true;
}
if (name == "1-0:2.8.2") {
name = "total_kwh_tariff_2_returned";
value.remove(11);
value.remove(0,1);
return true;
}
if (name == "1-0:1.7.0") {
name = "current_kw";
value.remove(7);
value.remove(0,1);
return true;
}
if (name == "1-0:2.7.0") {
name = "current_kw_return";
value.remove(7);
value.remove(0,1);
return true;
}
if (name == "0-1:24.2.1") {
name = "total_gas_m3";
int pos = value.indexOf(')');
value.remove(0, pos+1);
value.remove(10);
value.remove(0,1);
return true;
}
return false;
}
// Declaration of the Hello module
class IotsaP1Mod : public IotsaMod, IotsaApiProvider
#ifdef IOTSA_WITH_BLE
, IotsaBLEApiProvider
#endif
{
public:
IotsaP1Mod(IotsaApplication &_app)
: IotsaMod(_app),
api(this, _app, nullptr)
{}
void setup() override;
void serverSetup() override;
void loop() override;
String info() override;
#ifdef IOTSA_WITH_API
bool getHandler(const char *path, JsonObject& reply) override;
bool putHandler(const char *path, const JsonVariant& request, JsonObject& reply) override { return false; }
bool postHandler(const char *path, const JsonVariant& request, JsonObject& reply) override { return false; }
#endif
private:
void handler();
bool readTelegram();
char telegram[MAX_TELEGRAM_SIZE];
int telegramSize;
protected:
#ifdef IOTSA_WITH_API
IotsaApiService api;
#endif
#ifdef IOTSA_WITH_BLE
IotsaBleApiService bleApi;
bool blePutHandler(UUIDstring charUUID) override { return false; };
bool bleGetHandler(UUIDstring charUUID) override;
static constexpr UUIDstring serviceUUID = "2B000000-BAAD-4A33-898A-3E8902CC1E7A";
static constexpr UUIDstring p1UUID = "2B000001-BAAD-4A33-898A-3E8902CC1E7A";
#endif // IOTSA_WITH_BLE
};
// Implementation of the Hello module
void IotsaP1Mod::setup() {
pinMode(PIN_ENABLE, OUTPUT); // Set ENABLE to output/low -> pulldown -> no enable signal
digitalWrite(PIN_ENABLE, LOW);
pinMode(PIN_DATA, INPUT_PULLUP);
#ifdef ESP32
p1Serial.begin(DATA_BAUDRATE, SERIAL_8N1, PIN_DATA, -1, true);
#else
p1Serial.begin(DATA_BAUDRATE);
#endif
p1Serial.setTimeout(3000);
#ifdef IOTSA_WITH_BLE
bleApi.setup(serviceUUID, this);
// Explain to clients what the rgb characteristic looks like
bleApi.addCharacteristic(p1UUID, BLE_READ, BLE2904::FORMAT_UTF8, 0x2700, "P1 telegram");
#endif
}
bool IotsaP1Mod::readTelegram() {
p1Serial.flush();
pinMode(PIN_ENABLE, INPUT); // Set ENABLE to input -> hi-Z -> enable signal via external pullup
// Remove initial 0x00 byte
telegramSize = p1Serial.readBytes(telegram, MAX_TELEGRAM_SIZE);
pinMode(PIN_ENABLE, OUTPUT); // Set ENABLE to output/low -> pulldown -> no enable signal
while (telegramSize > 0 && telegram[0] != '/') {
telegramSize--;
memmove(telegram, telegram+1, telegramSize);
}
return telegramSize > 0;
}
bool IotsaP1Mod::getHandler(const char *path, JsonObject& reply) {
if (readTelegram()) {
P1Parser p(telegram);
if (p.valid()) {
while(p.more()) {
String name, value;
p.next(name, value);
reply[name] = value;
}
} else {
reply["error"] = "Invalid P1 telegram received";
}
} else {
reply["error"] = "No P1 telegram received";
}
return true;
}
#ifdef IOTSA_WITH_BLE
bool IotsaP1Mod::bleGetHandler(UUIDstring charUUID) {
if (charUUID == p1UUID) {
IFDEBUG IotsaSerial.println("BLE getHandler for P1 telegram");
if (readTelegram()) {
// When using BLE we only return the important values, not the full telegram
String rv = "{";
P1Parser p(telegram);
if (p.valid()) {
while(p.more()) {
String name, value;
if (p.next(name, value)) {
rv += "\"";
rv += name;
rv += "\":\"";
rv += value;
rv += "\",";
}
}
rv += "\"error\":null}";
bleApi.set(p1UUID, rv);
} else {
bleApi.set(p1UUID, String("{\"error\":\"Invalid P1 telegram received\"}"));
}
} else {
bleApi.set(p1UUID, String("{\"error\":\"No P1 telegram received\"}"));
}
return true;
}
return false;
}
#endif
void
IotsaP1Mod::handler() {
// By default, return in the format acceptable to the first Accept: entry
String format = server->header("Accept");
int semiPos = format.indexOf(';');
if (semiPos > 0) format.remove(semiPos);
// Allow it to be overridden by format argument
for (uint8_t i=0; i<server->args(); i++){
if( server->argName(i) == "format") {
format = server->arg(i);
}
}
if (format == "" || format == "text/plain" || format == "*/*") {
if (readTelegram()) {
server->send_P(200, "text/plain", telegram, telegramSize);
} else {
server->send(503, "text/plain", "No P1 telegram received");
}
} else if (format == "json" || format == "application/json") {
if (readTelegram()) {
P1Parser p(telegram);
if (p.valid()) {
String message = "{";
while(p.more()) {
String name, value;
p.next(name, value);
message += "\"";
message += name;
message += "\":\"";
message += value;
message += "\"";
if (p.more()) message += ",";
}
message += "}\n";
server->send(200, "application/json", message);
} else {
String msg("Invalid P1 telegram received:\n");
msg += telegram;
server->send(503, "text/plain", msg);
}
} else {
server->send(503, "text/plain", "No P1 telegram received");
}
} else if (format == "xml" || format == "application/xml") {
if (readTelegram()) {
P1Parser p(telegram);
if (p.valid()) {
String message = "<smartMeter>";
while(p.more()) {
String name, value;
bool known = p.next(name, value);
if (known) {
message += "<";
message += name;
message += ">";
message += value;
message += "</";
message += name;
message += ">";
} else {
message += "<unknown tag=\"";
message += name;
message += "\">";
message += value;
message += "</unknown>";
}
}
message += "</smartMeter>\n";
server->send(200, "application/xml", message);
} else {
server->send(503, "text/plain", "Invalid P1 telegram received");
}
} else {
server->send(503, "text/plain", "No P1 telegram received");
}
} else {
String message = "Unknown format ";
message += format;
server->send(422, "text/plain", message);
}
}
void IotsaP1Mod::serverSetup() {
// Setup the web server hooks for this module.
server->on("/p1", std::bind(&IotsaP1Mod::handler, this));
api.setup("/api/p1", true, false, false);
name = "p1";
}
String IotsaP1Mod::info() {
// Return some information about this module, for the main page of the web server.
String rv = "<p>See <a href=\"/p1\">/p1</a> for current household energy usage";
rv += " (Also available in <a href=\"/p1?format=json\">json</a> and <a href=\"/p1?format=xml\">xml</a>).";
#ifdef IOTSA_WITH_REST
rv += " Or use REST api at <a href='/api/p1'>/api/led</a>.";
#endif
#ifdef IOTSA_WITH_BLE
rv += " Or use BLE service " + String(serviceUUID) + " on device " + iotsaConfig.hostName + ".";
#endif
rv += "</p>";
return rv;
}
void IotsaP1Mod::loop() {
// Nothing to do in the loop, for this module
}
// Instantiate the Hello module, and install it in the framework
IotsaP1Mod p1Mod(application);
// Standard setup() method, hands off most work to the application framework
void setup(void){
#if 0
// We lower power, the P1 port can only supply 100mA.
WiFi.setOutputPower(0);
#endif
application.setup();
application.serverSetup();
#ifndef ESP32
ESP.wdtEnable(WDTO_120MS);
#endif
}
// Standard loop() routine, hands off most work to the application framework
void loop(void){
application.loop();
}