emporia-vue-local / emporia-vue2-reversing

MIT License
53 stars 12 forks source link

Decoding sensor data #1

Open krconv opened 2 years ago

krconv commented 2 years ago

Repurposing this issue for the discussion that it started. Was originally a question about connecting, and turned into a discussion about deciding the sensor data.


In the blog post, you mentioned that one of your USB-Serial adaptors wasn't working when you tried to connect to the ESP32. I'm wondering if I'm hitting the same issue; I only have one serial adaptor to test with at the moment though.

I'm using the FT232RL FTDI Mini USB to TTL Serial Converter, and getting nothing while trying to connect:

> esptool.py --port /dev/cu.usbserial-A50285BI read_flash 0 0x400000 flash_contents.bin
esptool.py v2.8
Serial port /dev/cu.usbserial-A50285BI
Connecting........_____....._____....._____....._____....._____....._____....._____

A fatal error occurred: Failed to connect to Espressif device: Timed out waiting for packet header

Process I'm taking to enter flashing mode:

  1. Power with 3.3v
  2. Pull IO0 pin to GND
  3. Pull EN pin to GND, and release
  4. Release IO0 pin

But that's not working; my only thought is that maybe there is something else connected with TX/RX that is interfering? And a different serial converter could sort through the noise?

Any guidance would be appreciated!


Edit by @flaviut for visibility:

There is now a ESPHome component. You can follow development at

krconv commented 2 years ago

Actually, I had TX and RX backwards; TX on the debug header goes to RX on the ESP32. My bad (you mentioned it in the blog post and I missed it). Just successfully downloaded the flash and got debug logs

flaviut commented 2 years ago

Ha! This stuff happens, glad you got it figured out! Please feel free to ask anything else, and let me know if you end up building something on top of all this!

krconv commented 2 years ago

My main goal is to build an esphome integration. I've never had to fully reverse a chip before, but it feel like a fun challenge that I might be able to help with, and I like the small form-factor and low price of the Vue. I'd appreciate any pointers if you have any; currently waiting for a USB oscilloscope to help with identifying what each of the pins are doing.

flaviut commented 2 years ago

Sure! First off, oscilloscopes are very helpful for identifying the function of things, but when it comes to actually reading digital data you want a logic analyzer.

Anyway, please do poke at it, it's a lot of fun. If you'd like though, I briefly described the physical communication on the board here: https://flaviutamas.com/2021/reversing-emporia-vue-2#on-board-communications

There's another chip on the board that communicates with the ESP32 over I2C. The raw I2C message gets dumped over MQTT as hex here: https://github.com/flaviut/emporia-vue2-reversing/blob/master/example_messages.txt#L2-L19. Note that right below that hex data are the output results: ~the I2C data is combined with the calibration data stored in NVS to produce the final output:~ The calibration data in NVS is nulled and not used at all.

diagram showing data flow

flaviut commented 2 years ago

By the way, I've given you write access to this repo! Please feel free to do whatever you wish with that access, but let's keep Emporia-copyrighted stuff off of here to avoid causing them problems :)

Maelstrom96 commented 2 years ago

Hey - I'm actually working on the exact same goal as @krconv. I'm mostly trying to make it so I can add a separate ESP32 running ESPHome that would read the messages on the i2c bus and publish that data to MQTT or through the native ESPHome API, without the need to flashing the onboard module. The reason being that I like having the data in their UI, but I also want to be able to do second triggers on the energy consumption. At first I was thinking about making a MQTT bridge to their AWS IOT mqtt endpoint, but I wasn't able to reverse engineer the certificates/auth method.

Because of this, I've been working on reverse engineering the i2c message data and I think I mostly have it figured out at this point. Here is my data spreadsheet I used to figure out / validate the parsing logic: https://docs.google.com/spreadsheets/d/1Yu98tDRhFJqqMntNak6X297SKyzQtf0_Cvv_g7qKewk/edit?usp=sharing.

The only data missing from the message is the calibration - which is only on the esp32 - and the voltage frequency.

Edit: Forgot to say, thanks @flaviut for the post and the data in the repo, it was immensely useful. Also, I updated my Vue2 firmware to the last version (Vue2-1612298550, Tue Feb 2 20:42:33 UTC 2021) and it's still outputting logs and the TEST_MODE is still there and it looks like it's still working.

flaviut commented 2 years ago

WOW! This is incredible! I'm curious about how you went about doing this, I did spend a while looking at things in a hex editor, but didn't really get anywhere.

flaviut commented 2 years ago

It took me a bit to understand the spreadsheet, but now that I do, I've written up a C declaration for that data:

// all data is little-endian
struct __attribute__((__packed__)) ch_power {
    int32_t ph1, ph2, ph3;
}

struct __attribute__((__packed__)) vue2_i2c_data {
    uint32_t unknown1;
    ch_power power[19];
    int16_t ph_voltage[3];
    int16_t frequency;
    int16_t ph2_offset_deg;
    int16_t ph3_offset_deg;
    int16_t current[19];
    uint16_t unknown2;
}

One thing that I suspect is going on here (from my use of the Vue 2 locally) is a non-1:1 mapping of channel numbers to data indexes. I think, but am not sure, that some of the indexes here do not match up with the numbers printed on the box.

Maelstrom96 commented 2 years ago

I'll start off by saying that I'm mostly out of my league when it comes down to reverse engineering things, but I wanted to give it a shot anyway.

Input P[V] values

The first thing I did was to manually check the debug/v2 message that you had in your post to see if I could find any pattern. An obvious one was the amount of FFFF value bytes separated by 2 bytes of data. I also noticed that all the P[V] values were small-ish negative ones. Doing a find and count for FFFF in Notepad++ returned 57 match. Dividing that value with the 3 P[V] per input results in 19 - the same amount of input we have in the logs. That block was then clearly the data for all the input power values.

I copied the first 12 bytes (which should correspond to the first 3 P[V] of I01) in this amazing hex tool and started to check for other patterns in the data. I also copied another 12 bytes from another message with positive values, and the values seemed to correlate when parsed as INT32 (DCBA).

I wasn't sure what to do then, since the decimal values weren't the same as the expected values. I decided to plot values from multiple inputs on a graph to see if there was a linear equation hidden somewhere.

Here's what it gave me : 2021-11-22_23h44_31

As you can see, it does look like it's linear, but calculating the actual expression gave me a pretty significant offset (which can also be seen on the graph since the line is not going through 0). A small offset is expected since the values are most likely rounded, but the result is too much. Also, by looking at the graph, we can see that there are 2 general groups. I was able to see that the slope value was different between I01-I03 and I04+.

After thinking about it, and checking the points (expected value and RAW decimal) on the graph, I noticed that not all the points were exactly on the trend line, so I started to wonder if the calibration numbers could be at play, since it would explain the deviation. I decided to multiply the values by their respective calibration and plot those values.

Here's the graph after that : 2021-11-22_23h48_04

As you can see, the deviation became negligible, and the offset was almost 0 (still some but very small, > 0.03). This told me I should now be able to calculate the slope and then use that to get the P[V] value. The slope 5.5% for the first 3 inputs, and 22% for the rest.

Voltage ADC

After that, I was wondering what other values I could find, and decided to enter the next couple of bytes into the HEX tool, and got three values that I could spot easily in the debug output:

pos | Raw | INT16 -- | -- | -- 0 | 00 0F | **15** 2 | 00 0C | **12** 4 | 00 0B | **11**
V1:   0.3,  0.4 Hz, *15*, 0.0225336
V2:   0.3, 0 degrees, *12*, 0.0220000
V3:   0.2, 0 degrees, *11*, 0.0220000

I wasn't sure what those values where, but I decided to see if multiplying this with the calibration data would actually give the voltage, and it actually did. So I figure this is a voltage ADC value.

Input Current?

At this point, the amount of bytes of data was pretty low, not enough for 19 x int32, I also noticed that the voltage ADC was an int16, so I started to see if I could notice any pattern with 19 x int16 in a row. It was pretty easy to spot since all the values were pretty much the same, see for yourself :

0050 0050 0050 FD4F FD4F FD4F FD4F FD4F 0350 0350 0350 FD4F FD4F FD4F FD4F FD4F 0350 0350 0350

I01:  372.4, P[V1]:   -80.0, P[V2]:   -21.9, P[V3]:    -4.9
I02:  372.4, P[V1]:   -80.0, P[V2]:   -21.9, P[V3]:    -4.9
I03:  372.4, P[V1]:   -80.0, P[V2]:   -21.9, P[V3]:    -4.9
I04:   93.1, P[V1]:   -19.4, P[V2]:    -5.2, P[V3]:    -0.9
I05:   93.1, P[V1]:   -20.3, P[V2]:    -5.6, P[V3]:    -1.7
I06:   93.1, P[V1]:   -20.5, P[V2]:    -5.8, P[V3]:    -1.3
I07:   93.1, P[V1]:   -19.2, P[V2]:    -4.4, P[V3]:    -2.4
I08:   93.1, P[V1]:   -20.0, P[V2]:    -5.7, P[V3]:    -0.7
I09:   93.1, P[V1]:   -20.9, P[V2]:    -4.8, P[V3]:    -1.0
I10:   93.1, P[V1]:   -19.4, P[V2]:    -6.2, P[V3]:    -1.0
I11:   93.1, P[V1]:   -20.1, P[V2]:    -6.1, P[V3]:    -0.8
I12:   93.1, P[V1]:   -19.4, P[V2]:    -5.2, P[V3]:    -0.9
I13:   93.1, P[V1]:   -20.3, P[V2]:    -5.6, P[V3]:    -1.7
I14:   93.1, P[V1]:   -20.5, P[V2]:    -5.8, P[V3]:    -1.3
I15:   93.1, P[V1]:   -19.2, P[V2]:    -4.4, P[V3]:    -2.4
I16:   93.1, P[V1]:   -20.0, P[V2]:    -5.7, P[V3]:    -0.7
I17:   93.1, P[V1]:   -20.9, P[V2]:    -4.8, P[V3]:    -1.0
I18:   93.1, P[V1]:   -19.4, P[V2]:    -6.2, P[V3]:    -1.0
I19:   93.1, P[V1]:   -20.1, P[V2]:    -6.1, P[V3]:    -0.8

I wasn't sure why there were 2 different values FD4F & 0350 for what looked like 93.1, but I was still pretty sure that they were the right bytes. I tried to multiply one of the values with it's input offset annnnnnnnnnnd:

0x0050 = 20480

20480 x 5.5 = 112,640

Well, that doesn't look anything close to 372.4... How about a division?

20480 / 5.5 = 3,723.63636363636[...]

That was mostly pure luck to find that, but it looks like it is indeed how it's calculated. Just need to shift the period and we're golden!

Voltage degrees

I know it's pretty useless and not needed, but at this point I feel like superman, so I wanted to see If I could crack the code.

The amount of bytes left is pretty limited : 03EC 52EA [...] A601 8E00 0000 [...] 0000

There's only a single block with 3 int16, so it's only logical that A601 8E00 0000 are the right ones, but converting them didn't really give useful numbers :

V1: 120.2, 61.6 Hz, 5241 ADC, 0.0229308 Calibration
V2: 121.3, 121 degrees, 5574 ADC, 0.0217630 Calibration
V3:   8.1, 0 degrees, 369 ADC, 0.0220000 Calibration
pos | Raw | INT16 -- | -- | -- 0 | 01 A6 | 422 2 | 00 8E | 142 4 | 00 00 | 0

The 0 degree made sense, but how about the 142? How does that become 121 degrees? I wondered if the values were a ratio of 360Β°. Calculating it actually gives the right value.

142       x
---  =  ----
422     360

x = 360 x 142 / 422
x = 121.13[...]

I haven't figured out if the voltage frequency is in the data, but I don't think it is, since the first 4 bytes seems to be some kind of counter, and the last 2 bytes are always 0x0000. It could actually be the 422 value, but I would need to have more data with variations to confirm it.

Maelstrom96 commented 2 years ago

One thing that I suspect is going on here (from my use of the Vue 2 locally) is a non-1:1 mapping of channel numbers to data indexes. I think, but am not sure, that some of the indexes here do not match up with the numbers printed on the box.

If you have a message that you think doesn't match the mapping, let me know. I don't believe that it will deviate from this structure but we never know.

Maelstrom96 commented 2 years ago

@flaviut I wanted to know - do you happen to know the i2c frequency? I'm trying to connect to it but I'm not having much luck at 50kHz or 100kHz.

flaviut commented 2 years ago

@Maelstrom96 I believe it's 100kHz. I uploaded a dump from sigrok pulseview to https://github.com/flaviut/emporia-vue2-reversing/blob/master/i2c%20dump.bin, and unless the time base is off in that, it shows 100kHz.

2021-11-23-221659_950x909_scrot

That hopefully the screenshot will also explain how it expects to be asked for data.

krconv commented 2 years ago

This is awesome! I just had fun decoding the message too, and it's cool to see that I landed in the same place. I used Python to help me walk through the message, activating each of the inputs and finding the bytes that changed. In case your curious, this is my helper script. I was just about do try to figure out the calibration as well, and ended up with a linear relationship (just eye-balled it, didn't verify it was exactly linear) between points on I04[V2], similar to you.

From my testing, I'm pretty sure the uint16 with offset 0x00EE is the frequency; it was sandwiched between the AC voltages and degrees, and it was consistently changing with the frequency output in MQTT. The conversion for my device is 0.0014268004584473604 * <sensor_value> + 0.4 = frequency; tried it on the example messages @flaviut and it was a little off, but I think that is due to a different calibration.

For calibration data; I think we could do the same thing that the existing CT clamp sensor in ESPhome does, where whoever is flashing the device would need to manually calibrate each of the sensors. Maybe we could reverse engineer the calibration data from nvs, and make that easier? I think it's worth skipping that for now.

@Maelstrom96 Would you want to work together on the integration? I'm not familiar with building an ESPHome integration, are you? I am familiar with C++ and think I could make faster progress on the integration though if you want to keep looking into the connections on the board because I'm not as familiar with the signal processing side of things.

krconv commented 2 years ago

And I'm pretty excited...I feel like this will be a huge improvement for cost effective energy monitoring if we can get it to work

krconv commented 2 years ago

Reopening for visibility, to make it easier to find for others interested

Maelstrom96 commented 2 years ago

@krconv I'm not really familiar with ESPHome components. I tried to see how ESPHome handles I2C devices and I'm not sure if we're going to have an issue with the TwoWire lib that they're using. The reason being that is looks like requests read buffer is limited to 32 bytes with TwoWire, and we need to query the full 284 bytes all at once.

krconv commented 2 years ago

I've been trying to learn how various ESPHome components work. I think under the hoods, i2c will use the TwoWire library by default, but we can configure it to talk to the onboard i2c controller instead and I'm guessing that's how the the original software does it.

I'm not sure what other implications this has, but this is an example from here to tell ESPHome to use the native ESP API:

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf
    version: recommended
Maelstrom96 commented 2 years ago

We'll most likely have an issue with https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.h too. And if you look at Wire.cpp, the requestFrom() implementation cast down the "len" value to an uint_8, which doesn't go high enough for our 284 bytes.

krconv commented 2 years ago

I tried out that approach, and agreed I don't think it's much of an option. I think setting the framework type to esp-idf will cause it to load this driver (instead of any of the Arduino ones), and from what I can tell it doesn't have any internal buffers, which I think is good. But, setting the framework type also causes everything to attempt to compile using the native framework, and there isn't one for MQTT according to esphome, so validation throws this error:

image

From what I understand, Arduino is a generic framework, and arduino-esp32 are the bindings written to port Arduino to ESP32. ESPHome just barely added support for using the native esp-idf framework in release 2021.10. It seems like if we could get @flaviut's PR merged to the esp8266 and esp32 Arduino libraries, we could keep doing I2C through Arduino, or if we have enough support on esp-idf, we could go in that direction (because this component would only need to be used on esp32 anyways).

Also, I've been thinking about a potential Config format; have either of you tried that out yet? Any thoughts on this idea?

Config Idea
sensor:
  - platform: emporia_vue
    update_interval: 2s
    phases:
      - id: phase_a
        phase_input: BLACK # i.e. the color of the wire that is connected to the phase
      - id: phase_b
        phase_input: RED
    power:
      - id: total_phase_a
        phase_id: phase_a # not sure how the factory software figures out which phase to use; this makes it explicit
        ct_input: A # uses A-C, 1-16 as labeled on the outside of the Vue
        filters: # and each one would have a separate calibration
          - calibrate_linear:
            - 0 -> 0
            - 2393 -> 2.0
      - id: kitchen_outlets_power
        phase_id: phase_a
        ct_input: "1"
        filters:
          - calibrate_linear:
            - 0 -> 0
            - 2393 -> 2.0
      - id: stove_power
        name: Stove Power
        phase_id: phase_b
        ct_input: "2"
        filters:
          - calibrate_linear:
            - 0 -> 0
            - 3438 -> 2.0
          - lambda: return x * 2;
 

I've been testing out config validation on my fork using this external component:

external_components:
  - source: github://krconv/esphome@add-emporia-vue
    components: [ emporia_vue ]
    refresh: 1min

I haven't tested anything on hardware yet, but my next step might be to connect a spare ESP32's i2c port to the Vue and see if it can talk to the sensor at all with the esp-idf library

flaviut commented 2 years ago

cal_data is a false lead--the calibration data is internal to the ESP32 for their wifi hardware.

The real calibration constants are cReal and cApparent.

However, the values here are surprising. The interesting data is marked as erased, while the uninteresting version is not:

      {
        "entry_state": "Erased",
        "entry_key": "cReal",
        "entry_data": "AAAgKJkOI0EAAOC+axEjQQAA8OEE2SFBAACAbhjnAkEAAHCC3dECQQAAhAty2wJBAADg+pYAA0EAAKhAYtoCQQAA2O8s5wJBAAAkLELwAkEAADA5JMoBQQAAtLRw6wJBAAC4VIYKA0EAACjF59kCQQAAYHSiBwNBAAAwksz1AkEAALj8Wt8CQQAA6OxK6AJBAAAQYf0SA0E="
      },
      {
        "entry_state": "Erased",
        "entry_key": "cApparent",
        "entry_data": "AACA/dMMwEAAAACbBxDAQAAAgB+DMcFAAAAAb/v/n0AAAACo+dufQAAAABPd7p9AAACAwU0VoEAAAACnb+ifQAAAAAaV/Z9AAACAftMIoEAAAIAUqTShQAAAAJyuA6BAAAAAsfAboEAAAACm7+ifQAAAgOacGaBAAAAAIswKoEAAAAB68PKfQAAAAEllAaBAAACAiX0koEA="
      },
      {
        "entry_state": "Erased",
        "entry_key": "cReal",
        "entry_data": "AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEA="
      },
      {
        "entry_state": "Written",
        "entry_key": "cApparent",
        "entry_data": "AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEA="
      },

Decoding these gets you basically what you expect: 19 entries, the first 3 of which are different than the rest.

$ base64 -d | xxd
AAAgKJkOI0EAAOC+axEjQQAA8OEE2SFBAACAbhjnAkEAAHCC3dECQQAAhAty2wJBAADg+pYAA0EAAKhAYtoCQQAA2O8s5wJBAAAkLELwAkEAADA5JMoBQQAAtLRw6wJBAAC4VIYKA0EAACjF59kCQQAAYHSiBwNBAAAwksz1AkEAALj8Wt8CQQAA6OxK6AJBAAAQYf0SA0E=
00000000: 0000 2028 990e 2341 0000 e0be 6b11 2341  .. (..#A....k.#A
00000010: 0000 f0e1 04d9 2141 0000 806e 18e7 0241  ......!A...n...A
00000020: 0000 7082 ddd1 0241 0000 840b 72db 0241  ..p....A....r..A
00000030: 0000 e0fa 9600 0341 0000 a840 62da 0241  .......A...@b..A
00000040: 0000 d8ef 2ce7 0241 0000 242c 42f0 0241  ....,..A..$,B..A
00000050: 0000 3039 24ca 0141 0000 b4b4 70eb 0241  ..09$..A....p..A
00000060: 0000 b854 860a 0341 0000 28c5 e7d9 0241  ...T...A..(....A
00000070: 0000 6074 a207 0341 0000 3092 ccf5 0241  ..`t...A..0....A
00000080: 0000 b8fc 5adf 0241 0000 e8ec 4ae8 0241  ....Z..A....J..A
00000090: 0000 1061 fd12 0341                      ...a...A
$ base64 -d | xxd
AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEA=
00000000: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000010: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000020: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000030: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000040: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000050: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000060: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000070: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000080: 0000 0000 0000 2440 0000 0000 0000 2440  ......$@......$@
00000090: 0000 0000 0000 2440                      ......$@

I bet that these can be decoded as:

struct __attribute__((__packed__)) calib_entry {
    int32_t offset, slope;
}
typedef calib_entry[19] calibration;
flaviut commented 2 years ago

@Maelstrom96 You're right--increasing the number there won't work unless we adjust the function signatures too. We already have https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.h#L98, and it's a real shame that that function is written in terms of the other functions rather than having all the other functions call it. However, it seems like a simple-enough refactoring, and the ESP32-arduino project seems responsive to contributions.

@krconv

framework type to esp-idf will cause it to load this driver (instead of any of the Arduino ones), and from what I can tell it doesn't have any internal buffers, which I think is good. But, setting the framework type also causes everything to attempt to compile using the native framework

:(

I guess you could try adding MQTT support to the esp-idf version of the esphome project, that'd probably be something useful for everyone. But I think the fastest & easiest way would be to do PRs to the wire library, since they're pretty simple. I can do that over the next day or two!

Also, I've been thinking about a potential Config format; have either of you tried that out yet? Any thoughts on this idea?

Huh. Neat! I had no idea ESPHome made things that easy, that's really nice!

krconv commented 2 years ago

The calibration data I think will be helpful, the more I think about it. Calibrating 19 CT clamps manually might be a real pain; maybe we could provide a script that helps parse the calibration data later on.

That sounds great; I agree getting MQTT on esp-idf would probably be helpful for everyone, but ya it might slow things down. I also checked the api component, and that relies on Arduino too; so there would be no way to get the sensor data to home assistant without some more development. I did find a native MQTT library; if we do go that route, we might be the first integration that requires esp-idf, which might have some other implications. I'll take a look at the work involved to support MQTT on esp-idf and see if the i2c using the native library even works for this scenario.

flaviut commented 2 years ago

So I did some digging, and here's how things work:

The NVS calibration on the Emporia Vue 2 that I got isn't used. I'm not sure why the cReal and cApparent NVS values exist, but they are set to zero and not used. The last bit of each entry is probably a valid bit, because it is 0 in all of the zeroed entries, and 1 in the original, non-zero entries.

However, that doesn't mean the Vue 2 has no calibration at all. After all, we saw that it does have some sort of calibration constants baked into the firmware from @Maelstrom96's message reverse engineering efforts. What it does have is the following array:

``` FLOAT_ARRAY_ARRAY_3f40e8f0 XREF[3]: 400d0f50(*), calibration:401096c3(*), calibration:401096c9(*) 3f40e8f0 ac 35 b9 float[8] 3c c6 6d b4 3c 58 3f40e8f0 ac 35 b9 3c c6 float[3] [0] XREF[3]: 400d0f50(*), 6d b4 3c 58 39 calibration:401096c3(*), b4 3c calibration:401096c9(*) 3f40e8f0 ac 35 b9 3c float 0.0226086 [0] XREF[3]: 400d0f50(*), calibration:401096c3(*), calibration:401096c9(*) 3f40e8f4 c6 6d b4 3c float 0.022025 [1] 3f40e8f8 58 39 b4 3c float 0.022 [2] 3f40e8fc ac 35 b9 3c 28 float[3] [1] be b5 3c c9 5f b5 3c 3f40e8fc ac 35 b9 3c float 0.0226086 [0] 3f40e900 28 be b5 3c float 0.0221854 [1] 3f40e904 c9 5f b5 3c float 0.0221404 [2] 3f40e908 73 53 b9 3c 58 float[3] [2] 39 b4 3c da c5 b4 3c 3f40e908 73 53 b9 3c float 0.0226228 [0] 3f40e90c 58 39 b4 3c float 0.022 [1] 3f40e910 da c5 b4 3c float 0.022067 [2] 3f40e914 62 98 b8 3c 58 float[3] [3] 39 b4 3c 58 39 b4 3c 3f40e914 62 98 b8 3c float 0.0225336 [0] 3f40e918 58 39 b4 3c float 0.022 [1] 3f40e91c 58 39 b4 3c float 0.022 [2] 3f40e920 60 d9 bb 3c 52 float[3] [4] 48 b2 3c 58 39 b4 3c 3f40e920 60 d9 bb 3c float 0.0229308 [0] 3f40e924 52 48 b2 3c float 0.021763 [1] 3f40e928 58 39 b4 3c float 0.022 [2] 3f40e92c 32 dd b5 3c 27 float[3] [5] 33 b9 3c 58 39 b4 3c 3f40e92c 32 dd b5 3c float 0.0222002 [0] 3f40e930 27 33 b9 3c float 0.0226074 [1] 3f40e934 58 39 b4 3c float 0.022 [2] 3f40e938 25 d0 bb 3c 58 float[3] [6] 39 b4 3c 49 36 b2 3c 3f40e938 25 d0 bb 3c float 0.0229264 [0] 3f40e93c 58 39 b4 3c float 0.022 [1] 3f40e940 49 36 b2 3c float 0.0217544 [2] 3f40e944 10 d6 b5 3c 58 float[3] [7] 39 b4 3c 76 87 b9 3c 3f40e944 10 d6 b5 3c float 0.0221968 [0] 3f40e948 58 39 b4 3c float 0.022 [1] 3f40e94c 76 87 b9 3c float 0.0226476 [2] ```

From trying to read the decompiled code, it seems like the calibration constants it chooses are based on how the phases are aligned on the input wires. I'm having a lot of trouble understanding what the code is doing, but it's around 401040dc in the ELF file, and looks something like this when decompiled:

``` if ((undefined *)(uint)DAT_3ffcc320 == (undefined *)0x0) { pcStack44 = &DAT_3ffcc320; puStack48 = (undefined *)(uint)DAT_3ffcc320; line_00 = (undefined *)lineNo(); pcVar8 = &DAT_3f409e16; log1(3,s_RECONNECT_3f40a774 + 7,&DAT_3f409e16,(int)line_00); *pcStack44 = 1; memw(); if (80.0 < fStack1356) { if (fStack1344 < 40.0) { fVar18 = 1.0 / (float)(ulonglong)_DAT_3ffcddc6; if (ABS(fVar18 - 120.0) < 20.0) { puVar4 = (undefined *)0x4; } else { if (20.0 <= ABS(fVar18 - 240.0)) { puVar4 = puStack48; if (20.0 <= ABS(fVar18 - 180.0)) { uVar3 = 0xc9; goto LAB_401040b3; } } else { puVar4 = (undefined *)0x5; } } } else { if (fStack1344 <= 80.0) goto LAB_401040c8; puVar4 = (undefined *)0x1; } } else { LAB_401040c8: if (40.0 <= fStack1356) { LAB_4010413e: uVar3 = 200; LAB_401040b3: *pcStack44 = uVar3; memw(); goto LAB_40104015; } if (40.0 <= fStack1344) { if (fStack1344 <= 80.0) goto LAB_4010413e; fVar18 = 1.0 / (float)(ulonglong)_DAT_3ffcddc8; if (20.0 <= ABS(fVar18 - 120.0)) { if (20.0 <= ABS(fVar18 - 240.0)) { if (20.0 <= ABS(fVar18 - 180.0)) { uVar3 = 0xca; goto LAB_401040b3; } puVar4 = (undefined *)0x2; } else { puVar4 = (undefined *)0x7; } } else { puVar4 = (undefined *)0x6; } } else { puVar4 = (undefined *)0x3; } } calibration(puVar4,s_RECONNECT_3f40a774 + 7,&DAT_3f409e16,line_00,s_RECONNECT_3f40a774 + 7); } ``` `puVar4` is the index into the array. and like this when disassembled: ``` 40103fa8 a2 a6 54 movi a10,0x654 40103fab 1a aa add.n a10,a10,a1 40103fad 88 0a l32i.n a8,a10,0x0 40103faf 0c 16 movi.n a6,0x1 40103fb1 62 48 00 s8i a6,a8=>DAT_3ffcc320,0x0 40103fb4 c0 20 00 memw 40103fb7 3d f0 nop.n 40103fb9 3d f0 nop.n 40103fbb 3d f0 nop.n 40103fbd 3d f0 nop.n 40103fbf 61 69 31 l32r a6,F_80.0 = 80.0 40103fc2 50 06 fa wfr f0,a6 40103fc5 62 a6 50 movi a6,0x650 40103fc8 1a 66 add.n a6,a6,a1 40103fca 23 01 4d lsi f2,a1,0x134 40103fcd 20 00 4b olt.s b0,f0,f2 40103fd0 a1 66 31 l32r a10,F_40.0 = 40.0 40103fd3 50 1a fa wfr f1,a10 40103fd6 92 26 00 l32i a9,a6,0x0 40103fd9 76 10 02 bt b0,LAB_40103fdf 40103fdc 06 3a 00 j LAB_401040c8 LAB_40103fdf XREF[1]: 40103fd9(j) 40103fdf 33 01 50 lsi f3,a1,0x140 40103fe2 10 03 4b olt.s b0,f3,f1 40103fe5 76 10 02 bt b0,LAB_40103feb 40103fe8 06 34 00 j LAB_401040bc LAB_40103feb XREF[1]: 40103fe5(j) 40103feb 61 60 31 l32r a6,DAT_400d056c = 3FFCDDC4h 40103fee 62 16 01 l16ui a6,a6,offset DAT_3ffcddc6 40103ff1 00 06 da ufloat.s f0,a6,0x0 40103ff4 61 5f 31 l32r a6,F_120.0 = 120.0 40103ff7 50 26 fa wfr f2,a6 40103ffa 20 10 1a sub.s f1,f0,f2 40103ffd a1 5d 31 l32r a10,F_20.0 = 20.0 40104000 10 11 fa abs.s f1,f1 40104003 50 2a fa wfr f2,a10 40104006 20 01 4b olt.s b0,f1,f2 40104009 76 10 02 bt b0,LAB_4010400f 4010400c 06 1c 00 j LAB_40104080 LAB_4010400f XREF[1]: 40104009(j) 4010400f a2 a0 04 movi a10,0x4 LAB_40104012 XREF[7]: 40104094(j), 401040ac(j), 401040c4(j), 401040d9(j), 40104105(j), 4010411c(j), 40104134(j) 40104012 25 6a 05 call8 calibration = "CT" = 1Bh undefined calibration() LAB_40104015 XREF[2]: 40103f80(j), 401040b9(j) 40104015 a5 9b 03 call8 provisioningStatusLessThan48Bytes bool provisioningStatusLessThan4 = "CT" = 1Bh,"[0;32mI (%d) %s: Calculat = 1Bh 40104018 16 2a 04 beqz a10,LAB_4010405e 4010401b e5 53 05 call8 getUxQueue = "CT" void * getUxQueue(void) = 1Bh 4010401e 16 ca 03 beqz a10,LAB_4010405e 40104021 65 1e 05 call8 uxQueueHasSpace bool uxQueueHasSpace(void) = "CT" = 1Bh 40104024 fc 6a bnez.n a10,LAB_4010405e 40104026 81 ed 30 l32r a8,->lineNo = 40086ec4 40104029 e0 08 00 callx8 a8=>lineNo = "CT" = 1Bh int lineNo(void) 4010402c c1 55 31 l32r a12,PTR_DAT_400d0580 = 3f409e43 4010402f dd 0a mov.n a13,a10 40104031 ed 03 mov.n a14,a3 40104033 bd 03 mov.n a11,a3 40104035 0c 3a movi.n a10,0x3 40104037 81 17 31 l32r a8,->log1 = 40086d88 4010403a e0 08 00 callx8 a8=>log1 = "CT" int log1(int level, char * funcn = 1Bh 4010403d ad 02 mov.n a10,a2 4010403f a5 cd 00 call8 V2Queue = "CT" void * V2Queue(int param_1) = 1Bh 40104042 62 05 05 l8ui a6,a5,offset DAT_3ffcc5fd 40104045 dc 56 bnez.n a6,LAB_4010405e 40104047 c2 a2 10 movi a12,0x210 4010404a 20 b2 20 mov a11,a2 4010404d a1 4d 31 l32r a10,DAT_400d0584 = 3FFCC350h 40104050 0c 16 movi.n a6,0x1 40104052 81 e5 30 l32r a8,->memcpy = 40091ab0 40104055 e0 08 00 callx8 a8=>memcpy = "CT" void * memcpy(void * dest, void 40104058 62 45 05 s8i a6,a5,offset DAT_3ffcc5fd 4010405b c0 20 00 memw LAB_4010405e XREF[4]: 40104018(j), 4010401e(j), 40104024(j), 40104045(j) 4010405e 20 a2 20 mov a10,a2 40104061 52 c7 3c addi a5,a7,0x3c 40104064 e5 83 00 call8 resetCalibrationData undefined4 * resetCalibrationDat = "CT" = 1Bh 40104067 57 34 05 bltu a4,a5,LAB_40104070 4010406a e5 c9 00 call8 loadAllCalibrations undefined4 loadAllCalibrations(u = "CT" 4010406d 40 74 20 mov a7,a4 LAB_40104070 XREF[1]: 40104067(j) 40104070 b2 a0 00 movi a11,0x0 40104073 a2 a0 17 movi a10,0x17 40104076 a5 71 04 call8 FUN_40108790 = "CT" undefined FUN_40108790() 40104079 40 64 20 mov a6,a4 4010407c 86 35 ff j LAB_40103d56 4010407f 00 ?? 00h LAB_40104080 XREF[1]: 4010400c(j) 40104080 61 3e 31 l32r a6,F_240.0 = 240.0 40104083 50 36 fa wfr f3,a6 40104086 30 10 1a sub.s f1,f0,f3 40104089 10 11 fa abs.s f1,f1 4010408c 20 01 4b olt.s b0,f1,f2 4010408f 76 00 05 bf b0,LAB_40104098 40104092 0c 5a movi.n a10,0x5 40104094 86 de ff j LAB_40104012 40104097 00 ?? 00h LAB_40104098 XREF[1]: 4010408f(j) 40104098 61 39 31 l32r a6,F_180.0 = 180.0 4010409b 50 16 fa wfr f1,a6 4010409e 10 00 1a sub.s f0,f0,f1 401040a1 10 00 fa abs.s f0,f0 401040a4 20 00 4b olt.s b0,f0,f2 401040a7 76 00 05 bf b0,LAB_401040b0 401040aa ad 09 mov.n a10,a9 401040ac 86 d8 ff j LAB_40104012 401040af 00 ?? 00h LAB_401040b0 XREF[1]: 401040a7(j) 401040b0 62 af c9 movi a6,-0x37 LAB_401040b3 XREF[2]: 4010413b(j), 40104141(j) 401040b3 62 48 00 s8i a6,a8=>DAT_3ffcc320,0x0 401040b6 c0 20 00 memw 401040b9 06 d6 ff j LAB_40104015 LAB_401040bc XREF[1]: 40103fe8(j) 401040bc 30 00 4b olt.s b0,f0,f3 401040bf 76 00 05 bf b0,LAB_401040c8 401040c2 0c 1a movi.n a10,0x1 401040c4 86 d2 ff j LAB_40104012 401040c7 00 ?? 00h LAB_401040c8 XREF[2]: 40103fdc(j), 401040bf(j) 401040c8 10 02 4b olt.s b0,f2,f1 401040cb 76 00 6f bf b0,LAB_4010413e 401040ce 23 01 50 lsi f2,a1,0x140 401040d1 10 02 4b olt.s b0,f2,f1 401040d4 76 00 04 bf b0,LAB_401040dc 401040d7 0c 3a movi.n a10,0x3 401040d9 46 cd ff j LAB_40104012 LAB_401040dc XREF[1]: 401040d4(j) 401040dc 20 00 4b olt.s b0,f0,f2 401040df 76 00 5b bf b0,LAB_4010413e 401040e2 61 22 31 l32r a6,DAT_400d056c = 3FFCDDC4h 401040e5 62 16 02 l16ui a6,a6,offset DAT_3ffcddc8 401040e8 00 06 da ufloat.s f0,a6,0x0 401040eb 61 21 31 l32r a6,F_120.0 = 120.0 401040ee 50 26 fa wfr f2,a6 401040f1 20 10 1a sub.s f1,f0,f2 401040f4 91 20 31 l32r a9,F_20.0 = 20.0 401040f7 10 11 fa abs.s f1,f1 401040fa 50 29 fa wfr f2,a9 401040fd 20 01 4b olt.s b0,f1,f2 40104100 76 00 04 bf b0,LAB_40104108 40104103 0c 6a movi.n a10,0x6 40104105 46 c2 ff j LAB_40104012 LAB_40104108 XREF[1]: 40104100(j) 40104108 61 1c 31 l32r a6,F_240.0 = 240.0 4010410b 50 36 fa wfr f3,a6 4010410e 30 10 1a sub.s f1,f0,f3 40104111 10 11 fa abs.s f1,f1 40104114 20 01 4b olt.s b0,f1,f2 40104117 76 00 05 bf b0,LAB_40104120 4010411a 0c 7a movi.n a10,0x7 4010411c 86 bc ff j LAB_40104012 4010411f 00 ?? 00h LAB_40104120 XREF[1]: 40104117(j) 40104120 61 17 31 l32r a6,F_180.0 = 180.0 40104123 50 16 fa wfr f1,a6 40104126 10 00 1a sub.s f0,f0,f1 40104129 10 00 fa abs.s f0,f0 4010412c 20 00 4b olt.s b0,f0,f2 4010412f 76 00 05 bf b0,LAB_40104138 40104132 0c 2a movi.n a10,0x2 40104134 86 b6 ff j LAB_40104012 40104137 00 ?? 00h LAB_40104138 XREF[1]: 4010412f(j) 40104138 62 af ca movi a6,-0x36 4010413b 06 dd ff j LAB_401040b3 LAB_4010413e XREF[2]: 401040cb(j), 401040df(j) 4010413e 62 af c8 movi a6,-0x38 40104141 86 db ff j LAB_401040b3 ```
Maelstrom96 commented 2 years ago

so there would be no way to get the sensor data to home assistant without some more development.

The native ESPHome API will be able to export the data to Home Assistant without the need for MQTT. Once the updates to the arduino libs are done, everything should work on the Arduino and esp-idf using their I2CDevice class. Right now, you should be able to test it without MQTT fine.

Also, there's still 2 things that I'm not sure what to do about them :

Also, I might try it out your fork @krconv on my bench today if I get the time.

flaviut commented 2 years ago

TLDR because my message above was basically a dump of my thoughts:

I wonder if the calibration is really changing that much per CT

Calibration is not different per CT. Calibration is different depending on the phase order on the voltage sensing side in some way I do not understand.

Getting the ~0.022 value per phase seems to be easy enough since it's being printed to Serial, and could be captured before flashing ESPHome on the ESP32 quite easily.

Yup, this is probably the easiest way to do things. And there's only 6 combinations of wires, so the wire->calibration constants could be tested manually pretty easily.

flaviut commented 2 years ago

The thing is that I doubt that the "current" number is actually instantaneous current

For some reason, I thought it was spot-on. It isn't quite, looking at a recent utility bill:

But that still is very close, especially with the only calibration here being a global, per-voltage calibration that's pre-set from the factory and not device-specific.


edit: both misread my bill, and excluded a day in my report

Maelstrom96 commented 2 years ago
  • I'm being billed for 873kWh
  • Integrating & summing the two phases comes out to 752.2kWh
  • Integrating & summing the various circuits comes out to 752.5kWh

Yeah, that would be the kind of variation I would expect if it wasn't being counted correctly - Definitely something to investigate, since many people on the Emporia forum seems to say that the totals are really close to their utility bills (which makes sense since the 200A CT should rarely be below ~1A, their minimum for accurate readings)

Maelstrom96 commented 2 years ago

Also, something interesting I noticed is that in the Pulseview dump from @flaviut, the intervals between data requests on the i2c bus aren't every second like I expected. They seem to be every ~240ms.

Something else that is intriguing is that the debug messages always seems to start by 0x03, and @krconv documented it as the message "version", but I'm seeing it with a value of 0x03 and 0x00 sometimes.

The first two bytes and the two bytes after seems to be some kind of counters, and, in the dump, the first one seem to alternate every message, while the second value is updated every 2 messages. E.g. msg1-0x03A2, msg2-0x00A2, msg3-0x0306, msg4-0x0006, msg5-0x0391, msg6-0x0091.

So I think It might indicate something else and be related to the "current" value, so we can sum it correctly.

Maelstrom96 commented 2 years ago
  • I'm being billed for ~873kWh~ 790kWh
  • Integrating & summing the two phases comes out to ~752.2kWh~ 782.1kWh
  • Integrating & summing the various circuits comes out to ~752.5kWh~ 782.2kWh

Do you have the current formula / procedure that you use to get from the current value to those power consumptions?

flaviut commented 2 years ago

I completely ignore the current. I used this code to ingest the data into InfluxDB: https://github.com/flaviut/emporia-vue2-reversing/blob/master/parse_mqtt_dbg.py#L196-L242

And this query to process it:

import "math"

from(bucket: "home_automation")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "home_power")
  |> filter(fn: (r) => r["_field"] == "power")
  |> filter(fn: (r) => r["phase"] != "")
  |> group(columns: ["_start", "_stop", "phase"])
  |> integral(unit:1h)
  |> group()
  |> keep(columns: ["_value", "phase"])

I don't really understand why the integral() function takes a unit

flaviut commented 2 years ago

Something else that is intriguing is that the debug messages always seems to start by 0x03, and @krconv documented it as the message "version", but I'm seeing it with a value of 0x03 and 0x00 sometimes.

Looking at the code, it seems like the existing code mostly ignores messages that do not have it set to 0x03:

      if ((iVar1 == 0) && (pcVar6 = pcVar2 + 0x400, pcVar2[0x410] == '\x03')) {
        pcVar7 = pcVar2 + 0x410;
        pcVar8 = (char *)0x11c;
        pvVar16 = memcpy(&uStack1664,pcVar7,0x11c);
        FUN_40104f54(pvVar16,pcVar7,0x11c,line_00,pcVar9);
        FUN_40109564(auStack104,pcVar7,0x11c,line_00,pcVar9);
        pcVar7 = pcVar2;
        FUN_4010432c(auStack332,pcVar2,0x11c,line_00,pcVar9);
        pcVar6 = pcVar2;
      }
      uVar5 = FUN_40092448(0,pcVar6,pcVar8,line_00,pcVar9);

Of course, it's extremely likely I'm misunderstanding what is going on here.

Maelstrom96 commented 2 years ago

Looking at the code, it seems like the existing code mostly ignores messages that do not have it set to 0x03:

I wouldn't be surprise, it would make sense that this would indicate to the ESP that the data is "ready". An 0x00 would indicate that the data is being updated, and then that would explain why the third and fourth bytes have a new value when we get a new 0x03.

Maelstrom96 commented 2 years ago

I completely ignore the current. I used this code to ingest the data into InfluxDB

Looking at the code, you still seem to send the current into the influxDB. Would it be possible for you to dump the full timeseries for the period that match your known power usage (from your utility provider), so we could try and calculate the current formula that might give an even more precise power consumption value.

flaviut commented 2 years ago

Here's the full data for the past 30 days: https://drive.google.com/file/d/14VMxbSIGjlCfecbEYWAQfevvbe-ccUAM/view?usp=sharing

Note: it extracts to 5.4GB of data.

Day kWh
Oct 27 17
Oct 28 19
Oct 29 15
Oct 30 10
Oct 31 21
Nov 01 22
Nov 02 22
Nov 03 20
Nov 04 19
Nov 05 22
Nov 06 21
Nov 07 28
Nov 08 19
Nov 09 21
Nov 10 26
Nov 11 22
Nov 12 21
Nov 13 27
Nov 14 18
Nov 15 23
Nov 16 31
Nov 17 18
Nov 18 18
Nov 19 24
Nov 20 19
Nov 21 21
Nov 22 22
Nov 23 23
Nov 24 10
Nov 25 11
Nov 26 23
Nov 27 29
krconv commented 2 years ago

A couple observations I've had:

The thing is that I doubt that the "current" number is actually instantaneous current

What if it is actually "the current passed since last reading"? i.e. the current that's passed since the last message with a 0x3 "version" header.

Theory: The ESP polls the sensor every ~240ms. Meanwhile, the sensor is on some clock where it'll calculate "instantaneous" (or ~2 second window) current draw, voltage, and calculate power consumption. Maybe the "version" acts as a "is new reading" flag, where reading it causes it to be cleared. To calculate total power draw accurately, every single sensor reading would need to be aggregated together, because missing one measurement would mean missing the power consumption in that ~2s window.

I'm still having a hard time understanding how AC power is calculated (i.e. I understand DC P=V*I, but the integrals for AC I might not understand right). But, if the "power" sensor readings really are power readings, and have a unit of W, then wouldn't we only need to sum those up over a time period to get Wh/KWh? That is where I think the total_daily_reading component might come in, because it a) calculates the time since last reading and b) multiplies the time delta by the instantaneous power reading and c) adds it to a total counter (which I think can be consumed by home assistant's energy dashboard)

krconv commented 2 years ago

I flashed some code to my Vue, and am successfully reading from I2C on the esp-idf framework. I also tested using Arduino (just to see what would happen without the bigger buffer), and the I2C call returns a 28 error code; couldn't find what that means though.

Here's the output being spit out from ESPHome

[VV][scheduler:152]: Running interval 'update' with interval=1000 last_execution=4294966081 (now=580)
[VV][i2c.idf:137]: 0x64 RX 030452A6C8FDFFFFDDFDFFFFE7FDFFFFC7B9FAFFC7B6FDFF18BEFDFFEA050000E5020000ED020000120500009EFFFFFFABFFFFFF69070000530100006D0100003FF8FFFFF0F9FFFFFFF9FFFFF861FBFFC63AFEFFC64BFEFFDF0FFBFFF6D4FDFFE7AFFDFF6FF70A00B32708002DEF07006B400B00EB330800EF1A08004C930A000AF507000DD20700CE58FBFF7128FEFF121CFEFF6781FAFFC2A8FDFF68B6FDFF3E45FBFF2A2AFEFF8763FEFFF861FBFFC63AFEFFC64BFEFFDF0FFBFFF6D4FDFFE7AFFDFF98E4F9FFAD17FDFFBB32FDFFAD0EFAFFEB42FDFFEF1AFDFFAA49FAFF4951FDFFEF35FDFF923D37332F33A
[VV][emporia_vue:031]: Raw Sensor Data: 03.04.52.A6.C8.FD.FF.FF.DD.FD.FF.FF.E7.FD.FF.FF.C7.B9.FA.FF.C7.B6.FD.FF.18.BE.FD.FF.EA.05.00.00.E5.02.00.00.ED.02.00.00.12.05.00.00.9E.FF.FF.FF.AB.FF.FF.FF.69.07.00.00.53.01.00.00.6D.01.00.00.3F.F8.FF.FF.F0.F9.FF.FF.FF.F9.FF.FF.F8.61.FB.FF.C6.3A.FE.FF.C6.4B.FE.FF.DF.0F.FB.FF.F6.D4.FD.FF.E7.AF.FD.FF.6F.F7.0A.00.B3.27.08.00.2D.EF.07.00.6B.40.0B.00.EB.33.08.00.EF.1A.08.00.4C.93.0A.00.0A.F5.07.00.0D.D2.07.00.CE.58.FB.FF.71.28.FE.FF.12.1C.FE.FF.67.81.FA.FF.C2.A8.FD
[V][sensor:062]: 'A': Received new state -0.592032
[D][sensor:113]: 'A': Sending state -0.59203 W with 0 decimals of accuracy
[V][sensor:062]: 'C': Received new state 1.578056
[D][sensor:113]: 'C': Sending state 1.57806 W with 0 decimals of accuracy
[V][sensor:062]: '1': Received new state 1.352917
[D][sensor:113]: '1': Sending state 1.35292 W with 0 decimals of accuracy
[V][sensor:062]: '2': Received new state 1.977260
[D][sensor:113]: '2': Sending state 1.97726 W with 0 decimals of accuracy
[V][sensor:062]: '3': Received new state -2.068984
[D][sensor:113]: '3': Sending state -2.06898 W with 0 decimals of accuracy
[V][component:186]: Component emporia_vue.sensor took a long time for an operation (0.18 s).
[V][component:187]: Components should block for at most 20-30ms.```

It seems like it's parsing it correctly according to the struct definition, and can successfully get the data from I2C. I compared the power values for a known load with ESPHome installed vs flashing the original software on there, and the values seemed close. I noticed with ESPHome, the values fluctuate between -2 W and 2 W though, where it is exactly zero with the original software; I wonder if the original software filters out readings below 2 watts or something like that.

This is the config I'm testing with:

Config
substitutions:
  device_name: home-power-monitor
  friendly_name: Home Power Monitor
  assigned_ip_address: "192.168.86.223"

esphome:
  name: ${device_name}

esp32:
  board: esp-wrover-kit
  framework:
    type: esp-idf

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  use_address: ${assigned_ip_address}

ota:

logger:
  level: VERY_VERBOSE

i2c:

sensor:
  - platform: emporia_vue
    update_interval: 1s
    phases:
      - id: phase_a
        input: BLACK
    power:
      - name: "A"
        phase_id: phase_a
        input: "A"
      - name: "C"
        phase_id: phase_a
        input: "C"
      - name: "1"
        phase_id: phase_a
        input: "1"
      - name: "2"
        phase_id: phase_a
        input: "2"
      - name: "3"
        phase_id: phase_a
        input: "3"

external_components:
  - source: github://krconv/esphome@add-emporia-vue
    components: [emporia_vue]
    refresh: 1min
Maelstrom96 commented 2 years ago

Wow, good job on the code!

Here are some things I found while looking at the code and log output :

Edit: Here's a good example on how we could start a subtask on a separate thread that would handle the i2c reads and power value calculations in a non loop-blocking way : https://github.com/esphome/esphome/blob/939fb313df622d3fabc14ea837e296c7aa5ec0ec/esphome/components/esp32_camera/esp32_camera.cpp#L35

Maelstrom96 commented 2 years ago

I also tested using Arduino (just to see what would happen without the bigger buffer), and the I2C call returns a 28 error code

@krconv I'm 99% sure that it's this here : https://github.com/esphome/esphome/blob/939fb313df622d3fabc14ea837e296c7aa5ec0ec/esphome/components/i2c/i2c_bus_arduino.cpp#L75

It's just telling you that the amount of byte read is not equal to the requested len, which would be accurate since we're overflowing the uint8_t.

Maelstrom96 commented 2 years ago

@krconv Would it be possible for you to give us write access to your esphome fork so we can all work on the same repo?

flaviut commented 2 years ago

This is very exciting! Nice job @krconv, I've also read all your code & it looks very clean!

With regards to the calibration, I thought you were planning to use ESPHome's built-in linear calibration? Is this change a user-friendliness change, or does using the built-in thing break something else?

krconv commented 2 years ago

Thanks for the review on the code; really helpful, I'm still trying to understand how to work with ESPHome. Just gave you both access, feel free to push directly or PR as you desire. The calibration code I had in there was mainly for local testing to see if I could get anything consistent with real values (I was uploading to GitHub and downloading it onto my Home Assistant machine to test); I added a few more TODOs to mark things that aren't done yet too.

I tried out 400kHz, which seems to work sometimes, but also sometimes times out ([20:59:43][VV][i2c.idf:119]: RX from 64 failed: timeout). Tried 200kHz, which seems to be reliable, and tried out 800kHz to see what would happen, and that causes I2C timeouts on every read.

For calibration; I'm still wrapping my head around it, and I don't feel strongly either way. I think using some "fake" calibration data to at least get the numbers to seem more understandable might be good for user friendly-ness, even if they aren't accurate, and could include instructions for how to calibrate further. If getting the phase calibrations from logs works as we expect, I think we should also give an option to plug those in directly as well to help avoid manual calibration. I still don't trust the readings, and I kinda think we're gonna run into something that encourages us to use the other sensor data that we aren't currently reading (maybe the counter in the last byte of the header or the current data); I feel like there's so much going on in the logs, and other pins transmitting data on the board that reading the sensor data directly feels like we're cheating somehow.

I like @Maelstrom96 idea to control the how often we poll from the MCU ourselves, and let how often we actually publish the value be controlled by config. Tonight I tested calibrating one of my circuits to read accurate values (using calibrate_linear), and then adjusting how often we read from the sensor to see how it affects the readings. It seems like when we read from the sensor, we only get the last ~1s of data, so if we wait too long then we miss data. For example, I set the update interval to 5s, and turned the stove on for 4 seconds, and turned it off right before the reading, and the sensor just returned a reading of 0W, instead of an average over the 5 seconds. So it seems like we'll need to implement that averaging mechanism.

It is really cool to see how fast the values come into Home Assistant. It's realtime, instead of the ~5 second delay with the Emporia app. I get a kick out of turning the stove on and off just to watch the numbers bounce back and forth πŸ˜†

Maelstrom96 commented 2 years ago

It seems like when we read from the sensor, we only get the last ~1s of data, so if we wait too long then we miss data.

So my latest theory is that the co-MCU is averaging the last ~480ms of power consumption into the power readings values. So in order to get accurate total energy counts, we might need to compile the values using all the message data as soon as it's updated.

Maelstrom96 commented 2 years ago

I don't have the time to test it right now, but here are some changes I made to the codebase if you want to test them @krconv : https://github.com/Maelstrom96/esphome/tree/add-emporia-vue2

Edit: Just thought about it and you might not be able to test it using the "External_Component" configuration since I made changes to the esphome/const.py file.

Edit2: Changed the code so it could be correctly imported for testing. Also, you will have to rename the "power" configuration to "ct".

krconv commented 2 years ago

I can take a look tonight! I've been playing around with putting the read task in the loop(), interested to see how the scheduled task works. BTW, I noticed that averaging a few reading together makes the readings around 0W more stable which is encouraging. Got my the two 400A clamps on the main power cables, and running that for a day to see if our calculations are close to my power company's readings 🀞

Maelstrom96 commented 2 years ago

@krconv Not sure if it's just something with my setup, but when using the esp-idf framework instead of the default Arduino one, the wifi just doesn't want to connect, and the serial/UART output is all over the place. I've tried installing the code without the emporia_vue module and by cleaning the build files to make sure that wasn't playing a role, and it's still happening with a pretty bare configuration file. Are you also seeing this? I'm using the latest ESPHome 2021.11.04.

                                                                                [V][wifi_esp32:434]: tcpip_adapter_dhcpc_stop failed: ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED
                                                                                                                                                                            [E][wifi:291]: wifi_sta_connect_ failed!
                       [D][wifi:370]: Starting scan...
                                                      [V][wifi_esp32:647]: Event: WiFi Scan Done status=0 number=18 scan_id=177
                                                                                                                               [D][wifi:385]: Found networks:
                                                                                                                                                             [I][wifi:429]: - 'AJ - IOT' (FA:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                    [D][wifi:430]:     Channel: 5
                                                 [D][wifi:431]:     RSSI: -38 dB
                                                                                [I][wifi:429]: - 'AJ - IOT' (FA:92:BF:5D:F4:37) β–‚β–„β–†β–ˆ
                                                                                                                                    [D][wifi:430]:     Channel: 5
                                                                                                                                                                 [D][wifi:431]:     RSSI: -52 dB
   [D][wifi:434]: - 'AJ - 2.4Ghz' (FE:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                          [D][wifi:434]: - 'AJ' (F4:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                                                        [D][wifi:434]: - '' (02:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                                                                                                    [D][wifi:434]: - 'AJ' (F4:92:BF:5D:F4:37) β–‚β–„β–†β–ˆ
     [D][wifi:434]: - 'AJ - 2.4Ghz' (FE:92:BF:5D:F4:37) β–‚β–„β–†β–ˆ
                                                            [D][wifi:434]: - '' (02:92:BF:5D:F4:37) β–‚β–„β–†β–ˆ
                                                                                                        [D][wifi:434]: - 'JLWIFI' (90:AA:C3:37:0F:58) β–‚β–„β–†β–ˆ
                                                                                                                                                          [D][wifi:434]: - 'yeelink-light-color2_miapC64C' (04:CF:8C:7C:C6:4C) β–‚β–„β–†β–ˆ
                                      [D][wifi:434]: - 'BELL641' (48:29:52:FA:48:7E) β–‚β–„β–†β–ˆ
                                                                                         [D][wifi:434]: - 'yeelink-light-color2_miapCAC7' (04:CF:8C:7C:CA:C7) β–‚β–„β–†β–ˆ
                                                                                                                                                                  [D][wifi:434]: - 'oxio-7410' (E8:2C:6D:50:74:14) β–‚β–„β–†β–ˆ
                          [D][wifi:434]: - 'WiFiGilles' (10:33:BF:E6:25:7E) β–‚β–„β–†β–ˆ
                                                                                [D][wifi:434]: - '' (10:33:BF:E6:25:81) β–‚β–„β–†β–ˆ
                                                                                                                            [D][wifi:434]: - 'oxio-7410' (3C:90:66:F8:E8:64) β–‚β–„β–†β–ˆ
                                                                                                                                                                                 [D][wifi:434]: - 'HP-Print-36-ENVY 4500 series' (64:51:06:EC:E1:36) β–‚β–„β–†β–ˆ
                                                            [D][wifi:434]: - 'BELL872' (C0:3C:04:1B:FA:86) β–‚β–„β–†β–ˆ
                                                                                                               [I][wifi:245]: WiFi Connecting to 'AJ - IOT'...
                                                                                                                                                              [V][wifi:247]: Connection Params:
  [V][wifi:248]:   SSID: 'AJ - IOT'
                                   [V][wifi:251]:   BSSID: FA:92:BF:5D:EF:A6
                                                                            [V][wifi:271]:   Password: NOPE
                                                                                                                               [V][wifi:276]:   Channel: 5
                                                                                                                                                          [V][wifi:283]:   Manual IP: Static IP=172.16.10.4 Gateway=172.16.10.1 Subnet=255.255.255.0 DNS1=0.0.0.0 DNS2=0.0.0.0
                                                                                 [V][wifi:287]:   Hidden: NO
                                                                                                            [V][wifi_esp32:434]: tcpip_adapter_dhcpc_stop failed: ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED
           [E][wifi:291]: wifi_sta_connect_ failed!
                                                   [D][wifi:370]: Starting scan...
                                                                                  [V][wifi_esp32:647]: Event: WiFi Scan Done status=0 number=18 scan_id=178
                                                                                                                                                           [D][wifi:385]: Found networks:
                                                                                                                                                                                         [I][wifi:429]: - 'AJ - IOT' (FA:92:BF:5D:F4:37) β–‚β–„β–†β–ˆ
                                                [D][wifi:430]:     Channel: 5
                                                                             [D][wifi:431]:     RSSI: -53 dB
                                                                                                            [I][wifi:429]: - 'AJ - IOT' (FA:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                                                                                                                [D][wifi:430]:     Channel: 5
                                                                                                                                                                                            [D][wifi:431]:     RSSI: -38 dB
                              [D][wifi:434]: - 'AJ' (F4:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                            [D][wifi:434]: - 'AJ - 2.4Ghz' (FE:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                                                                                   [D][wifi:434]: - '' (02:92:BF:5D:EF:A6) β–‚β–„β–†β–ˆ
                                                                                                                                                                               [D][wifi:434]: - ''

Flashing the same config with Arduino seems to work fine, but wouldn't work OK with our i2c requirements.

krconv commented 2 years ago

I did hit an issue with connecting to the board remotely once (I've been uploading OTA), but I'm not sure if it was WiFi or not. Whatever it was, cutting power and restarting fixed it for me. Here is the full minimal config/console that I tried:

Config
esphome:
  name: home-power-monitor

esp32:
  board: esp-wrover-kit
  framework:
    type: esp-idf

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  use_address: "192.168.86.223"

ota:

logger:
  level: DEBUG

api:
Output
[19:52:00][I][app:099]: ESPHome version 2021.11.4 compiled on Dec  1 2021, 19:50:35
[19:52:00][C][wifi:488]: WiFi:
[19:52:00][C][wifi:350]:   Local MAC: 40:F5:20:6A:26:68
[19:52:00][C][wifi:351]:   SSID: [redacted]
[19:52:00][C][wifi:352]:   IP Address: 192.168.86.223
[19:52:00][C][wifi:354]:   BSSID: [redacted]
[19:52:00][C][wifi:355]:   Hostname: 'home-power-monitor'
[19:52:00][C][wifi:357]:   Signal strength: -30 dB β–‚β–„β–†β–ˆ
[19:52:00][C][wifi:361]:   Channel: 11
[19:52:00][C][wifi:362]:   Subnet: 255.255.255.0
[19:52:00][C][wifi:363]:   Gateway: 192.168.86.1
[19:52:00][C][wifi:364]:   DNS1: 192.168.86.1
[19:52:00][C][wifi:365]:   DNS2: 0.0.0.0
[19:52:00][C][logger:233]: Logger:
[19:52:00][C][logger:234]:   Level: DEBUG
[19:52:00][C][logger:235]:   Log Baud Rate: 115200
[19:52:00][C][logger:236]:   Hardware UART: UART0
[19:52:00][C][ota:082]: Over-The-Air Updates:
[19:52:00][C][ota:083]:   Address: 192.168.86.223:3232
[19:52:00][C][api:134]: API Server:
[19:52:00][C][api:135]:   Address: 192.168.86.223:6053
[19:52:00][C][api:139]:   Using noise encryption: NO
[19:52:00][C][mdns:084]: mDNS:
[19:52:00][C][mdns:085]:   Hostname: home-power-monitor

Good call on deleting the build dir; I've noticed that some of the flags that change for the esp-idf library don't properly recompile all of the libraries.

I tested out that branch on my Vue, and the repeating task seemed to work first try. My notes

I updated my branch here too; I copied some of your code (I'll figure out how to get the commits to show credit correctly later, committed them all as me to avoid the rebase/cherry-pick for now). Two notable difference

I'm still waiting for the latest power readings from my power company; if they seem accurate, how would you guys feel about me opening a PR to the main repo? It seems pretty common for PRs to be open for a while with ESPHome and to make changes/give feedback, and I think it'll be good for visibility and to make it easier to for us and others review code with inline comments

Maelstrom96 commented 2 years ago

So regarding my issue with the wifi component, it was a problem when using the manual_ip setting. I've submitted a PR to fix it and it has been merged.

The main mentality with esphome components seems to be to leave the data as raw as possible and change the behavior of the sensors using filters. For you data rate issue, you should be able to just setup a throttle filter. Also, a reason that we would want all those values is so that our total_daily_energy sensors are as accurate as possible. Even if you apply filters, the energy component will get the ~500ms state update and it will be able to calculate the most precise power consumption possible.

For the task I made, it should not be 100% blocking since other tasks should run when we're in vTaskDelay. Still need to investigate why ota wasn't working for you. I will be testing it locally.

Maelstrom96 commented 2 years ago

A big problem I see with the way you do the loop is that the blocking/execution time will be way to high. There's also potential for even higher block time with this->read in cases where the read times out for whatever reason. I haven't found a way to change the default timeout of 1s yet.

krconv commented 2 years ago

Cool!

For the polling component vs publish on every reading; I don't think we are loosing any accuracy in our data by not publishing every value, they are still all accounted for in the average. People would need to add a filter to each of their CT sensors to reduce the frequency if they want the power data in Home Assistant (i.e. not just energy usage but current power load as well), or otherwise risk publishing too many updates. I think setting a single update_interval on the component is more intuitive, even if it isn't what is going on under the hoods; anyone could still set update interval to ~510ms if they want to speed up the readings.

I notice the priority on the RTOS task is set to 0; maybe reducing it would help (just a guess)? FWIW, the read loop is taking 23ms to read from I2c, which I think is ok given that this is working with specialized hardware and not intended for general purpose use. Aren't we taking the same amount of time with RTOS as well, or do they run on different cores? I'd argue giving ESP control over the loop would provide easier debugging, even if we occasionally get warnings that the loop took too long.

Got the most recent readings from my power company, and the values look pretty close to accurate as is! Mine were about 2% below the power company's readings, which I hope is only due to me not calibrating the phases exactly (using the default of 0.022 because I didn't get the calibration data before flashing). The missing sections are times of the day I was testing the Vue and readings weren't accurate image

Maelstrom96 commented 2 years ago

Regarding the task, the whole point was to offload the i2c read to the second CPU core, so it is running on the other core (cpu1). Also, I've just checked to validate the information, and the lower the priority number, the lower the priority is, so 0 should be the lowest one, which is the same priority as the idle task (see https://www.freertos.org/RTOS-task-priority.html)