mirror of
https://github.com/nekorevend/esphome-emporia-vue-utility.git
synced 2026-01-10 05:10:40 -07:00
Initial Commit
This commit is contained in:
33
README.md
33
README.md
@@ -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
56
docs/pinout.md
Normal file
@@ -0,0 +1,56 @@
|
||||
## Pinouts
|
||||
|
||||

|
||||
|
||||
### 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
55
docs/protocol.md
Normal 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
232
src/emporia_vue_utility.h
Normal 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
127
src/vue-utility-full.yaml
Normal 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
|
||||
|
||||
41
src/vue-utility-minimal.yaml
Normal file
41
src/vue-utility-minimal.yaml
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user