-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathpe32me162ir_pub.ino
1129 lines (1030 loc) · 36.6 KB
/
pe32me162ir_pub.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
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* pe32me162ir_pub // Talk to optical port of ISKRA ME-162, export to MQTT
*
* Components:
* - ISKRA ME-162 electronic meter with optical port
* - a digital IR-transceiver from https://wiki.hal9k.dk/projects/kamstrup --
* BEWARE: be sure to check the direction of the two BC547 transistors.
* The pictures on the wiki have them backwards. Check the
* https://wiki.hal9k.dk/_media/projects/kamstrup-schem-v2.pdf !
* - ESP8266 (NodeMCU, with Wifi) _or_ an Arduino (Uno?). Wifi/MQTT publish
* support is only(!) available for the ESP8266 at the moment.
* - attach PIN_IR_RX<->RX, PIN_IR_TX<->TX, 3VC<->VCC (or 5VC), GND<->GND
* - (optional: analog light sensor to attach to A0<->SIG (and 3VC and GND))
*
* Building/dependencies:
* - Arduino IDE
* - (for ESP8266) board: package_esp8266com_index.json
* - (for ESP8266) library: ArduinoMqttClient
* - (for Arduino) library (manually): CustomSoftwareSerial (by ledongthuc)
*
* Configuration:
* - Connect the pins as specified above and below (PIN_IR_RX, PIN_IR_TX);
* - connect the IR-tranceiver to the ISKA ME-162, with _our_ TX on the left
* and _our_ RX on the right;
* - Copy config.h.example to config.h and adapt it to your situation and
* preferences.
* - Copy arduino_secrets.h.example to arduino_secrets.h and fill in your
* credentials and MQTT information.
*
* ISKRA ME-162 electricity meter notes:
* > The optical port complies with the IEC 62056-21 (IEC
* > 61107) standard, a mode C protocol is employed;
* > data transmission rate is 9600 bit/sec.
* ...
* > The optical port wavelength is 660 nm and luminous
* > intensity is min. 1 mW/sr for the ON state.
*
* IEC 62056-21 mode C:
* - is a bidirectional ASCII protocol;
* - that starts in 300 baud with 1 start bit, 7 data bits, 1 (even)
* parity bit and one stop bit;
* - can be upgraded to higher baud rate (9600) after the handshake;
* - and allows manufacturer-specific extensions.
* - See: github.com/lvzon/dsmr-p1-parser/blob/master/doc/IEC-62056-21-notes.md
*
* TODO:
* - clean up debug
* - add/standardize first MQTT push (with data_readout and date and time)
*/
#include "pe32me162ir_pub.h"
#define VERSION "v3~pre3"
/* On the ESP8266, the baud rate needs to be sufficiently high so it
* doesn't affect the SoftwareSerial. (Probably because this Serial is
* blocking/serial? 9600 is too low.)
* On the Arduino Uno, the baud rate is free to choose. Just make sure
* you don't try to cram large values into a 16-bits int. */
static const long SERMON_BAUD = 115200; // serial monitor for debugging
#if defined(ARDUINO_ARCH_ESP8266)
static const int PIN_IR_RX = 5; // D1 / GPIO5
static const int PIN_IR_TX = 4; // D2 / GPIO4
#else /*defined(ARDUINO_ARCH_AVR)*/
static const int PIN_IR_RX = 9; // digital pin 9
static const int PIN_IR_TX = 10; // digital pin 10
#endif
DECLARE_PGM_CHAR_P(wifi_ssid, SECRET_WIFI_SSID);
DECLARE_PGM_CHAR_P(wifi_password, SECRET_WIFI_PASS);
DECLARE_PGM_CHAR_P(mqtt_broker, SECRET_MQTT_BROKER);
static const int mqtt_port = SECRET_MQTT_PORT;
DECLARE_PGM_CHAR_P(mqtt_topic, SECRET_MQTT_TOPIC);
#ifdef OPTIONAL_LIGHT_SENSOR
static const int PULSE_THRESHOLD = 100; // analog value between 0 and 1023
#endif //OPTIONAL_LIGHT_SENSOR
static const int STATE_CHANGE_TIMEOUT = 15; // reset state after 15s of no change
enum State {
STATE_WR_LOGIN = 0,
STATE_RD_IDENTIFICATION,
STATE_WR_REQ_DATA_MODE, /* data (readout) mode */
STATE_RD_DATA_READOUT,
STATE_RD_DATA_READOUT_SLOW,
STATE_WR_RESTART,
STATE_WR_LOGIN2,
STATE_RD_IDENTIFICATION2,
STATE_WR_PROG_MODE, /* programming mode */
STATE_RD_PROG_MODE_ACK,
STATE_WR_REQ_OBIS,
STATE_RD_RESP_OBIS,
STATE_MAYBE_PUBLISH,
STATE_SLEEP
};
/* Subset of OBIS (or EDIS) codes from IEC 62056 provided by the ISKRA ME-162.
* [A-B:]C.D.E[*F] where the ISKRA ME-162 does not do [A-B:] medium addressing.
* Note that I removed the T1 and T2 types here, as in the Netherlands,
* Ripple Control (TF-signal) will be completely disabled for non-"smart"
* meters by July 2021. Switching between T1 and T2 will not be possible on
* dumb meters: you'll be stuck using a single tariff anyway. */
enum Obis {
OBIS_C_1_0 = 0, // Meter serial number
OBIS_F_F_0, // Fatal error meter status
OBIS_0_9_1, // Time (returns (hh:mm:ss))
OBIS_0_9_2, // Date (returns (YY.MM.DD))
// We read these two in a loop, the rest are irrelevant to us.
OBIS_1_8_0, // Positive active energy (A+) total [Wh]
OBIS_2_8_0, // Negative active energy (A+) total [Wh]
#if 0
// Available in ME-162, but not that useful to us:
OBIS_1_8_1, // Positive active energy (A+) in tariff T1 [Wh]
OBIS_1_8_2, // Positive active energy (A+) in tariff T2 [Wh]
OBIS_1_8_3, // Positive active energy (A+) in tariff T3 [Wh]
OBIS_1_8_4, // Positive active energy (A+) in tariff T4 [Wh]
OBIS_2_8_1, // Negative active energy (A+) in tariff T1 [Wh]
// [...]
OBIS_15_8_0, // Total absolute active energy (= 1_8_0 + 2_8_0)
#endif
// Alas, not in ME-162 (returns (ERROR) when queried):
//OBIS_1_7_0, // Positive active instantaneous power (A+) [W]
//OBIS_2_7_0, // Negative active instantaneous power (A+) [W]
//OBIS_16_7_0, // Sum active instantaneous power [W] (= 1_7_0 - 2_7_0)
//OBIS_16_8_0, // Sum of active energy without blockade (= 1_8_0 - 2_8_0)
OBIS_LAST
};
/* Trick to allow defining an array of PROGMEM strings. */
typedef struct { char pgm_str[7]; } obis_pgm_t;
/* Keep in sync with the Obis enum! */
const obis_pgm_t Obis_[] PROGMEM = {
{"C.1.0"}, {"F.F"}, {"0.9.1"}, {"0.9.2"}, {"1.8.0"}, {"2.8.0"},
#if 0
{"1.8.1"}, {"1.8.2"}, {"1.8.3"}, {"1.8.4"}, {"2.8.1"}, {"15.8.0"},
#endif
{"UNDEF"}
};
struct obis_values_t {
unsigned long values[OBIS_LAST];
};
/* C-escape, for improved serial monitor readability */
static const char *cescape(
char *buffer, const char *p, size_t maxlen, bool progmem = false);
static inline const pgm_char *cescape(
char *buffer, const pgm_char *p, size_t maxlen) {
return to_pgm_char_p(cescape(buffer, from_pgm_char_p(p), maxlen, true));
}
/* Calculate and (optionally) check block check character (BCC) */
static int din_66219_bcc(const char *s);
/* Convert string to Obis number or OBIS_LAST if not found */
static inline enum Obis str2Obis(const char *key, int keylen);
/* Convert Obis string to number */
inline const pgm_char *Obis2str(Obis obis) {
return to_pgm_char_p(Obis_[obis].pgm_str);
}
/* Parse data readout buffer and populate obis_values_t */
static void parse_data_readout(struct obis_values_t *dst, const char *src);
#ifdef HAVE_MQTT /* and HAVE_WIFI */
static void ensure_wifi();
static void ensure_mqtt();
#else
static inline void ensure_wifi() {} /* noop */
static inline void ensure_mqtt() {} /* noop */
#endif
/* Helpers */
template<class T> static inline void iskra_tx(const T *p);
template<class T> static inline void serial_print_cescape(const T *p);
static inline void trace_rx_buffer();
/* Helper to add a little type safety to memcmp. */
static inline int memcmp_cstr(const char *s1, const char *s2, size_t len) {
return memcmp(s1, s2, len);
}
/* Neat trick to let us do multiple Serial.print() using the << operator:
* Serial << x << " " << y << LF; */
template<class T> inline Print &operator << (Print &obj, T arg) {
obj.print(arg);
return obj;
};
/* Events */
static State on_data_block_or_data_set(char *data, size_t pos, State st);
static State on_hello(const char *data, size_t end, State st);
static void on_data_readout(const char *data, size_t end);
static void on_response(const char *data, size_t end, Obis obis);
static void publish();
/* ASCII control codes */
const char C_SOH = '\x01';
#define S_SOH "\x01"
const char C_STX = '\x02';
#define S_STX "\x02"
const char C_ETX = '\x03';
#define S_ETX "\x03"
const char C_ACK = '\x06';
#define S_ACK "\x06"
const char C_NAK = '\x15';
#define S_NAK "\x15"
const char C_ENDL = '\n';
#define S_ENDL "\n"
/* We use the guid to store something unique to identify the device by.
* For now, we'll populate it with the ESP8266 Wifi MAC address,
* if available. */
static char guid[24] = "<no_wifi_found>"; // "EUI48:11:22:33:44:55:66"
#ifdef HAVE_WIFI
# ifdef MQTT_TLS
WiFiClientSecure wifiClient;
# else
WiFiClient wifiClient;
# endif
# ifdef HAVE_MQTT
MqttClient mqttClient(wifiClient);
# endif
#endif
#ifdef MQTT_TLS
static const uint8_t mqtt_fingerprint[20] PROGMEM = SECRET_MQTT_FINGERPRINT;
#endif
#ifdef MQTT_AUTH
# ifndef MQTT_TLS
# error MQTT_AUTH requires MQTT_TLS
# else
DECLARE_PGM_CHAR_P(mqtt_user, SECRET_MQTT_USER);
DECLARE_PGM_CHAR_P(mqtt_pass, SECRET_MQTT_PASS);
# endif
#endif
/* We need a (Custom)SoftwareSerial because the Arduino Uno does not do
* 300 baud. Once we get up to speed, we _could_ use the HardwareSerial
* instead. (But we don't, right now.)
* - the Arduino Uno Hardware Serial does not support baud below 1200;
* - IR-communication starts with 300 baud;
* - using SoftwareSerial allows us to use the hardware serial monitor
* for debugging;
* - Arduino SoftwareSerial does only SERIAL_8N1 (8bit, no parity, 1 stop bit)
* (so we use CustomSoftwareSerial; for ESP8266 the included
* SoftwareSerial already includes different modes);
* - we require SERIAL_7E1 (7bit, even parity, 1 stop bit).
* Supply RX pin, TX pin, inverted=false. Our IR-device uses:
* HIGH == no TX light == (serial) idle */
SoftwareSerial iskra(PIN_IR_RX, PIN_IR_TX, false);
/* Current state, scheduled state, current "write" state for retries */
State state, next_state, write_state;
unsigned long last_statechange;
/* Storage for incoming data. If the data readout is larger than this size
* bytes, then the rest of the code won't cope. (The observed data is at most
* 200 octets long, so this should be sufficient.) */
size_t buffer_pos;
const int buffer_size = 800;
char buffer_data[buffer_size + 1];
/* IEC 62056-21 6.3.2 + 6.3.14:
* 3chars + 1char-baud + (optional) + 16char-ident */
char identification[32];
#ifdef OPTIONAL_LIGHT_SENSOR
/* Record low and high pulse values so we can debug/monitor the light
* sensor values from the MQTT data. */
short pulse_low = 1023;
short pulse_high = 0;
#endif //OPTIONAL_LIGHT_SENSOR
Obis next_obis;
EnergyGauge gauge; /* feed it 1.8.0 and 2.8.0, get 1.7.0 and 2.7.0 */
unsigned long last_publish;
void setup()
{
Serial.begin(SERMON_BAUD);
while (!Serial)
delay(0);
#ifdef HAVE_WIFI
strncpy(guid, "EUI48:", 6);
strncpy(guid + 6, WiFi.macAddress().c_str(), sizeof(guid) - (6 + 1));
# ifdef MQTT_TLS
wifiClient.setFingerprint(mqtt_fingerprint);
# endif
# ifdef MQTT_AUTH
mqttClient.setUsernamePassword(mqtt_user, mqtt_pass);
# endif
#endif
pinMode(PIN_IR_RX, INPUT);
pinMode(PIN_IR_TX, OUTPUT);
// Welcome message
delay(200); /* tiny sleep to avoid dupe log after double restart */
Serial << F("Booted pe32me162ir_pub " VERSION " guid ") << guid << C_ENDL;
// Initial connect (if available)
ensure_wifi();
ensure_mqtt();
// Send termination command, in case we were already connected and
// in 9600 baud previously.
iskra.begin(9600, SWSERIAL_7E1);
iskra_tx(F(S_SOH "B0" S_ETX "q"));
// Initial values
state = next_state = STATE_WR_LOGIN;
last_statechange = last_publish = millis();
}
void loop()
{
switch (state) {
/* #1: At 300 baud, we send "/?!\r\n" or "/?1!\r\n" */
case STATE_WR_LOGIN:
case STATE_WR_LOGIN2:
write_state = state;
/* Communication starts at 300 baud, at 1+7+1+1=10 bits/septet. So, for
* 30 septets/second, we could wait 33.3ms when there is nothing. */
iskra.begin(300, SWSERIAL_7E1);
iskra_tx(F("/?!\r\n"));
next_state = (state == STATE_WR_LOGIN
? STATE_RD_IDENTIFICATION : STATE_RD_IDENTIFICATION2);
break;
/* #2: We receive "/ISK5ME162-0033\r\n" */
case STATE_RD_IDENTIFICATION:
case STATE_RD_IDENTIFICATION2:
if (iskra.available()) {
while (iskra.available() && buffer_pos < buffer_size) {
char ch = iskra.read();
if (0) {
#if defined(ARDUINO_ARCH_AVR)
/* On the Arduino Uno, we tend to three of these after sending
* STATE_WR_LOGIN (at 300 baud), before reception. */
} else if (buffer_pos == 0 && ch == 0x7f) {
Serial << F("<< (skipping 0x7f)" S_ENDL); // only observed on Arduino
#endif
} else if (ch == '\0') {
Serial << F("<< (unexpected NUL, ignoring)" S_ENDL);
} else {
buffer_data[buffer_pos++] = ch;
buffer_data[buffer_pos] = '\0';
}
if (ch == '\n' && buffer_pos >= 2 &&
buffer_data[buffer_pos - 2] == '\r') {
buffer_data[buffer_pos - 2] = '\0'; /* drop "\r\n" */
next_state = on_hello(buffer_data + 1, buffer_pos - 3, state);
buffer_pos = 0;
break;
}
}
trace_rx_buffer();
}
/* When there is no data, we could wait 30ms for another 10 bits.
* But we've seen odd thing happen. Busy-loop instead. */
break;
/* #3: We send an ACK with "speed 5" to switch to 9600 baud */
case STATE_WR_REQ_DATA_MODE:
case STATE_WR_PROG_MODE:
write_state = state;
/* ACK V Z Y:
* V = protocol control (0=normal, 1=2ndary, ...)
* Z = 0=NAK or 'ISK5ME162'[3] for ACK speed change (9600 for ME-162)
* Y = mode control (0=readout, 1=programming, 2=binary)
* "\ACK 001\r\n" should NAK speed, but go into programming mode,
* but that doesn't work on the ME-162. */
if (state == STATE_WR_REQ_DATA_MODE) {
iskra_tx(F(S_ACK "050\r\n")); // 050 = 9600baud + data readout mode
next_state = STATE_RD_DATA_READOUT;
} else {
iskra_tx(F(S_ACK "051\r\n")); // 051 = 9600baud + programming mode
next_state = STATE_RD_PROG_MODE_ACK;
}
/* We're assuming here that the speed change does not affect the
* previously written characters. It shouldn't if they're written
* synchronously. */
iskra.begin(9600, SWSERIAL_7E1);
break;
/* #4: We expect "\STX $EDIS_DATA\ETX $BCC" with power info. */
case STATE_RD_DATA_READOUT:
case STATE_RD_DATA_READOUT_SLOW:
case STATE_RD_PROG_MODE_ACK: /* \SOH P0\STX ()\ETX $BCC */
case STATE_RD_RESP_OBIS: /* \STX (0032835.698*kWh)\ETX $BCC */
if (iskra.available()) {
while (iskra.available() && buffer_pos < buffer_size) {
char ch = iskra.read();
if (0) {
#if defined(ARDUINO_ARCH_AVR)
/* On the Arduino Uno, we tend to six of these after sending
* STATE_WR_REQ_OBIS (at 9600 baud), before reception. */
} else if (buffer_pos == 0 && ch == 0x7f) {
Serial << F("<< (skipping 0x7f)" S_ENDL); // only observed on Arduino
#endif
} else if (ch == '\0') {
Serial << F("<< (unexpected NUL, ignoring)" S_ENDL);
} else {
buffer_data[buffer_pos++] = ch;
buffer_data[buffer_pos] = '\0';
}
if (ch == C_NAK) {
Serial << F("<< ");
serial_print_cescape(buffer_data);
next_state = write_state;
buffer_pos = 0;
break;
}
if (buffer_pos >= 2 && buffer_data[buffer_pos - 2] == C_ETX) {
/* If the last non-BCC token is EOT, we should send an ACK
* to get the rest. But seeing that message ends with ETX, we
* should not ACK. */
Serial << F("<< ");
serial_print_cescape(buffer_data);
/* We're looking at a BCC now. Validate. */
int res = din_66219_bcc(buffer_data);
if (res < 0) {
Serial << F("bcc fail: ") << res << C_ENDL;
/* Hope for a restransmit. Reset buffer. */
buffer_pos = 0;
break;
}
/* Valid BCC. Call appropriate handlers and switch state. */
next_state = on_data_block_or_data_set(
buffer_data, buffer_pos, state);
buffer_pos = 0;
break;
}
}
trace_rx_buffer();
}
break;
/* #5: Terminate the connection with "\SOH B0\ETX " */
case STATE_WR_RESTART:
write_state = state;
iskra_tx(F(S_SOH "B0" S_ETX "q"));
next_state = STATE_WR_LOGIN2;
break;
/* Continuous: send "\SOH R1\STX 1.8.0()\ETX " for 1.8.0 register */
case STATE_WR_REQ_OBIS:
write_state = state;
{
char buf[16];
#if !defined(TEST_BUILD)
/* Type safety is not available for the *_P functions.. Probably
* because this is C-compatible. So, we'll use PSTR() instead of F().
* PSTR() also puts the string in PROGMEM, but does not cast to the
* __FlashStringHelper. */
snprintf_P(buf, 15, PSTR(S_SOH "R1" S_STX "%S()" S_ETX),
Obis2str(next_obis));
#else
snprintf(buf, 15, (S_SOH "R1" S_STX "%s()" S_ETX),
from_pgm_char_p(Obis2str(next_obis)));
#endif
char bcc = din_66219_bcc(buf);
int pos = strlen(buf);
buf[pos] = bcc;
buf[pos + 1] = '\0';
iskra_tx(buf);
next_state = STATE_RD_RESP_OBIS;
}
break;
/* Continuous: maybe publish data to remote */
case STATE_MAYBE_PUBLISH:
#ifdef HAVE_MQTT
/* We don't necessarily publish every 60s, but we _do_ need to keep
* the MQTT connection alive. poll() is safe to call often. */
mqttClient.poll();
#endif
{
int tdelta_s = (millis() - last_publish) / 1000;
int power = gauge.get_instantaneous_power();
/* DEBUG */
Serial << F("time to publish? ") << power <<
F(" Watt, ") << tdelta_s << F(" seconds");
if (gauge.has_significant_change())
Serial << F(", has significant change");
Serial << C_ENDL;
/* Only push every 120s or more often when there are significant
* changes. */
if (tdelta_s >= 120 ||
/* power is higher than 400: then we have more detail */
(tdelta_s >= 60 && !(-400 < power && power < 400)) ||
(tdelta_s >= 25 && gauge.has_significant_change())) {
publish();
gauge.reset();
#ifdef OPTIONAL_LIGHT_SENSOR
pulse_low = 1023;
pulse_high = 0;
#endif
last_publish = millis();
}
}
next_state = STATE_SLEEP;
break;
/* Continuous: just sleep a slight bit */
case STATE_SLEEP:
#ifdef OPTIONAL_LIGHT_SENSOR
/* This is a mashup between simply doing the poll-for-new-totals
* every second, and only-a-poll-after-pulse:
* - polling every second or so gives us decent, but not awesome, averages
* - polling after a pulse gives us great averages
* But, the pulse may not work properly once we have both positive
* and negative power counts. Also, if we can do without the extra
* photo transistor, it makes installation simpler.
* (But, while it is available, it might increase the accuracy of
* the averages. Needs confirmation!) */
{
/* Pulse or not, after one second it's time. */
bool have_waited_a_second = (millis() - last_statechange) >= 1200;
short val = analogRead(A0);
/* Debug information, sent over MQTT. */
pulse_low = min(pulse_low, val);
pulse_high = max(pulse_high, val);
if (val >= PULSE_THRESHOLD || have_waited_a_second) {
if (!have_waited_a_second) {
/* Sleep cut short, for better average calculations. */
Serial << F("pulse: Got value ") << val << C_ENDL;
/* Add delay. It appears that after a Wh pulse, the meter takes at
* most 1000ms to update the Wh counter. Without this delay, we'd
* usually get the Wh count of the previous second, except
* sometimes. That effect caused seemingly random high and then
* low spikes in the Watt averages. */
delay(1000);
}
next_obis = OBIS_1_8_0;
next_state = STATE_WR_REQ_OBIS;
}
}
#else //!OPTIONAL_LIGHT_SENSOR
/* Wait 1.2s and then schedule a new request. */
if ((millis() - last_statechange) >= 1200) {
next_obis = OBIS_1_8_0;
next_state = STATE_WR_REQ_OBIS;
}
#endif //!OPTIONAL_LIGHT_SENSOR
break;
}
/* Always check for state change timeout */
if (state == next_state &&
(millis() - last_statechange) > (STATE_CHANGE_TIMEOUT * 1000)) {
if (buffer_pos) {
Serial << F("<< (stale buffer sized ") << buffer_pos << F(") ");
serial_print_cescape(buffer_data);
}
/* Note that after having been connected, it may take up to a minute
* before a new connection can be established. So we may end up here
* a few times before reconnecting for real. */
Serial << F("timeout: State change took to long, resetting..." S_ENDL);
next_state = STATE_WR_LOGIN;
}
/* Handle state change */
if (state != next_state) {
Serial << F("state: ") << state << F(" -> ") << next_state << C_ENDL;
state = next_state;
buffer_pos = 0;
last_statechange = millis();
}
}
State on_hello(const char *data, size_t end, State st)
{
/* buffer_data = "ISK5ME162-0033" (ISKRA ME-162) (no "/" or "\r\n")
* - uppercase 'K' means slow-ish (200ms (not 20ms) response times)
* - (for protocol mode C) suggest baud '5'
* (0=300, 1=600, 2=1200, 3=2400, 4=4800, 5=9600, 6=19200) */
Serial << F("on_hello: ") << data << C_ENDL;
/* Store identification string */
identification[sizeof(identification - 1)] = '\0';
strncpy(identification, data, sizeof(identification) - 1);
/* Check if we can upgrade the speed */
if (end >= 3 && data[3] == '5') {
// Send ACK, and change speed.
if (st == STATE_RD_IDENTIFICATION) {
return STATE_WR_REQ_DATA_MODE;
} else if (st == STATE_RD_IDENTIFICATION2) {
return STATE_WR_PROG_MODE;
}
}
/* If it was not a '5', we cannot upgrade to 9600 baud, and we cannot
* enter programming mode to get values ourselves. */
return STATE_RD_DATA_READOUT_SLOW;
}
State on_data_block_or_data_set(char *data, size_t pos, State st)
{
data[pos - 2] = '\0'; /* drop ETX */
switch (st) {
case STATE_RD_DATA_READOUT:
on_data_readout(data + 1, pos - 3);
return STATE_WR_RESTART;
case STATE_RD_PROG_MODE_ACK:
if (pos >= 6 && memcmp_P(data, F(S_SOH "P0" S_STX "()"), 6) == 0) {
next_obis = OBIS_1_8_0;
return STATE_WR_REQ_OBIS;
}
return STATE_WR_PROG_MODE;
case STATE_RD_RESP_OBIS:
on_response(data + 1, pos - 3, next_obis);
next_obis = (Obis)((int)next_obis + 1);
if (next_obis < OBIS_LAST) {
return STATE_WR_REQ_OBIS;
}
return STATE_MAYBE_PUBLISH;
default:
/* shouldn't get here.. */
break;
}
return st;
}
void on_data_readout(const char *data, size_t /*end*/)
{
struct obis_values_t vals;
unsigned long t = millis();
/* Data between STX and ETX. It should look like:
* > C.1.0(28342193) // Meter serial number
* > 0.0.0(28342193) // Device address
* > 1.8.0(0032826.545*kWh) // Total positive active energy (A+)
* > 1.8.1(0000000.000*kWh) // Positive active energy in first tariff (T1)
* > 1.8.2(0032826.545*kWh) // Positive active energy in second tariff (T2)
* > 2.8.0(0000000.001*kWh) // Total negative active energy (A-)
* > 2.8.1(0000000.000*kWh) // Negative active energy in first tariff (T1)
* > 2.8.2(0000000.001*kWh) // Negative active energy in second tariff (T2)
* > F.F(0000000) // Meter fatal error
* > ! // end-of-data
* (With "\r\n" everywhere.) */
parse_data_readout(&vals, data);
/* Ooh. The first samples are in! */
gauge.set_positive_active_energy_total(t, vals.values[OBIS_1_8_0]);
gauge.set_negative_active_energy_total(t, vals.values[OBIS_2_8_0]);
/* Keep this for debugging mostly. Bonus points if we also add current
* time 0.9.x */
Serial << F("on_data_readout: [") << identification << F("]: ") <<
data << C_ENDL;
ensure_wifi();
ensure_mqtt();
#ifdef HAVE_MQTT
// Use simple application/x-www-form-urlencoded format, except for
// the DATA bit (FIXME).
// FIXME: NOTE: This is limited to 256 chars in MqttClient.cpp
// (TX_PAYLOAD_BUFFER_SIZE).
// NOTE: We use String(mqtt_topic).c_str()) so you can use either
// PROGMEM or SRAM strings.
mqttClient.beginMessage(String(mqtt_topic).c_str());
mqttClient.print(F("device_id="));
mqttClient.print(guid);
// FIXME: move identification to another message; the one where we
// also add 0.9.1 and 0.9.2
mqttClient.print(F("&id="));
mqttClient.print(identification);
mqttClient.print(F("&DATA="));
// FIXME: replace CRLF in data with ", ". replace "&" with ";"
mqttClient.print(data); // FIXME: unformatted data..
mqttClient.endMessage();
#endif //HAVE_MQTT
}
static void on_response(const char *data, size_t end, Obis obis)
{
/* (0032835.698*kWh) */
Serial << F("on_response[") << Obis2str(obis) << F("]: ") <<
data << C_ENDL;
if ((obis == OBIS_1_8_0 || obis == OBIS_2_8_0) && (
end == 17 && data[0] == '(' && data[8] == '.' &&
memcmp_P(data + 12, F("*kWh)"), 5) == 0)) {
unsigned long t = millis();
long watthour = atol(data + 1) * 1000 + atol(data + 9);
if (obis == OBIS_1_8_0) {
gauge.set_positive_active_energy_total(t, watthour);
} else if (obis == OBIS_2_8_0) {
gauge.set_negative_active_energy_total(t, watthour);
}
}
}
/**
* Publish the latest data.
*
* Map:
* - 1.8.0 = e_pos_act_energy_wh = Positive active energy [Wh]
* - 2.8.0 = e_neg_act_energy_wh = Negative active energy [Wh]
* - 1.7.0 = e_pos_inst_power_w = Positive active instantaneous power [Watt]
* - 2.7.0 = e_neg_inst_power_w = Negative active instantaneous power [Watt]
*/
void publish()
{
ensure_wifi();
ensure_mqtt();
Serial <<
F("pushing: [1.8.0] ") << gauge.get_positive_active_energy_total() <<
F(" Wh, [2.8.0] ") << gauge.get_negative_active_energy_total() <<
F(" Wh, [16.7.0] ") << gauge.get_instantaneous_power() <<
F(" Watt" S_ENDL);
#ifdef HAVE_MQTT
// Use simple application/x-www-form-urlencoded format.
// NOTE: We use String(mqtt_topic).c_str()) so you can use either
// PROGMEM or SRAM strings.
mqttClient.beginMessage(String(mqtt_topic).c_str());
mqttClient.print(F("device_id="));
mqttClient.print(guid);
mqttClient.print(F("&e_pos_act_energy_wh="));
mqttClient.print(gauge.get_positive_active_energy_total());
mqttClient.print(F("&e_neg_act_energy_wh="));
mqttClient.print(gauge.get_negative_active_energy_total());
mqttClient.print(F("&e_inst_power_w="));
mqttClient.print(gauge.get_instantaneous_power());
mqttClient.print(F("&dbg_uptime="));
mqttClient.print(millis());
#ifdef OPTIONAL_LIGHT_SENSOR
mqttClient.print(F("&dbg_pulse="));
mqttClient.print(pulse_low);
mqttClient.print(F(".."));
mqttClient.print(pulse_high);
#endif //OPTIONAL_LIGHT_SENSOR
mqttClient.endMessage();
#endif //HAVE_MQTT
}
template<class T> static inline void serial_print_cescape(const T *p)
{
char buf[200]; /* watch out, large local variable! */
const T *restart = p;
do {
restart = cescape(buf, restart, 200);
Serial << buf;
} while (restart != NULL);
Serial << C_ENDL;
}
template<class T> static inline void iskra_tx(const T *p)
{
/* According to spec, the time between the reception of a message
* and the transmission of an answer is: between 200ms (or 20ms) and
* 1500ms. So adding an appropriate delay(200) before send should be
* sufficient. */
delay(20); /* on my local ME-162, delay(20) is sufficient */
/* Delay before debug print; makes more sense in monitor logs. */
Serial << F(">> ");
serial_print_cescape(p);
iskra.print(p);
}
static inline void trace_rx_buffer()
{
#if defined(ARDUINO_ARCH_ESP8266)
/* On the ESP8266, the SoftwareSerial.available() never returns true
* consecutive times: that means that we'd end up doing the trace()
* for _every_ received character.
* And when we have (slow) 9600 baud on the debug-Serial, this messes
* up communication with the IR-Serial: some echoes appeared in a
* longer receive buffer).
* Solution: no tracing. */
#else
/* On the Arduino Uno, we will see this kind of receive buildup,
* but only during the 300 baud connect handshake.
* 13:43:46.625 -> << / (cont)
* 13:43:46.658 -> << /I (cont)
* 13:43:46.692 -> << /IS (cont)
* 13:43:46.725 -> << /ISK (cont)
* ... */
if (buffer_pos) {
/* No cescape() this time, for performance reasons. */
Serial << F("<< ") << buffer_data << F(" (cont)" S_ENDL);
}
#endif
}
#ifdef HAVE_MQTT /* and HAVE_WIFI */
/**
* Check that Wifi is up, or connect when not connected.
*/
static void ensure_wifi()
{
if (WiFi.status() != WL_CONNECTED) {
WiFi.begin(wifi_ssid, wifi_password);
for (int i = 30; i >= 0; --i) {
if (WiFi.status() == WL_CONNECTED) {
break;
}
delay(1000);
}
if (WiFi.status() == WL_CONNECTED) {
Serial << F("Wifi UP on \"") << wifi_ssid << F("\", Local IP: ") <<
WiFi.localIP() << C_ENDL;
} else {
Serial << F("Wifi NOT UP on \"") << wifi_ssid << F("\"." S_ENDL);
}
}
}
/**
* Check that the MQTT connection is up or connect if it isn't.
*/
static void ensure_mqtt()
{
mqttClient.poll();
if (!mqttClient.connected()) {
// NOTE: We use String(mqtt_broker).c_str()) so you can use either
// PROGMEM or SRAM strings.
if (mqttClient.connect(String(mqtt_broker).c_str(), mqtt_port)) {
Serial << F("MQTT connected: ") << mqtt_broker << C_ENDL;
} else {
Serial << F("MQTT connection to ") << mqtt_broker <<
F(" failed! Error code = ") << mqttClient.connectError() << C_ENDL;
}
}
}
#endif //HAVE_MQTT
/**
* C-escape, for improved serial monitor readability
*
* Returns non-NULL to resume if we stopped because of truncation.
*/
static const char *cescape(
char *buffer, const char *p, size_t maxlen, bool progmem)
{
char ch = '\0';
char *d = buffer;
const char *de = d + maxlen - 5;
while (d < de) {
if (progmem) {
ch = pgm_read_byte(p);
} else {
ch = *p;
}
if (ch == '\0') {
break;
}
if (ch < 0x20 || ch == '\\' || ch == '\x7f') {
d[0] = '\\';
d[4] = ' ';
switch (ch) {
#if 1
// Extension
case C_SOH: d[1] = 'S'; d[2] = 'O'; d[3] = 'H'; d += 4; break; // SOH
case C_STX: d[1] = 'S'; d[2] = 'T'; d[3] = 'X'; d += 4; break; // STX
case C_ETX: d[1] = 'E'; d[2] = 'T'; d[3] = 'X'; d += 4; break; // ETX
case C_ACK: d[1] = 'A'; d[2] = 'C'; d[3] = 'K'; d += 4; break; // ACK
case C_NAK: d[1] = 'N'; d[2] = 'A'; d[3] = 'K'; d += 4; break; // NAK
#endif
// Regular backslash escapes
case '\0': d[1] = '0'; d += 1; break; // 0x00 (unreachable atm)
case '\a': d[1] = 'a'; d += 1; break; // 0x07
case '\b': d[1] = 'b'; d += 1; break; // 0x08
case '\t': d[1] = 't'; d += 1; break; // 0x09
case '\n': d[1] = 'n'; d += 1; break; // 0x0a
case '\v': d[1] = 'v'; d += 1; break; // 0x0b
case '\f': d[1] = 'f'; d += 1; break; // 0x0c
case '\r': d[1] = 'r'; d += 1; break; // 0x0d
case '\\': d[1] = '\\'; d += 1; break; // 0x5c
// The rest in (backslash escaped) octal
default:
d[1] = '0' + (ch >> 6);
d[2] = '0' + ((ch & 0x3f) >> 3);
d[3] = '0' + (ch & 7);
d += 3;
break;
}
} else {
*d = ch;
}
++p;
++d;
}
*d = '\0';
return (ch == '\0') ? NULL : p;
}
/**
* Calculate and (optionally) check block check character (BCC)
*
* IEC 62056-21 block check character (BCC):
* - ISO/IEC 1155:1978 or DIN 66219: XOR of all values;
* - ISO/IEC 1745:1975: start XOR after first SOH or STX.
*
* Return values:
* >=0 Calculated value
* -1 No checkable data
* -2 No ETX found
* -3 Bad BCC value
*/
static int din_66219_bcc(const char *s)
{
const char *p = s;
char bcc = 0;
while (*p != '\0' && *p != C_SOH && *p != C_STX)
++p;
if (*p == '\0')
return -1; /* no checkable data */
while (*++p != '\0' && *p != C_ETX)
bcc ^= *p;
if (*p == '\0')
return -2; /* no end of transmission?? */
bcc ^= *p;
if (*++p != '\0' && bcc != *p)
return -3; /* block check char was wrong */
return bcc;
}
/**
* Convert string to Obis enum or OBIS_LAST if not found
*
* Example: str2Obis("1.8.0", 5) == OBIS_1_8_0
* Example: Obis2str[OBIS_1_8_0] == "1.8.0"
*/
static inline Obis str2Obis(const char *key, int keylen)
{
for (int i = 0; i < OBIS_LAST; ++i) {
if (memcmp_P(key, to_pgm_char_p(Obis_[i].pgm_str), keylen) == 0)
return (Obis)i;
}
return OBIS_LAST;
}
/**
* Parse data readout buffer and populate obis_values_t
*/
static void parse_data_readout(struct obis_values_t *dst, const char *src)
{
memset(dst, 0, sizeof(*dst));
while (*src != '\0') {
int len = 0;
const char *key = src;
while (*src != '\0' && *src != '(')
++src;
if (*src == '\0')
break;
len = (src++ - key);
int i = str2Obis(key, len);
if (i < OBIS_LAST) {
const char *value = src;
while (*src != '\0' && *src != ')')
++src;
if (*src == '\0')
break;
len = (src++ - value);
long lval = atol(value);
/* "0032826.545*kWh" */
if (len == 15 && value[7] == '.' &&
memcmp_P(value + 11, F("*kWh"), 4) == 0) {
lval = lval * 1000 + atol(value + 8);
}
dst->values[i] = lval;
}
while (*src != '\0' && *src++ != '\r')
;
if (*src++ != '\n')
break;
}
}
#ifdef TEST_BUILD
static int STR_EQ(const char *func, const char *got, const char *expected)
{
if (strcmp(expected, got) == 0) {
printf("OK (%s): \"\"\"%s\"\"\"\n", func, expected);
return 1;
} else {