Initial Commit

This commit is contained in:
Joe Rouvier
2022-01-10 22:25:20 -08:00
committed by GitHub
parent 75dfc46883
commit 0919368685
6 changed files with 543 additions and 1 deletions

View File

@@ -1 +1,32 @@
# esphome-emporia-vue # ESPHome Emporia Vue Utility Connect Unofficial Firmware
This is an unauthorized and unoffical firmware for the Emporia View Utility Connect device that reports energy usage to Home Assistant and completely divorces the device from Emporia's servers.
## Disclaimer
This software is of generally poor quality and should not be used by anyone. When you install the software on your device, it
will no longer report data to Emporia. You should backup the original Emporia firmware before installing this.
## Installation
Connect a your USB to serial adapter to the port marked "P3" as follows:
| Pin | Description | USB-Serial port |
| --- | --------- | --------------- |
| 1 | IO0 | RTS |
| 2 | EN | DTR |
| 3 | GND | GND |
| 4 | TX | RX |
| 5 | RX | TX |
| 6 | +5v | +5v |
Note that pin 6 (the pin just above the text "EmporiaEnergy") is 5 volts, not 3.3v. Use caution not to apply 5V to the wrong pin or
the magic smoke may come out. You may want to not connect pin 6 at all and instead plug the device into a usb port to provide power,
a portable USB battery pack works well for this.
Instead of connecting IO0 and EN, you can simply short IO0 to ground while connecting power to get the device into bootloader mode.
Download [emporia_vue_utility.h](src/emporia_vue_utility.h) and either one of [vue-utility-full.yaml](src/vue-utility-full.yaml) or
[vue-utility-minimal.yaml](src/vue-utility-minimal.yaml).
Execute `esphome run vue-utility-*.yaml` to build and install

56
docs/pinout.md Normal file
View File

@@ -0,0 +1,56 @@
## Pinouts
![Emporia Vue Utility device](device_800.jpg)
### P1
JTAG header for the ESP32 module. The pinout is the same as as the [ESP-Prog](https://docs.espressif.com/projects/espressif-esp-iot-solution/en/latest/hw-reference/ESP-Prog_guide.html)
| Pin | ESP Pin | ESP Port | Description |
| --- | ------- | -------- | ----------- |
| 1 | | | +3v3 |
| 2 | 13 | GPIO14 | JTAG TMS |
| 3 | | | GND |
| 4 | 16 | GPIO13 | JTAG TCK |
| 5 | | | GND |
| 6 | 23 | GPIO15 | JTAG TDO |
| 7 | | | GND |
| 8 | 14 | GPIO12 | JTAG TDI |
| 9 | | | GND |
| 10 | 3 | | EN |
### P2
Power supply
| Pin | Description |
| --- | ----------- |
| 1 | +5v |
| 2 | +3v3 |
| 3 | GND |
### P3
ESP32 Serial programming interface. Note that pin 6 is 5 volt, not 3.3 volt.
| Pin | ESP Pin | ESP Port |
| --- | ------- | -------- |
| 1 | 25 | GPIO0 |
| 2 | 3 | EN |
| 3 | | GND |
| 4 | 35 | TX |
| 5 | 35 | RX |
| 6 | | +5v |
### P4
JTAG interface to the MGM111 module
### P5
| Pin | ESP Pin | MGM111 Pin | Description |
| --- | ------- | ---------- | ----------- |
| 1 | GPIO21 | | ESP RX |
| 2 | GPIO22 | | ESP TX |
| 3 | | | GND |

55
docs/protocol.md Normal file
View File

@@ -0,0 +1,55 @@
# Protocol details
Each message sent or received begins with a `$` (hex 24). Messages end with a carrage return (`\r` / hex 0D).
### Message types
| Msg char | Hex value | Description |
| -------- | --------- | ----------- |
| r | 0x72 | Get meter reading |
| j | 0x6a | Join the meter |
| m | 0x6d | Get mac address (of the MGM111)|
| i | 0x69 | Get install code |
| f | 0x66 | Get firmware version |
## Sending messages
Messages from the ESP to the MGM111 have just the previously mentioned `$` starting delimiter, then a message type character and the
ending `\r` delimiter. Each message is therefore exactly 3 bytes long. For example, to get the a mac address, send the hex bytes
`24 6D 0D` (`$m\r`)
## Receiving responses
Response format bytes:
| 0 | 1 | 2 | 3 | ... | x |
| -- | -- | -- | -- | --- | - |
| `$` | 0x01 | *\<msg type\>* | *\<payload length\>* | *\<payload\>...* | `\r` |
Byte 1, 0x01 indicates that this is a response
Byte 2 is the same messages type that was used to trigger this response
Byte 3 is the length of the payload that follows
Bytes 4+ are the payload of the response
The final byte is the terminator
Ex:
`24 01 6D 08 11 22 33 44 55 66 77 88 0D`
#### Mac address response payload
The mac address reponse bytes are in reverse order, if the device responds with `11 22 33 44 55 66 77 88` then the mac address is
`88:77:66:55:44:33:22:11`
#### Install code response payload
The install code bytes are also swapped, see mac address response above
#### Meter reading response payload
| Bytes | Meaning |
| ----- | ------- |
| 0 - 3 | Unknown, usually zeros |
| 4 - 8 | Watt hours consumed. Sometimes an invalid number greater than `0x0040 0000` is returned, which is not well understood |
| 9 - 45 | Unknown, seems to always be zeros |
| 46 - 50 | Unknown, seems to typically be `0x0000 0001` |
| 51 - 55 | Potentially a Watt-Hour to kWh divisor, typically `0x0000 0x03E8` (1000) |
| 56 - 60 | Current watts being consumed |
| 61+ | Unknown |

232
src/emporia_vue_utility.h Normal file
View File

@@ -0,0 +1,232 @@
#include "esphome.h"
#include "sensor.h"
#include "esphome/components/gpio/output/gpio_binary_output.h"
class EmporiaVueUtility : public Component, public UARTDevice {
public:
EmporiaVueUtility(UARTComponent *parent): UARTDevice(parent) {}
Sensor *kWh = new Sensor();
Sensor *W = new Sensor();
const char *TAG = "EmporiaVue";
struct MeterReading {
char header;
char is_resp;
char msg_type;
uint8_t data_len;
byte unknown1[4];
byte watt_hours[4];
byte unknown2[36];
byte unknown3[4];
byte unknown4[4];
byte unknown5[4];
byte watts[4];
byte unknown6[48];
byte unknown7[4];
};
union input_buffer {
byte data[260];
struct Message msg;
struct MeterReading mr;
} input_buffer;
uint16_t pos = 0;
uint16_t data_len;
time_t last_meter_reading = 0;
time_t last_meter_requested = 0;
time_t last_meter_join = 0;
const time_t meter_reading_interval = 5;
const time_t meter_join_interval = 30;
// Reads and logs everything from serial until it runs
// out of data or encounters a 0x0d byte (ascii CR)
void dump_serial_input(bool logit) {
while (available()) {
if (input_buffer.data[pos] == 0x0d) {
break;
}
input_buffer.data[pos] = read();
if (pos == sizeof(input_buffer.data)) {
if (logit) {
ESP_LOGE(TAG, "Filled buffer with garbage:");
ESP_LOG_BUFFER_HEXDUMP(TAG, input_buffer.data, pos, ESP_LOG_ERROR);
}
pos = 0;
} else {
pos++;
}
}
if (pos > 0 && logit) {
ESP_LOGE(TAG, "Skipped input:");
ESP_LOG_BUFFER_HEXDUMP(TAG, input_buffer.data, pos-1, ESP_LOG_ERROR);
}
pos = 0;
data_len = 0;
}
size_t read_msg() {
if (!available()) {
return 0;
}
while (available()) {
char c = read();
uint16_t prev_pos = pos;
input_buffer.data[pos] = c;
pos++;
switch (prev_pos) {
case 0:
if (c != 0x24 ) { // 0x24 == "$", the start of a message
ESP_LOGE(TAG, "Invalid input at position %d: 0x%x", pos, c);
dump_serial_input(true);
pos = 0;
return 0;
}
break;
case 1:
if (c != 0x01 ) { // 0x01 means "response"
ESP_LOGE(TAG, "Invalid input at position %d 0x%x", pos, c);
dump_serial_input(true);
pos = 0;
return 0;
}
break;
case 2:
// This is the message type byte
break;
case 3:
// The 3rd byte should be the data length
data_len = c;
break;
case sizeof(input_buffer.data) - 1:
ESP_LOGE(TAG, "Buffer overrun");
dump_serial_input(true);
return 0;
default:
if (pos < data_len + 5) {
;
} else if (c == 0x0d) { // 0x0d == "/r", which should end a message
return pos;
} else {
ESP_LOGE(TAG, "Invalid terminator at pos %d 0x%x", pos, c);
ESP_LOGE(TAG, "Following char is 0x%x", read());
dump_serial_input(true);
return 0;
}
}
} // while(available())
return 0;
}
uint32_t byte_32_read(const byte *inb) {
uint32_t x = 0;
x += inb[0] << 24;
x += inb[1] << 16;
x += inb[2] << 8;
x += inb[3];
return x;
}
void handle_resp_meter_reading() {
uint32_t input_value;
struct MeterReading *mr;
mr = &input_buffer.mr;
// Read the watt-hours value
input_value = byte_32_read(mr->watt_hours);
if (input_value >= 4194304) {
ESP_LOGE(TAG, "Invalid watt-hours data");
ESP_LOG_BUFFER_HEXDUMP(TAG, mr->watt_hours, 4, ESP_LOG_ERROR);
ESP_LOGE(TAG, "Full packet:");
ESP_LOG_BUFFER_HEXDUMP(TAG, input_buffer.data, pos, ESP_LOG_ERROR);
} else {
kWh->publish_state(float(input_value) / 1000.0);
}
// Read the instant watts value
input_value = byte_32_read(mr->watts);
if (input_value == 8388608) { // Appears to be "missing data" message (0x00 80 00 00)
ESP_LOGD(TAG, "Instant Watts value missing");
}
else if (input_value >= 131072) {
ESP_LOGE(TAG, "Unreasonable watts value %d", input_value);
ESP_LOG_BUFFER_HEXDUMP(TAG, mr->watts, 4, ESP_LOG_ERROR);
ESP_LOGE(TAG, "Full packet:");
ESP_LOG_BUFFER_HEXDUMP(TAG, input_buffer.data, pos, ESP_LOG_ERROR);
} else {
W->publish_state(input_value);
}
}
void handle_resp_meter_join() {
ESP_LOGD(TAG, "Got meter join response");
}
void send_meter_request() {
const byte msg[] = { 0x24, 0x72, 0x0d };
ESP_LOGD(TAG, "Sending request for meter reading");
write_array(msg, sizeof(msg));
}
void send_meter_join() {
const byte msg[] = { 0x24, 0x6a, 0x0d };
ESP_LOGD(TAG, "Sending meter join");
write_array(msg, sizeof(msg));
}
void setup() override {
write(0x0d);
dump_serial_input(false);
sleep(1);
dump_serial_input(false);
sleep(1);
dump_serial_input(false);
}
void loop() override {
float curr_kWh;
uint32_t curr_Watts;
int packet_len = 0;
int bytes_rx;
char msg_type = 0;
size_t msg_len = 0;
byte inb;
time_t now;
msg_len = read_msg();
now = time(&now);
if (msg_len != 0) {
msg_type = input_buffer.data[2];
switch (msg_type) {
case 'r': // Meter reading
handle_resp_meter_reading();
last_meter_reading = now;
break;
case 'j': // Meter reading
handle_resp_meter_join();
break;
default:
ESP_LOGE(TAG, "Unhandled response type '%c'", msg_type);
ESP_LOG_BUFFER_HEXDUMP(TAG, input_buffer.data, msg_len, ESP_LOG_ERROR);
break;
}
pos = 0;
}
if (now - last_meter_requested > meter_reading_interval) {
send_meter_request();
last_meter_requested = now;
}
if ((now - last_meter_reading > (meter_reading_interval * 5))
&& (now - last_meter_join > meter_join_interval)) {
send_meter_join();
last_meter_join = now;
}
}
};

127
src/vue-utility-full.yaml Normal file
View File

@@ -0,0 +1,127 @@
###############
# Full config #
###############
esphome:
name: vue-utility
platform: ESP32
board: esp-wrover-kit
includes:
- emporia_vue_utility.h
# Add your own wifi credentials
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ota:
logger:
api:
mqtt:
broker: !secret mqtt_broker
id: vue-utility
username: !secret mqtt_username
password: !secret mqtt_password
discovery: False # Only if you use the HA API usually
# This uart connects to the MGM111
uart:
id: emporia_uart
rx_pin: GPIO21
tx_pin: GPIO22
baud_rate: 115200
sensor:
- platform: custom
lambda: |-
auto vue = new EmporiaVueUtility(id(emporia_uart));
App.register_component(vue);
return {vue->kWh, vue->W};
sensors:
- name: "kWh"
id: kwh
accuracy_decimals: 3
state_class: total_increasing
device_class: energy
# Reduce the rate of reporting the value to
# once every 5 minutes and/or when 0.1 kwh
# have been consumed, unless the fast_reporting
# button has been pushed
filters:
- or:
- throttle: 5m
- delta: 0.1
- lambda: |-
if (id(fast_reporting)) return(x);
return {};
on_raw_value:
then:
lambda: |-
ESP_LOGI("Vue", "kWh = %0.3f", x);
- name: "Watts"
id: watts
unit_of_measurement: "W"
accuracy_decimals: 0
state_class: measurement
device_class: power
# Report every 5 minutes or when +/- 20 watts
filters:
- or:
- throttle: 5m
- delta: 20
- lambda: |-
if (id(fast_reporting)) return(x);
return {};
on_raw_value:
then:
lambda: |-
ESP_LOGI("Vue", "Watts = %0.3f", x);
# This gives you a button that temporarily causes results to be
# reported every few seconds instead of on significant change
# and/or every 5 minutes
button:
- platform: template
name: "Fast Reporting"
id: fast_reporting_button
on_press:
- lambda: id(fast_reporting) = true;
- delay: 5min
- lambda: id(fast_reporting) = false;
# Global value for above button
globals:
- id: fast_reporting
type: bool
restore_value: no
initial_value: "false"
# This LED is marked D3 on the pcb and is the power led on the case
status_led:
pin:
number: GPIO25
# It *is* inverted, but being normally on looks better
inverted: false
# The other two LEDs on the device
# Not currently used by anything
output:
# D2 LED / "Wifi" on the case
- platform: gpio
id: wifi_led
name: "Wifi LED"
pin:
number: GPIO33
inverted: true
# D1 LED / "connect" on the case
- platform: gpio
id: connect_led
name: "Connect LED"
pin:
number: GPIO32
inverted: true

View File

@@ -0,0 +1,41 @@
#######################
# Bare minimum config #
#######################
esphome:
name: vue-utility
platform: ESP32
board: esp-wrover-kit
includes:
- emporia_vue_utility.h
# Add your own wifi credentials
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ota:
logger:
api:
# This uart connects to the MGM111
uart:
id: emporia_uart
rx_pin: GPIO21
tx_pin: GPIO22
baud_rate: 115200
sensor:
- platform: custom
lambda: |-
auto vue = new EmporiaVueUtility(id(emporia_uart));
App.register_component(vue);
return {vue->kWh, vue->W};
sensors:
- name: "kWh"
id: kwh
- name: "Watts"
id: watts