1 /** @file
2     Blueline PowerCost Monitor protocol.
3 
4     Copyright (C) 2020 Justin Brzozoski
5 
6     This program is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10  */
11 
12 #include <stdlib.h>
13 #include "fatal.h"
14 #include "decoder.h"
15 
16 /**
17 BlueLine Innovations Power Cost Monitor, tested with BLI-28000.
18 
19 Much of the groundwork for this implementation was based on reading the source and notes from older
20 implementations, but this implementation was a fresh rewrite by Justin Brzozoski in 2020.  I would
21 not have been able to figure this out without the other implementations to look at, but I wanted an
22 implementation that didn't need to know the Kh factor or monitor ID ahead of time, which required
23 changes.
24 
25 Some references used include:
26 
27 https://github.com/merbanan/rtl_433/pull/38 - an abandoned pull request on rtl_433 by radredgreen
28 
29 https://github.com/CapnBry/Powermon433 - a standalone Arduino-based Blueline monitor
30 
31 http://scruss.com/blog/2013/12/03/blueline-black-decker-power-monitor-rf-packets/ - the blog post
32 where the other authors were trading notes in the comments
33 
34 The IR-reader/sensor will transmit 3 bursts every ~30 seconds.  The low-level encoding is on/off
35 keyed pulse-position modulation (OOK_PPM).  The on pulses are always 0.5ms, while the off pulses
36 are either 0.5ms for logic 1 or 1.0ms for logic 0.  Each burst is 32 bits long.  The pauses
37 between the 3 grouped bursts is roughly 100ms.
38 
39 Data is sent less significant byte first for multi-byte fields.
40 
41 The basic layout of all bursts is as follows:
42 
43 - First is a 1 byte header, which is always the value 0xFE.
44 - Second is a 2 byte payload, which is interpreted differently based on the two lowest bits of the first byte
45 - Finally is a 1 byte CRC, calculated across the 2 payload bytes (not the header)
46 
47 The CRC is a CRC-8-ATM with polynomial 100000111, but it may be required to modify the payload bytes before
48 calculating it depending on message type.
49 
50 There are 4 message types that can be indicated by the 2 lowest bits of the first payload byte:
51 - 0: ID message (payload is not offset)
52 - 1: power message (payload is offset)
53 - 2: temperature/status message (payload is offset)
54 - 3: energy message (payload is offset)
55 
56 For the ID message (0), the CRC can be calculated directly on the payload as sent, and when the payload is
57 interpreted as a 16-bit integer it gives the ID of the transmitter.  This message is sent when the
58 monitor is first powered on and if the button on the monitor is pressed briefly.  If the button on the
59 monitor is held for >10 seconds, the monitor will change it's ID and report the new one.
60 
61 While the transmitter ID's are 16-bit, none of them can have any of the two lowest bits set or they
62 would not be able to transmit their ID as message type 0, and when the offset is calculated (see below) they
63 would also change the message type.
64 
65 For the 3 other message types, the payload must be offset before calculating the CRC or
66 interpreting the data.  The offset is done by treating the whole payload as a single 16-bit integer and
67 then subtracting the ID of the transmitter.  After the offset is done, then the CRC may be calculated
68 and the payload may be interpreted.
69 
70 Note that if the transmitter's ID isn't known, the code can't easily determine if messages other than an
71 ID payload are good or bad, and can't interpret their data correctly.  However, if the "auto" mode is enabled,
72 the system can try to learn the transmitter's ID by various methods. (See USAGE HINTS below)
73 
74 For the power message (1), the offset payload gives the number of milliseconds gap between impulses for the most
75 recent impulses seen by the monitor.  To convert from this 'gap' to kilowatts, you will need your meter's
76 Kh value. The Kh value is written obviously on the front of most meters, and 1.0 and 7.2 are very common.
77 
78     kW = (3600/gap) * Kh
79 
80 Note that the 'gap' value clamps to a maximum of 65533 (0xFFFD), so there is a non-zero floor when calculating the
81 kW value using this report.  For example, with a Kh of 7.2, the lowest kW value you will ever see when monitoring
82 the 'gap' value is (3600/65533)*7.2 = 0.395kW.  If you need power monitoring for impulse rates slower than every
83 65.533 seconds to do things like confirm that your power consumption is 0kW, you need to monitor the impulse
84 counts and timing between energy messages (see 3 below).
85 
86 For the temperature message (2), the offset payload gives the temperature in an odd scaling in the last byte,
87 and has some flag bits in the first byte.  The only known flag bit is the battery.  rtl_433 handles scaling back to
88 degrees celsius automatically.
89 
90 For the energy message (3), the offset payload contains a continuously running power impulse accumulator. I'm
91 not sure if there is a way to reset the accumulator.  The intended way to use it is to remember the accumulator
92 value at the beginning of a time period, and then subtract that from the value at the end of the time period.  The
93 accumulator will roll over to 0 after 65535.
94 
95     kWh = 0.001 * (accumulated pulses) * Kh
96 
97 Since the Kh value on all meters can vary, we do not handle it in rtl_433 and just report the raw millisecond
98 gap and accumulated impulses as received directly from the monitor.
99 
100 ## Usage hints:
101 
102 The requirement of knowing the ID before being able to receive a message means that this decoder
103 will generally require a parameter to be useful.  When running in the default mode with no parameters,
104 the only message it is able to decode is the one that announces a monitor's ID.  So, assuming you can get to
105 the monitor to power cycle it or hit the button, this is the recommended method:
106 
107 - 1) Start rtl_433
108 - 2) Tap the button or power cycle the monitor
109 - 3) Look for the rtl_433 output indicating the BlueLine monitor ID and note the ID field
110 - 4) Stop rtl_433
111 - 5) Restart rtl_433, explicitly passing the ID as a parameter to this decoder
112 
113 For example, if you see the ID 45364 in step 3, you would start the decoder with a command like:
114 
115     rtl_433 -R 176:45364
116 
117 If you are unable to access the monitor to have it send the ID message, you can also use the "auto" parameter:
118 
119     rtl_433 -vv -R 176:auto
120 
121 Verbose mode should be specified first on the command line to see what the "auto" mode is doing.
122 
123 The auto parameter will try to brute-force the ID on any messages that look like they are from a
124 BlueLine monitor.  This method usually succeeds within a few minutes, but is likely to get false positives
125 if there is more than one monitor in range or the messages being received are all identical (i.e. if the
126 meter is continuously reporting 0 watts).  If it succeeds, it will start reporting data with the new ID,
127 which you should then use as a parameter when you re-run rtl_433 in the future.
128 
129 Finally, passing a parameter to this decoder requires specifying it explicitly, which normally disables all
130 other default decoders.  If you want to pass an option to this decoder without disabling all the other defaults,
131 the simplest method is to explicity exclude this one decoder (which implicitly says to leave all other defaults
132 enabled), then add this decoder back with a parameter.  The command line looks like this:
133 
134     rtl_433 -R -176 -R 176:45364
135 
136 */
137 
138 #define BLUELINE_BITLEN      32
139 #define BLUELINE_STARTBYTE   0xFE
140 #define BLUELINE_CRC_POLY    0x07
141 #define BLUELINE_CRC_INIT    0x00
142 #define BLUELINE_CRC_BYTELEN 2
143 #define BLUELINE_TXID_MSG        0x00
144 #define BLUELINE_POWER_MSG       0x01
145 #define BLUELINE_TEMPERATURE_MSG 0x02
146 #define BLUELINE_ENERGY_MSG      0x03
147 
148 #define BLUELINE_ID_STEP_SIZE 4
149 #define MAX_POSSIBLE_BLUELINE_IDS (65536/BLUELINE_ID_STEP_SIZE)
150 #define BLUELINE_ID_GUESS_THRESHOLD 4
151 
152 struct blueline_stateful_context {
153     unsigned id_guess_hits[MAX_POSSIBLE_BLUELINE_IDS];
154     uint16_t current_sensor_id;
155     unsigned searching_for_new_id;
156 };
157 
rev_crc8(uint8_t const message[],unsigned nBytes,uint8_t polynomial,uint8_t remainder)158 static uint8_t rev_crc8(uint8_t const message[], unsigned nBytes, uint8_t polynomial, uint8_t remainder) {
159     unsigned byte, bit;
160 
161     // Run a CRC backwards to find out what the init value would have been.
162     // Alternatively, put a known init value in the first byte, and it will
163     // return a value that could be used in that place to get that init.
164 
165     // This logic only works assuming the polynomial has the lowest bit set,
166     // Which should be true for most CRC polynomials, but let's be safe...
167     if ((polynomial & 0x01) == 0) {
168         fprintf(stderr,"Cannot run reverse CRC-8 with this polynomial!\n");
169         return 0xFF;
170     }
171     polynomial = (polynomial >> 1) | 0x80;
172 
173     byte = nBytes;
174     while (byte--) {
175         bit = 8;
176         while (bit--) {
177             if (remainder & 0x01) {
178                 remainder = (remainder >> 1) ^ polynomial;
179             } else {
180                 remainder = remainder >> 1;
181             }
182 
183         }
184         remainder ^= message[byte];
185     }
186     return remainder;
187 }
188 
guess_blueline_id(r_device * decoder,const uint8_t * current_row)189 static uint16_t guess_blueline_id(r_device *decoder, const uint8_t *current_row)
190 {
191     struct blueline_stateful_context *const context = decoder->decode_ctx;
192     const uint16_t start_value = ((current_row[2] << 8) | current_row[1]);
193     const uint8_t recv_crc = current_row[3];
194     const uint8_t rcv_msg_type = (current_row[1] & 0x03);
195     uint16_t working_value;
196     uint8_t working_buffer[2];
197     uint8_t reverse_crc_result;
198     uint16_t best_id;
199     unsigned best_hits;
200     unsigned num_at_best_hits;
201     unsigned high_byte_steps;
202 
203     // TL;DR - Try all possible IDs against every incoming message, and count how many times each one
204     // succeeds.  If one of them passes a threshold, assume it must be the right one and return it.
205 
206     // We do some optimizations to try and do all those checks quickly, but it's still about the
207     // same as doing a CRC across 512 bytes of data for every 2 byte payload received.
208 
209     working_buffer[0] = BLUELINE_CRC_INIT;
210     working_buffer[1] = current_row[2];
211     high_byte_steps = 256;
212     best_id = 0;
213     best_hits = 0;
214     num_at_best_hits = 0;
215     while (high_byte_steps--) {
216         reverse_crc_result = rev_crc8(working_buffer, BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, recv_crc);
217         // Would this byte value have been usable while still being the same type of message we received?
218         if ((reverse_crc_result & 0x03) == rcv_msg_type) {
219             working_value = ((working_buffer[1] << 8) | reverse_crc_result);
220             working_value = start_value - working_value;
221             context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)]++;
222             if (context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)] >= best_hits) {
223                 if (context->id_guess_hits[(working_value/BLUELINE_ID_STEP_SIZE)] > best_hits) {
224                     best_hits = context->id_guess_hits[(working_value / BLUELINE_ID_STEP_SIZE)];
225                     best_id = working_value;
226                     num_at_best_hits = 1;
227                 } else {
228                     num_at_best_hits++;
229                 }
230             }
231         }
232         working_buffer[1] += 1;
233     }
234 
235     if (decoder->verbose) {
236         fprintf(stderr, "Attempting Blueline autodetect: best_hits=%u num_at_best_hits=%u\n", best_hits, num_at_best_hits);
237     }
238     return ((best_hits >= BLUELINE_ID_GUESS_THRESHOLD) && (num_at_best_hits == 1)) ? best_id : 0;
239 }
240 
blueline_decode(r_device * decoder,bitbuffer_t * bitbuffer)241 static int blueline_decode(r_device *decoder, bitbuffer_t *bitbuffer)
242 {
243     struct blueline_stateful_context *const context = decoder->decode_ctx;
244     data_t *data;
245     int row_index;
246     uint8_t *current_row;
247     int payloads_decoded = 0;
248     int most_applicable_failure = 0;
249     uint8_t calc_crc;
250     uint16_t offset_payload_u16 = 0;
251     uint8_t offset_payload_u8[BLUELINE_CRC_BYTELEN] = {0};
252 
253     // Blueline uses inverted 0/1
254     bitbuffer_invert(bitbuffer);
255 
256     // Look at each row we just received independently
257     for (row_index = 0; row_index < bitbuffer->num_rows; row_index++) {
258         current_row = bitbuffer->bb[row_index];
259 
260         // All valid rows will have a fixed length and start with the same byte
261         if ((bitbuffer->bits_per_row[row_index] != BLUELINE_BITLEN) || (current_row[0] != BLUELINE_STARTBYTE)) {
262             if (DECODE_ABORT_LENGTH < most_applicable_failure) {
263                 most_applicable_failure = DECODE_ABORT_LENGTH;
264             }
265             continue;
266         }
267 
268         // We need to know which type of message to decide how to check CRC
269         const unsigned message_type = (current_row[1] & 0x03);
270         const uint8_t recv_crc = current_row[3];
271 
272         if (message_type == BLUELINE_TXID_MSG) {
273             // No offset required before CRC or data handling
274             calc_crc = crc8(&current_row[1], BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, BLUELINE_CRC_INIT);
275         } else {
276             // Offset required before CRC or datahandling
277             offset_payload_u16 = ((current_row[2] << 8) | current_row[1]) - context->current_sensor_id;
278             offset_payload_u8[0] = (offset_payload_u16 & 0xFF);
279             offset_payload_u8[1] = (offset_payload_u16 >> 8);
280             calc_crc = crc8(&offset_payload_u8[0], BLUELINE_CRC_BYTELEN, BLUELINE_CRC_POLY, BLUELINE_CRC_INIT);
281         }
282 
283         // If the CRC didn't match up, ignore this row!
284         if (calc_crc != recv_crc) {
285             if ((context->searching_for_new_id) && (message_type != BLUELINE_TXID_MSG)) {
286                 uint16_t id_guess = guess_blueline_id(decoder, current_row);
287                 if (id_guess != 0) {
288                     if (decoder->verbose) {
289                         fprintf(stderr,"Switching to auto-detected Blueline ID %u\n", id_guess);
290                     }
291                     context->current_sensor_id = id_guess;
292                     context->searching_for_new_id = 0;
293                 }
294             }
295             if (DECODE_FAIL_MIC < most_applicable_failure) {
296                 most_applicable_failure = DECODE_FAIL_MIC;
297             }
298             continue;
299         }
300 
301         if (message_type == BLUELINE_TXID_MSG) {
302             const uint16_t received_sensor_id = ((current_row[2] << 8) | current_row[1]);
303             /* clang-format off */
304             data = data_make(
305                     "model",        "",             DATA_STRING, "Blueline-PowerCost",
306                     "id",           "",             DATA_INT,    received_sensor_id,
307                     "mic",          "Integrity",    DATA_STRING, "CRC",
308                     NULL);
309             /* clang-format on */
310             decoder_output_data(decoder, data);
311             payloads_decoded++;
312             if (context->searching_for_new_id) {
313                 if (decoder->verbose) {
314                     fprintf(stderr,"Switching to received Blueline ID %u\n", received_sensor_id);
315                 }
316                 context->current_sensor_id = received_sensor_id;
317                 context->searching_for_new_id = 0;
318             }
319         } else if (message_type == BLUELINE_POWER_MSG) {
320             const uint16_t ms_per_pulse = offset_payload_u16;
321             /* clang-format off */
322             data = data_make(
323                     "model",        "",             DATA_STRING, "Blueline-PowerCost",
324                     "id",           "",             DATA_INT,    context->current_sensor_id,
325                     "gap",          "",             DATA_INT,    ms_per_pulse,
326                     "mic",          "Integrity",    DATA_STRING, "CRC",
327                     NULL);
328             /* clang-format on */
329             decoder_output_data(decoder, data);
330             payloads_decoded++;
331         } else if (message_type == BLUELINE_TEMPERATURE_MSG) {
332             // TODO - Confirm battery flag is working properly
333 
334             // These were the estimates from Powermon433.
335             // But they didn't line up perfectly with my LCD display.
336             //
337             // A: deg_f = 0.823 * recvd_temp - 28.63
338             // B: deg_c = 0.457 * recvd_temp - 33.68
339 
340             // I logged raw radio values and their resulting display temperatures
341             // for a range of -13 to 34 degrees C, and it's not perfectly linear.
342             // It's not so far off that I think we should use something other than
343             // a linear fit, but it's likely got some fixed-point truncation errors
344             // in the official display code.
345             //
346             // I put all the points I had into Excel and asked for the best
347             // linear estimate, and it gave me roughly this:
348             //
349             // deg_C = 0.436 * recvd_temp - 30.36
350 
351             // In case anyone else wants to continue try and find a better equation,
352             // a full copy of my logged data is in the comments of this GitHub pull
353             // request:
354             //
355             // https://github.com/merbanan/rtl_433/pull/1590
356 
357             const uint8_t temperature = offset_payload_u8[1];
358             const uint8_t flags = offset_payload_u8[0] >> 2;
359             const uint8_t battery = (flags & 0x20) >> 5;
360             const float temperature_C = (0.436 * temperature) - 30.36;
361             /* clang-format off */
362             data = data_make(
363                     "model",            "",             DATA_STRING, "Blueline-PowerCost",
364                     "id",               "",             DATA_INT,    context->current_sensor_id,
365                     "flags",            "",             DATA_FORMAT, "%02x", DATA_INT, flags,
366                     "battery_ok",       "Battery",      DATA_INT,    !battery,
367                     "temperature_C",    "",             DATA_DOUBLE, temperature_C,
368                     "mic",              "Integrity",    DATA_STRING, "CRC",
369                     NULL);
370             /* clang-format on */
371             decoder_output_data(decoder, data);
372             payloads_decoded++;
373         } else { // Assume BLUELINE_ENERGY_MSG
374             // (The lowest two bits of the pulse count will always be the same because message_type is overlaid there)
375             const uint16_t pulses = offset_payload_u16;
376             /* clang-format off */
377             data = data_make(
378                     "model",            "",             DATA_STRING, "Blueline-PowerCost",
379                     "id",               "",             DATA_INT, context->current_sensor_id,
380                     "impulses",         "",             DATA_INT,    pulses,
381                     "mic",              "Integrity",    DATA_STRING, "CRC",
382                     NULL);
383             /* clang-format on */
384             decoder_output_data(decoder, data);
385             payloads_decoded++;
386         }
387     }
388 
389     return ((payloads_decoded > 0) ? payloads_decoded : most_applicable_failure);
390 }
391 
392 static char *output_fields[] = {
393         "model",
394         "id",
395         "flags",
396         "gap",
397         "impulses",
398         "battery_ok",
399         "temperature_C",
400         "mic",
401         NULL,
402 };
403 
404 r_device blueline;
405 
blueline_create(char * arg)406 static r_device *blueline_create(char *arg)
407 {
408     r_device *r_dev = create_device(&blueline);
409     if (!r_dev) {
410         fprintf(stderr, "blueline_create() failed\n");
411         return NULL; // NOTE: returns NULL on alloc failure.
412     }
413 
414     struct blueline_stateful_context *context = malloc(sizeof(*context));
415     if (!context) {
416         WARN_MALLOC("blueline_create()");
417         free(r_dev);
418         return NULL; // NOTE: returns NULL on alloc failure.
419     }
420     memset(context,0,sizeof(*context));
421     r_dev->decode_ctx = context;
422 
423     if (arg != NULL) {
424         if (strcmp(arg, "auto") == 0) {
425             // Setup for auto identification
426             context->searching_for_new_id = 1;
427             //fprintf(stderr, "Blueline decoder will try to autodetect ID.\n");
428         } else {
429             // Assume user is trying to pass in hex ID
430             context->current_sensor_id = strtoul(arg, NULL, 0);
431             //fprintf(stderr, "Blueline decoder using ID %u\n", context->current_sensor_id);
432         }
433     }
434 
435     return r_dev;
436 }
437 
438 r_device blueline = {
439         .name        = "BlueLine Innovations Power Cost Monitor",
440         .modulation  = OOK_PULSE_PPM,
441         .short_width = 500,
442         .long_width  = 1000,
443         .gap_limit   = 2000,
444         .reset_limit = 8000,
445         .decode_fn   = &blueline_decode,
446         .create_fn   = &blueline_create,
447         .disabled    = 0,
448         .fields      = output_fields,
449 };
450