├── .gitignore ├── LICENSE ├── README.md ├── components └── espsense │ ├── __init__.py │ └── espsense.h ├── configs ├── .gitignore ├── espsense-BN-Link_BNC-60_U133TJ.yml ├── espsense-EFUN_SH331W.yml ├── espsense-TopGreener_TGWF115APM.yml ├── espsense-TopGreener_TGWF115PQM.yml ├── espsense-basic.yml └── espsense.h └── espsense-example.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | **/.pioenvs/ 6 | **/.piolibdeps/ 7 | **/lib/ 8 | **/src/ 9 | **/platformio.ini 10 | /secrets.yaml 11 | *.pcap* 12 | *.pyc 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Charles Powell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPSense 2 | Use ESPHome to create virtual TP-Link Kasa HS110 plugs, that report energy usage to your Sense Home Energy Monitor 3 | 4 | Similar to the [SenseLink](https://github.com/cbpowell/SenseLink) project, ESPSense is an [ESPHome](https://esphome.io) custom component that emulates the protocol of [TP-Link Kasa HS110](https://www.tp-link.com/us/home-networking/smart-plug/hs110/) energy monitoring plugs. This lets you use your own ESP devices to report energy usage **directly** to your [Sense Home Energy Monitor](https://sense.com/)! 5 | 6 | **You should use this tool at your own risk!** Sense is not obligated to provide any support related to issues with this project, and there's no guarantee everything will reliably work, or even work at all. Neither I or Sense can guarantee it won't affect your Sense data, particularly if things go wrong! 7 | 8 | # Confirmed Compatible Smart Plugs 9 | One of the more useful cases is flashing other (commercial) energy-monitoring smart plugs with ESPHome, and then using them with ESPSense with no other integration required. Check out [the wiki](https://github.com/cbpowell/ESPSense/wiki) for details on confirmed "conversions" of other plugs, and an [OTA flash guide](https://github.com/cbpowell/ESPSense/wiki/Flashing-ESPHome-via-OTA)! 10 | 11 | The focus on the wiki is for plugs that are re-flashable "over the air" for simplicity, but if you're comfortable with soldering (and opening the plug) there are an incredible number of compatible plugs/devices compatible with ESPHome. 12 | 13 | # Usage 14 | Modify/create your ESPHome YAML definition to include: 15 | 1. an `external_component` directive, that specifies this component 16 | 2. a top level `espsense` directive, to configure the ESPSense component by specifying which ESPHome sensor(s) to utilize for power data for each plug (note: these can also be [template sensors](https://esphome.io/components/sensor/template.html) that return a wattage value!) 17 | 18 | From the included example YAML file: 19 | 20 | ```yaml 21 | external_components: 22 | # Pull the esphome component in from this GitHub repo 23 | - source: github://cbpowell/ESPSense 24 | components: [ espsense ] 25 | 26 | # Template sensor as an example 27 | sensor: 28 | - platform: template 29 | name: Test Sensor 30 | id: test_sensor 31 | unit_of_measurement: W 32 | 33 | espsense: 34 | # You can define up to 10 "plugs" to report to Sense 35 | # Power value can come from any of the following: 36 | # * A power sensor (in Watts) 37 | # * Calculated from a current sensor (in Amps) and a voltage sensor (in Volts) 38 | # * Calculated from a current sensor (in Amps) and a fixed voltage value 39 | plugs: 40 | - name: espsense 41 | power_sensor: test_sensor 42 | # current_sensor: some_current_sensor 43 | # voltage_sensor: some_voltage_sensor 44 | # voltage: 120.0 45 | # encrypt: false 46 | # mac_address: 35:4B:91:A1:FE:CC 47 | ``` 48 | ### Power Sensor 49 | Note that whatever sensor you tell ESPSense to monitor is assumed to report a **state in the units of watts!** If you want to report the power usage of a device indirectly (such as scaled on another parameter, or simply if on or off), you'll need to create a template sensor in ESPHome to calculate/report the wattage. 50 | 51 | ### MAC Address 52 | By default, the first plug defined will use the hardware MAC address of your device, if no MAC is explicitly configured. If additional plugs are defined (on the same hardware device) and no specific MAC is configured for those, a MAC address will be automatically generated for each from a hash of the provided plug name. 53 | 54 | ### Voltage and Current 55 | Sense does not currently care about plug voltage or current readings, but this is implemented to support data collection by things other than Sense, or in case Sense does eventually implement it! 56 | 57 | ### Encryption 58 | TP-Link plugs use a light "encryption" of the transmitted data, and the Sense monitor does expect to receive the data in encrypted form, so generally you will want to leave the `encrypt` setting as default (true). However you can specify to disable encryption if desired, which could be utilize for your own custom data collection approaches. 59 | 60 | 61 | Copyright 2020, Charles Powell 62 | -------------------------------------------------------------------------------- /components/espsense/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.core import CORE 4 | from esphome.components import sensor 5 | from esphome.const import CONF_ID, CONF_NAME, CONF_VOLTAGE, CONF_MAC_ADDRESS 6 | 7 | AUTO_LOAD = ["json"] 8 | 9 | CONF_PLUGS = "plugs" 10 | CONF_POWER_SENSOR = "power_sensor" 11 | CONF_CURRENT_SENSOR = "current_sensor" 12 | CONF_VOLTAGE_SENSOR = "voltage_sensor" 13 | CONF_ENCRYPT = "encrypt" 14 | 15 | json_ns = cg.esphome_ns.namespace("json") 16 | 17 | espsense_ns = cg.esphome_ns.namespace("espsense") 18 | ESPSense = espsense_ns.class_("ESPSense", cg.Component) 19 | ESPSensePlug = espsense_ns.class_("ESPSensePlug") 20 | 21 | def validate_plug_config(config): 22 | if CONF_POWER_SENSOR in config: 23 | return config 24 | elif CONF_CURRENT_SENSOR in config and CONF_VOLTAGE_SENSOR in config: 25 | return config 26 | elif CONF_CURRENT_SENSOR in config and CONF_VOLTAGE in config: 27 | return config 28 | 29 | raise cv.Invalid('invalid plug config') 30 | 31 | CONFIG_SCHEMA = ( 32 | cv.Schema( 33 | { 34 | cv.GenerateID(): cv.declare_id(ESPSense), 35 | cv.Required(CONF_PLUGS): cv.All( 36 | cv.ensure_list( 37 | { 38 | cv.GenerateID(): cv.declare_id(ESPSensePlug), 39 | cv.Required(CONF_NAME): cv.string, 40 | cv.Optional(CONF_POWER_SENSOR): cv.use_id(sensor.Sensor), 41 | cv.Optional(CONF_CURRENT_SENSOR): cv.use_id(sensor.Sensor), 42 | cv.Optional(CONF_VOLTAGE_SENSOR): cv.use_id(sensor.Sensor), 43 | cv.Optional(CONF_VOLTAGE, default="120.0"): cv.positive_float, 44 | cv.Optional(CONF_ENCRYPT, default="true"): cv.boolean, 45 | cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, 46 | }, 47 | validate_plug_config, 48 | ), 49 | cv.Length(min=1) 50 | ) 51 | } 52 | ) 53 | .extend(cv.COMPONENT_SCHEMA) 54 | ) 55 | 56 | async def to_code(config): 57 | if CORE.is_esp8266: 58 | cg.add_library("ESPAsyncUDP", "") 59 | elif CORE.is_esp32: 60 | cg.add_library("ESP32 Async UDP", None) 61 | 62 | var = cg.new_Pvariable(config[CONF_ID]) 63 | await cg.register_component(var, config) 64 | 65 | for plug_config in config[CONF_PLUGS]: 66 | plug_var = cg.new_Pvariable(plug_config[CONF_ID]) 67 | cg.add(plug_var.set_name(plug_config[CONF_NAME])) 68 | if CONF_POWER_SENSOR in plug_config: 69 | power_sensor = await cg.get_variable(plug_config[CONF_POWER_SENSOR]) 70 | cg.add(plug_var.set_power_sensor(power_sensor)) 71 | if CONF_CURRENT_SENSOR in plug_config: 72 | current_sensor = await cg.get_variable(plug_config[CONF_CURRENT_SENSOR]) 73 | cg.add(plug_var.set_current_sensor(current_sensor)) 74 | if CONF_VOLTAGE_SENSOR in plug_config: 75 | voltage_sensor = await cg.get_variable(plug_config[CONF_VOLTAGE_SENSOR]) 76 | cg.add(plug_var.set_voltage_sensor(voltage_sensor)) 77 | if CONF_MAC_ADDRESS in plug_config: 78 | cg.add(plug_var.set_mac_address(plug_config[CONF_MAC_ADDRESS])) 79 | cg.add(plug_var.set_voltage(plug_config[CONF_VOLTAGE])) 80 | cg.add(plug_var.set_encrypt(plug_config[CONF_ENCRYPT])) 81 | cg.add(var.addPlug(plug_var)) -------------------------------------------------------------------------------- /components/espsense/espsense.h: -------------------------------------------------------------------------------- 1 | // Copyright 2022, Charles Powell 2 | 3 | #include "esphome/components/json/json_util.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/core/application.h" 6 | #include "esphome/core/component.h" 7 | #include "esphome/core/helpers.h" 8 | #include "esphome/core/log.h" 9 | #include "esphome/core/version.h" 10 | 11 | #ifdef ARDUINO_ARCH_ESP32 12 | #include "AsyncUDP.h" 13 | #endif 14 | #ifdef ARDUINO_ARCH_ESP8266 15 | #include "ESPAsyncUDP.h" 16 | #endif 17 | 18 | namespace esphome { 19 | namespace espsense { 20 | 21 | #define RES_SIZE 400 22 | #define REQ_SIZE 70 23 | #define MAX_PLUG_COUNT 10 // Somewhat arbitrary as of now 24 | 25 | class ESPSensePlug { 26 | public: 27 | std::string name; 28 | std::string mac; 29 | bool encrypt = true; 30 | float voltage = 120.0; 31 | sensor::Sensor *power_sid = NULL; 32 | sensor::Sensor *voltage_sid = NULL; 33 | sensor::Sensor *current_sid = NULL; 34 | 35 | std::string base_json = "{\"emeter\": {\"get_realtime\":{ " 36 | "\"current\": %.02f, \"voltage\": %.02f, \"power\": %.02f, \"total\": 0, \"err_code\": 0}}, " 37 | "\"system\": {\"get_sysinfo\": " 38 | "{\"err_code\": 0, \"hw_ver\": 1.0, \"type\": \"IOT.SMARTPLUGSWITCH\", \"model\": \"HS110(US)\", " 39 | "\"mac\": \"%s\", \"deviceId\": \"%s\", \"alias\": \"%s\", \"relay_state\": 1, \"updating\": 0 }}}"; 40 | 41 | ESPSensePlug() {} 42 | 43 | void set_name(std::string name) { this->name = name; } 44 | void set_mac_address(std::string mac) { this->mac = mac; } 45 | void set_encrypt(bool encrypt) { this->encrypt = encrypt; } 46 | void set_voltage(float voltage) { this->voltage = voltage; } 47 | void set_power_sensor(sensor::Sensor *sensor) { this->power_sid = sensor; } 48 | void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sid = sensor; } 49 | void set_current_sensor(sensor::Sensor *sensor) { this->current_sid = sensor; } 50 | 51 | float get_power() { 52 | return get_sensor_reading(power_sid, 0.0); 53 | } 54 | 55 | float get_voltage() { 56 | return get_sensor_reading(voltage_sid, voltage); 57 | } 58 | 59 | float get_current() { 60 | return get_sensor_reading(current_sid, get_power() / get_voltage()); 61 | } 62 | 63 | float get_sensor_reading(sensor::Sensor *sid, float default_value) { 64 | if(sid != NULL && id(sid).has_state()) { 65 | return id(sid).state; 66 | } else { 67 | return default_value; 68 | } 69 | } 70 | 71 | int generate_response(char *data) { 72 | float power = get_power(); 73 | float voltage = get_voltage(); 74 | float current = get_current(); 75 | int response_len = snprintf(data, RES_SIZE, base_json.c_str(), current, voltage, power, mac.c_str(), mac.c_str(), name.c_str()); 76 | ESP_LOGD("ESPSense", "JSON out: %s", data); 77 | return response_len; 78 | } 79 | }; 80 | 81 | class ESPSense : public Component { 82 | public: 83 | AsyncUDP udp; 84 | 85 | ESPSense() : Component() {} 86 | 87 | float get_setup_priority() const override { return esphome::setup_priority::AFTER_WIFI; } 88 | 89 | void setup() override { 90 | if(udp.listen(9999)) { 91 | ESP_LOGI("ESPSense","Listening on port 9999"); 92 | // Parse incoming packets 93 | start_sense_response(); 94 | } else { 95 | ESP_LOGE("ESPSense", "Failed to start UDP listener!"); 96 | } 97 | } 98 | 99 | void addPlug(ESPSensePlug *plug) { 100 | if (plugs.size() >= MAX_PLUG_COUNT) { 101 | ESP_LOGW("ESPSense", "Attempted to add more than %ui plugs, ignoring", MAX_PLUG_COUNT); 102 | } 103 | 104 | if (plug->mac.empty()) 105 | { 106 | if (plugs.size() == 0) 107 | { 108 | // First plug to be added, and no MAC set, so default to own hardware MAC 109 | plug->set_mac_address(get_mac_address_pretty()); 110 | } else { 111 | // Generate a fake MAC address from the name to prevent issues when there are multiple plugs with the same MAC address 112 | uint32_t name_hash = fnv1_hash(plug->name); 113 | uint8_t *hash_pointer = (uint8_t *)&name_hash; 114 | char mac[20]; 115 | sprintf(mac, "%02X:%02X:%02X:%02X:%02X:%02X", 53, 75, hash_pointer[0], hash_pointer[1], hash_pointer[2], hash_pointer[3]); 116 | plug->set_mac_address(mac); 117 | } 118 | } 119 | 120 | plugs.push_back(plug); 121 | } 122 | 123 | private: 124 | float voltage; 125 | char response_buf[RES_SIZE]; 126 | std::vector plugs; 127 | 128 | #if ESPHOME_VERSION_CODE < VERSION_CODE(2022, 1, 0) 129 | StaticJsonBuffer<200> jsonBuffer; 130 | #endif 131 | 132 | void start_sense_response() { 133 | ESP_LOGI("ESPSense","Starting ESPSense listener"); 134 | udp.onPacket([&](AsyncUDPPacket &packet) { 135 | parse_packet(packet); 136 | }); 137 | } 138 | 139 | void parse_packet(AsyncUDPPacket &packet) { 140 | ESP_LOGD("ESPSense", "Got packet from %s", packet.remoteIP().toString().c_str()); 141 | 142 | if(packet.length() > REQ_SIZE) { 143 | // Not a Sense request packet 144 | ESP_LOGD("ESPSense", "Packet is oversized, ignoring"); 145 | return; 146 | } 147 | 148 | char request_buf[REQ_SIZE]; 149 | 150 | // Decrypt 151 | decrypt(packet.data(), packet.length(), request_buf); 152 | 153 | // Add null terminator 154 | request_buf[packet.length()] = '\0'; 155 | 156 | // Print into null-terminated string if verbose debugging 157 | ESP_LOGV("ESPSense", "Message: %s", request_buf); 158 | 159 | // Parse JSON 160 | #if ESPHOME_VERSION_CODE >= VERSION_CODE(2022, 1, 0) 161 | // ArduinoJson 6 162 | StaticJsonDocument<200> jsonDoc, emeterDoc; 163 | auto jsonError = deserializeJson(jsonDoc, request_buf); 164 | if(jsonError) { 165 | ESP_LOGW("ESPSense", "JSON parse failed! Error: %s", jsonError.c_str()); 166 | return; 167 | } 168 | ESP_LOGD("ESPSense", "Parse of message JSON successful"); 169 | 170 | // Check if this is a valid request by looking for emeter key 171 | if (!jsonDoc["emeter"]["get_realtime"]) { 172 | ESP_LOGD("ESPSense", "Message was not deserialized as a request for power measurement"); 173 | } else { 174 | #else 175 | // ArduinoJson 5 176 | jsonBuffer.clear(); 177 | JsonObject &req = jsonBuffer.parseObject(request_buf); 178 | if(!req.success()) { 179 | ESP_LOGW("ESPSense", "JSON parse failed!"); 180 | return; 181 | } 182 | ESP_LOGD("ESPSense", "Parse of message JSON successful"); 183 | 184 | // Check if this is a valid request by looking for emeter key 185 | JsonVariant request = req["emeter"]["get_realtime"]; 186 | if (!request.success()) { 187 | ESP_LOGD("ESPSense", "Message not a request for power measurement"); 188 | } else { 189 | #endif 190 | ESP_LOGD("ESPSense", "Power measurement requested"); 191 | for (auto *plug : this->plugs) { 192 | // Generate JSON response string 193 | int response_len = plug->generate_response(response_buf); 194 | char response[response_len]; 195 | if (plug->encrypt) { 196 | // Encrypt 197 | encrypt(response_buf, response_len, response); 198 | // Respond to request 199 | packet.write((uint8_t *)response, response_len); 200 | } else { 201 | // Response to request 202 | packet.write((uint8_t *)response_buf, response_len); 203 | } 204 | } 205 | } 206 | } 207 | 208 | void decrypt(const uint8_t *data, size_t len, char* result) { 209 | uint8_t key = 171; 210 | uint8_t a; 211 | for (int i = 0; i < len; i++) { 212 | uint8_t unt = data[i]; 213 | a = unt ^ key; 214 | key = unt; 215 | result[i] = char(a); 216 | } 217 | } 218 | 219 | void encrypt(const char *data, size_t len, char* result) { 220 | uint8_t key = 171; 221 | uint8_t a; 222 | for (int i = 0; i < len; i++) { 223 | uint8_t unt = data[i]; 224 | a = unt ^ key; 225 | key = a; 226 | result[i] = a; 227 | } 228 | } 229 | }; 230 | 231 | } // namespace espsense 232 | } // namespace esphome 233 | -------------------------------------------------------------------------------- /configs/.gitignore: -------------------------------------------------------------------------------- 1 | # Gitignore settings for ESPHome 2 | # This is an example and may include too much for your use-case. 3 | # You can modify this file to suit your needs. 4 | /.esphome/ 5 | **/.pioenvs/ 6 | **/.piolibdeps/ 7 | **/lib/ 8 | **/src/ 9 | **/platformio.ini 10 | /secrets.yaml 11 | -------------------------------------------------------------------------------- /configs/espsense-BN-Link_BNC-60_U133TJ.yml: -------------------------------------------------------------------------------- 1 | 2 | # Configuration for BN-Link BNC-60/U133TJ 3 | 4 | substitutions: 5 | plug_name: bnlink_01 6 | # Plug state to set upon powerup (or after power loss) 7 | # See options here: https://esphome.io/components/switch/gpio.html 8 | restore_mode: ALWAYS_ON 9 | 10 | # Base calibration to 90W lightbulb, Kill-a-Watt between plug and wall 11 | # Detail calibration can be done with calibrate_linear sensor filters below 12 | current_res: "0.00243" 13 | voltage_div: "755" 14 | # Increasing current_res reduces reported wattage 15 | # Increasing voltage_div increases reported voltage 16 | 17 | esphome: 18 | name: ${plug_name} 19 | platform: ESP8266 20 | board: esp01_1m 21 | 22 | wifi: 23 | ssid: !secret wifi_ssid 24 | password: !secret wifi_pass 25 | fast_connect: on 26 | 27 | # Enable fallback hotspot (captive portal) in case wifi connection fails 28 | ap: 29 | ssid: "${plug_name} Fallback Hotspot" 30 | password: !secret ap_pass 31 | 32 | ota: 33 | safe_mode: True 34 | password: !secret ota_pass 35 | 36 | captive_portal: 37 | 38 | # Enable logging 39 | logger: 40 | # level: DEBUG 41 | baud_rate: 0 # Disable UART logging, we have no physical connections! 42 | 43 | # Home Assistant API 44 | # Comment out if not using API, but you'll also need to remove the total_daily_energy and 45 | # time sensors below 46 | api: 47 | 48 | time: 49 | - platform: homeassistant 50 | id: homeassistant_time 51 | 52 | binary_sensor: 53 | - platform: gpio 54 | pin: 55 | number: GPIO3 56 | inverted: True 57 | name: "${plug_name} Button" 58 | on_press: 59 | then: 60 | - switch.toggle: "relay" 61 | 62 | switch: 63 | # Main plug control relay 64 | - platform: gpio 65 | name: "${plug_name} Relay" 66 | id: "relay" 67 | pin: GPIO14 68 | restore_mode: ${restore_mode} 69 | # Use blue LED to indicate relay state 70 | on_turn_on: 71 | - switch.turn_on: "led_blue" 72 | on_turn_off: 73 | - switch.turn_off: "led_blue" 74 | 75 | - platform: gpio 76 | name: "${plug_name} LED Blue" 77 | id: "led_blue" 78 | pin: GPIO13 79 | inverted: True 80 | restore_mode: ${restore_mode} # Matches relay 81 | 82 | # Red LED used for ESPHome status below, but this could be 83 | # reconfigured for other purposes! 84 | # - platform: gpio 85 | # name: "${plug_name} LED Red" 86 | # id: "led_red" 87 | # pin: GPIO1 88 | # inverted: True 89 | # restore_mode: ALWAYS_OFF 90 | 91 | status_led: 92 | # Use Red LED as ESPHome's built-in status indicator 93 | pin: 94 | number: GPIO1 95 | inverted: True 96 | 97 | sensor: 98 | - platform: hlw8012 99 | sel_pin: 100 | number: GPIO12 101 | inverted: True 102 | cf_pin: GPIO4 103 | cf1_pin: GPIO5 104 | current_resistor: ${current_res} 105 | voltage_divider: ${voltage_div} 106 | current: 107 | name: "${plug_name} Amperage" 108 | unit_of_measurement: A 109 | filters: 110 | # - calibrate_linear: 111 | # # Map X (from sensor) to Y (true value) 112 | # # At least 2 data points required 113 | # - 0.0 -> 0.0 114 | # - 1.0 -> 1.0 #load was on 115 | voltage: 116 | name: "${plug_name} Voltage" 117 | unit_of_measurement: V 118 | filters: 119 | # - calibrate_linear: 120 | # # Map X (from sensor) to Y (true value) 121 | # # At least 2 data points required 122 | # - 0.0 -> 0.0 123 | # - 1.0 -> 1.0 #load was on 124 | power: 125 | id: "wattage" 126 | name: "${plug_name} Wattage" 127 | unit_of_measurement: W 128 | filters: 129 | # - calibrate_linear: 130 | # # Map X (from sensor) to Y (true value) 131 | # # At least 2 data points required 132 | # - 0.0 -> 0.0 133 | # - 1.0 -> 1.0 #load was on 134 | change_mode_every: 8 135 | update_interval: 3s # Longer interval gives better accuracy 136 | 137 | - platform: total_daily_energy 138 | name: "${plug_name} Total Daily Energy" 139 | power_id: "wattage" 140 | filters: 141 | # Multiplication factor from W to kW is 0.001 142 | - multiply: 0.001 143 | unit_of_measurement: kWh 144 | 145 | # Extra sensor to keep track of plug uptime 146 | - platform: uptime 147 | name: ${plug_name} Uptime Sensor 148 | 149 | external_components: 150 | - source: github://cbpowell/ESPSense 151 | components: [ espsense ] 152 | 153 | espsense: 154 | plugs: 155 | - name: ${plug_name} 156 | power_sensor: wattage 157 | voltage: 120.0 158 | -------------------------------------------------------------------------------- /configs/espsense-EFUN_SH331W.yml: -------------------------------------------------------------------------------- 1 | 2 | #Configuration for EFUN SH3331W Smart Plug 3 | 4 | substitutions: 5 | plug_name: efun_plug1 6 | # Plug state to set upon powerup (or after power loss) 7 | # See options here: https://esphome.io/components/switch/gpio.html 8 | restore_mode: ALWAYS_ON 9 | 10 | # Base calibration to 90W lightbulb, Kill-a-Watt between plug and wall 11 | # Detail calibration can be done with calibrate_linear sensor filters below 12 | current_res: "0.00236" 13 | voltage_div: "878" 14 | # Increasing current_res reduces reported wattage 15 | # Increasing voltage_div increases reported voltage 16 | 17 | esphome: 18 | name: ${plug_name} 19 | platform: ESP8266 20 | board: esp01_1m 21 | 22 | wifi: 23 | ssid: !secret wifi_ssid 24 | password: !secret wifi_pass 25 | fast_connect: on 26 | 27 | # Enable fallback hotspot (captive portal) in case wifi connection fails 28 | ap: 29 | ssid: "${plug_name} Fallback Hotspot" 30 | password: !secret ap_pass 31 | 32 | ota: 33 | safe_mode: True 34 | password: !secret ota_pass 35 | 36 | captive_portal: 37 | 38 | # Enable logging 39 | logger: 40 | # level: DEBUG 41 | baud_rate: 0 # Disable UART logging, we have no physical connections! 42 | 43 | # Home Assistant API 44 | # Comment out if not using API, but you'll also need to remove the total_daily_energy and 45 | # time sensors below 46 | api: 47 | 48 | time: 49 | - platform: homeassistant 50 | id: homeassistant_time 51 | 52 | binary_sensor: 53 | - platform: gpio 54 | pin: 55 | number: GPIO13 56 | inverted: True 57 | name: "${plug_name}_button" 58 | on_press: 59 | then: 60 | - switch.toggle: "relay" 61 | 62 | switch: 63 | # Main plug control relay 64 | - platform: gpio 65 | name: "${plug_name} Relay" 66 | id: "relay" 67 | pin: GPIO15 68 | restore_mode: ${restore_mode} 69 | # Use blue LED to indicate relay state 70 | on_turn_on: 71 | - switch.turn_on: "led_blue" 72 | on_turn_off: 73 | - switch.turn_off: "led_blue" 74 | 75 | - platform: gpio 76 | name: "${plug_name} Blue LED" 77 | id: "led_blue" 78 | pin: GPIO2 79 | inverted: True 80 | restore_mode: ${restore_mode} # Matches relay 81 | 82 | # - platform: gpio 83 | # name: "${plug_name}_LED_Red" 84 | # pin: GPIO0 85 | # inverted: True 86 | # restore_mode: ALWAYS_OFF 87 | 88 | status_led: 89 | # Use Red LED as ESPHome's built-in status indicator 90 | pin: 91 | number: GPIO0 92 | inverted: True 93 | 94 | sensor: 95 | - platform: hlw8012 96 | sel_pin: 97 | number: GPIO12 98 | inverted: True 99 | cf_pin: GPIO05 100 | cf1_pin: GPIO14 101 | current_resistor: ${current_res} 102 | voltage_divider: ${voltage_div} 103 | current: 104 | name: "${plug_name} Amperage" 105 | unit_of_measurement: A 106 | filters: 107 | # - calibrate_linear: 108 | # # Map X (from sensor) to Y (true value) 109 | # # At least 2 data points required 110 | # - 0.0 -> 0.0 111 | # - 1.0 -> 1.0 #load was on 112 | voltage: 113 | name: "${plug_name} Voltage" 114 | unit_of_measurement: V 115 | filters: 116 | # - calibrate_linear: 117 | # # Map X (from sensor) to Y (true value) 118 | # # At least 2 data points required 119 | # - 0.0 -> 0.0 120 | # - 1.0 -> 1.0 #load was on 121 | power: 122 | id: "wattage" 123 | name: "${plug_name} Wattage" 124 | unit_of_measurement: W 125 | filters: 126 | # - calibrate_linear: 127 | # # Map X (from sensor) to Y (true value) 128 | # # At least 2 data points required 129 | # - 0.0 -> 0.0 130 | # - 1.0 -> 1.0 #load was on 131 | change_mode_every: 8 132 | update_interval: 3s 133 | 134 | - platform: total_daily_energy 135 | name: "${plug_name} Total Daily Energy" 136 | power_id: "wattage" 137 | filters: 138 | # Multiplication factor from W to kW is 0.001 139 | - multiply: 0.001 140 | unit_of_measurement: kWh 141 | 142 | # Extra sensor to keep track of plug uptime 143 | - platform: uptime 144 | name: ${plug_name} Uptime Sensor 145 | 146 | external_components: 147 | - source: github://cbpowell/ESPSense 148 | components: [ espsense ] 149 | 150 | espsense: 151 | plugs: 152 | - name: ${plug_name} 153 | power_sensor: wattage 154 | voltage: 120.0 155 | -------------------------------------------------------------------------------- /configs/espsense-TopGreener_TGWF115APM.yml: -------------------------------------------------------------------------------- 1 | 2 | # Configuration for TGWF115APM (Big 15A plug) 3 | 4 | substitutions: 5 | plug_name: topgreener_apm 6 | # Plug state to set upon powerup (or after power loss) 7 | # See options here: https://esphome.io/components/switch/gpio.html 8 | restore_mode: ALWAYS_ON 9 | 10 | # Base calibration to 90W lightbulb, Kill-a-Watt between plug and wall 11 | # Detail calibration can be done with calibrate_linear sensor filters below 12 | current_res: "0.00212" 13 | voltage_div: "2120" 14 | # Increasing current_res reduces reported wattage 15 | # Increasing voltage_div increases reported voltage 16 | 17 | esphome: 18 | name: ${plug_name} 19 | platform: ESP8266 20 | board: esp01_1m 21 | 22 | wifi: 23 | ssid: !secret wifi_ssid 24 | password: !secret wifi_pass 25 | fast_connect: on 26 | 27 | # Enable fallback hotspot (captive portal) in case wifi connection fails 28 | ap: 29 | ssid: "${plug_name} Fallback Hotspot" 30 | password: !secret ap_pass 31 | 32 | ota: 33 | safe_mode: True 34 | password: !secret ota_pass 35 | 36 | captive_portal: 37 | 38 | # web_server: 39 | 40 | # Logging 41 | logger: 42 | # level: DEBUG 43 | baud_rate: 0 # Disable UART logging, we have no physical connections! 44 | 45 | # Home Assistant API 46 | # Comment out if not using API, but you'll also need to remove the total_daily_energy and 47 | # time sensors below 48 | api: 49 | 50 | time: 51 | - platform: homeassistant 52 | id: homeassistant_time 53 | 54 | binary_sensor: 55 | - platform: gpio 56 | pin: 57 | number: GPIO3 58 | inverted: True 59 | name: "${plug_name} Button" 60 | on_press: 61 | then: 62 | - switch.toggle: "relay" 63 | # Note that blue LED appears to be tied to relay state internally (i.e. electrically) 64 | 65 | switch: 66 | # Main plug control relay 67 | - platform: gpio 68 | name: "${plug_name} Relay" 69 | id: "relay" 70 | pin: GPIO14 71 | restore_mode: ${restore_mode} 72 | 73 | # Used for Status LED below, but could be repurposed! 74 | # - platform: gpio 75 | # name: "${plug_name} Green LED" 76 | # id: "led_green" 77 | # pin: GPIO13 78 | # restore_mode: ALWAYS_ON 79 | 80 | status_led: 81 | # Use Green LED as ESPHome's built-in status indicator 82 | pin: 83 | number: GPIO13 84 | inverted: False 85 | 86 | sensor: 87 | - platform: hlw8012 88 | sel_pin: 89 | number: GPIO12 90 | inverted: True 91 | cf_pin: GPIO04 92 | cf1_pin: GPIO5 93 | current_resistor: ${current_res} 94 | voltage_divider: ${voltage_div} 95 | current: 96 | name: "${plug_name} Amperage" 97 | unit_of_measurement: A 98 | filters: 99 | # - calibrate_linear: 100 | # # Map X (from sensor) to Y (true value) 101 | # # At least 2 data points required 102 | # - 0.0 -> 0.0 103 | # - 1.0 -> 1.0 #load was on 104 | voltage: 105 | name: "${plug_name} Voltage" 106 | unit_of_measurement: V 107 | filters: 108 | # - calibrate_linear: 109 | # # Map X (from sensor) to Y (true value) 110 | # # At least 2 data points required 111 | # - 0.0 -> 0.0 112 | # - 1.0 -> 1.0 #load was on 113 | power: 114 | id: "wattage" 115 | name: "${plug_name} Wattage" 116 | unit_of_measurement: W 117 | filters: 118 | # - calibrate_linear: 119 | # # Map X (from sensor) to Y (true value) 120 | # # At least 2 data points required 121 | # - 0.0 -> 0.0 122 | # - 1.0 -> 1.0 #load was on 123 | change_mode_every: 8 124 | update_interval: 3s # Longer interval gives better accuracy 125 | 126 | - platform: total_daily_energy 127 | name: "${plug_name} Total Daily Energy" 128 | power_id: "wattage" 129 | filters: 130 | # Multiplication factor from W to kW is 0.001 131 | - multiply: 0.001 132 | unit_of_measurement: kWh 133 | 134 | # Extra sensor to keep track of plug uptime 135 | - platform: uptime 136 | name: ${plug_name} Uptime Sensor 137 | 138 | external_components: 139 | - source: github://cbpowell/ESPSense 140 | components: [ espsense ] 141 | 142 | espsense: 143 | plugs: 144 | - name: ${plug_name} 145 | power_sensor: wattage 146 | voltage: 120.0 147 | -------------------------------------------------------------------------------- /configs/espsense-TopGreener_TGWF115PQM.yml: -------------------------------------------------------------------------------- 1 | 2 | # Configuration for TGWF115PQM (Small 10A plug) 3 | 4 | substitutions: 5 | plug_name: topgreener_pqm 6 | # Plug state to set upon powerup (or after power loss) 7 | # See options here: https://esphome.io/components/switch/gpio.html 8 | restore_mode: ALWAYS_ON 9 | 10 | # Base calibration to 90W lightbulb, Kill-a-Watt between plug and wall 11 | # Detail calibration can be done with calibrate_linear sensor filters below 12 | current_res: "0.0019" 13 | voltage_div: "2150" 14 | # Increasing current_res reduces reported wattage 15 | # Increasing voltage_div increases reported voltage 16 | 17 | esphome: 18 | name: ${plug_name} 19 | platform: ESP8266 20 | board: esp01_1m 21 | 22 | wifi: 23 | ssid: !secret wifi_ssid 24 | password: !secret wifi_pass 25 | fast_connect: on 26 | 27 | # Enable fallback hotspot (captive portal) in case wifi connection fails 28 | ap: 29 | ssid: "${plug_name} Fallback Hotspot" 30 | password: !secret ap_pass 31 | 32 | ota: 33 | safe_mode: True 34 | password: !secret ota_pass 35 | 36 | captive_portal: 37 | 38 | # Enable logging 39 | logger: 40 | # level: DEBUG 41 | baud_rate: 0 # Disable UART logging, we have no physical connections! 42 | 43 | # Home Assistant API 44 | # Comment out if not using API, but you'll also need to remove the total_daily_energy and 45 | # time sensors below 46 | api: 47 | 48 | time: 49 | - platform: homeassistant 50 | id: homeassistant_time 51 | 52 | binary_sensor: 53 | - platform: gpio 54 | pin: 55 | number: GPIO3 56 | inverted: True 57 | name: "${plug_name} Button" 58 | on_press: 59 | then: 60 | - switch.toggle: "relay" 61 | # Note that blue LED appears to be tied to relay state internally (i.e. electrically) 62 | 63 | switch: 64 | # Main plug control relay 65 | - platform: gpio 66 | name: "${plug_name} Relay" 67 | id: "relay" 68 | pin: GPIO14 69 | restore_mode: ${restore_mode} 70 | 71 | # Used for Status LED below, but could be repurposed! 72 | # - platform: gpio 73 | # name: "${plug_name} Green LED" 74 | # id: "led_green" 75 | # pin: GPIO13 76 | # restore_mode: ALWAYS_ON 77 | 78 | status_led: 79 | # Use Green LED as ESPHome's built-in status indicator 80 | pin: 81 | number: GPIO13 82 | inverted: False 83 | 84 | sensor: 85 | - platform: hlw8012 86 | sel_pin: 87 | number: GPIO12 88 | inverted: True 89 | cf_pin: GPIO04 90 | cf1_pin: GPIO5 91 | current_resistor: ${current_res} 92 | voltage_divider: ${voltage_div} 93 | current: 94 | name: "${plug_name} Amperage" 95 | unit_of_measurement: A 96 | filters: 97 | # - calibrate_linear: 98 | # # Map X (from sensor) to Y (true value) 99 | # # At least 2 data points required 100 | # - 0.0 -> 0.0 101 | # - 1.0 -> 1.0 #load was on 102 | voltage: 103 | name: "${plug_name} Voltage" 104 | unit_of_measurement: V 105 | filters: 106 | # - calibrate_linear: 107 | # # Map X (from sensor) to Y (true value) 108 | # # At least 2 data points required 109 | # - 0.0 -> 0.0 110 | # - 1.0 -> 1.0 #load was on 111 | power: 112 | id: "wattage" 113 | name: "${plug_name} Wattage" 114 | unit_of_measurement: W 115 | filters: 116 | # Moving average filter to try and reduce a periodic drop of ~1-2W 117 | # Unsure of cause, may be a better solution! 118 | - sliding_window_moving_average: 119 | window_size: 2 120 | send_every: 1 121 | # - calibrate_linear: 122 | # # Map X (from sensor) to Y (true value) 123 | # # At least 2 data points required 124 | # - 0.0 -> 0.0 125 | # - 1.0 -> 1.0 #load was on 126 | change_mode_every: 8 127 | update_interval: 3s # Longer interval gives better accuracy 128 | 129 | - platform: total_daily_energy 130 | name: "${plug_name} Total Daily Energy" 131 | power_id: "wattage" 132 | filters: 133 | # Multiplication factor from W to kW is 0.001 134 | - multiply: 0.001 135 | unit_of_measurement: kWh 136 | 137 | # Extra sensor to keep track of plug uptime 138 | - platform: uptime 139 | name: ${plug_name} Uptime Sensor 140 | 141 | external_components: 142 | - source: github://cbpowell/ESPSense 143 | components: [ espsense ] 144 | 145 | espsense: 146 | plugs: 147 | - name: ${plug_name} 148 | power_sensor: wattage 149 | voltage: 120.0 150 | -------------------------------------------------------------------------------- /configs/espsense-basic.yml: -------------------------------------------------------------------------------- 1 | # Basic "safe" config 2 | 3 | substitutions: 4 | plug_name: esphome_basic 5 | 6 | esphome: 7 | name: ${plug_name} 8 | platform: ESP8266 9 | board: esp01_1m 10 | 11 | wifi: 12 | ssid: !secret wifi_ssid 13 | password: !secret wifi_pass 14 | fast_connect: on 15 | 16 | # Enable fallback hotspot (captive portal) in case wifi connection fails 17 | ap: 18 | ssid: "${plug_name} Fallback Hotspot" 19 | password: !secret ap_pass 20 | 21 | captive_portal: 22 | 23 | api: 24 | 25 | web_server: 26 | port: 80 27 | 28 | ota: 29 | safe_mode: True 30 | password: !secret ota_pass 31 | 32 | # Enable logging 33 | logger: 34 | level: DEBUG 35 | baud_rate: 0 # Disable UART logging, we have no physical connections! 36 | -------------------------------------------------------------------------------- /configs/espsense.h: -------------------------------------------------------------------------------- 1 | ../espsense.h -------------------------------------------------------------------------------- /espsense-example.yml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: espsense 3 | platform: ESP8266 4 | board: nodemcu 5 | 6 | 7 | wifi: 8 | ssid: !secret wifi_ssid 9 | password: !secret wifi_pass 10 | fast_connect: on 11 | 12 | # Enable fallback hotspot (captive portal) in case wifi connection fails 13 | ap: 14 | ssid: "${plug_name} Fallback Hotspot" 15 | password: !secret ap_pass 16 | 17 | captive_portal: 18 | 19 | # Enable logging 20 | logger: 21 | level: DEBUG 22 | 23 | ota: 24 | safe_mode: True 25 | password: "ota_safe_pass" 26 | 27 | # Template sensor, random values published by the below time component as an example 28 | sensor: 29 | - platform: template 30 | name: Test Sensor 31 | id: test_sensor 32 | unit_of_measurement: W 33 | 34 | 35 | time: 36 | - platform: sntp 37 | id: sntp_time 38 | on_time: 39 | # Every 5 sec 40 | - seconds: /5 41 | then: 42 | # Change the sensor value randomly every 5 seconds 43 | # !!!!! Don't actually use this! For demo purposes only !!!!! 44 | - lambda: !lambda |- 45 | long randomPowerState = random(5, 15); 46 | id(test_sensor).publish_state((float)randomPowerState); 47 | 48 | 49 | external_components: 50 | # Pull the esphome component in from this GitHub repo 51 | - source: github://cbpowell/ESPSense 52 | components: [ espsense ] 53 | 54 | espsense: 55 | # You can define up to 10 "plugs" to report to Sense 56 | # Power value can come from any of the following: 57 | # * A power sensor (in Watts) 58 | # * Calculated from a current sensor (in Amps) and a voltage sensor (in Volts) 59 | # * Calculated from a current sensor (in Amps) and a fixed voltage value 60 | plugs: 61 | - name: espsense 62 | power_sensor: test_sensor 63 | # current_sensor: some_current_sensor 64 | # voltage_sensor: some_voltage_sensor 65 | # voltage: 120.0 66 | # encrypt: false 67 | # mac_address: 35:4B:91:A1:FE:CC 68 | --------------------------------------------------------------------------------