SenseLink - TP-Link Smart Plug Emulation

Hi everyone - I’ve written a Python tool/program that allows you to emulate (multiple) TP-Link HS110 plugs, and define power usage data to send to Sense via these emulated plugs. You can find it here:

You can define two types of plugs:

  • Static: These have a constant reported power
  • Dynamic: These types report a power based on some other parameter, through integrations

Edit: Some quick examples:

  1. You have an always-on porch light, that you know consumes 60W constantly, and just disappears into your Other usage. You can define a 60W “static” virtual plug to define that usage for Sense, and have it show up in your device list.
  2. You have a smart dimmer light switch driving 6x 14w dumb LED bulbs, connected to Home Assistant. Using a “dynamic” plug you can map the brightness % value to power usage, and get that data into Sense

Check out the example configuration file for an idea of how the setup works.

At the moment the only integration is with Home Assistant via the Websockets API, but expanding to other IoT/SmartHome systems should be straightforward. I’m planning to implement HTTP GET/POST and MQTT methods soon. I’m also interested in using SenseLink to get “live” data from other energy-monitoring smart plugs into Sense.

I was driven to develop this because I’ve got a few Lutron Caseta dimmer switches that run a lot of scenes/automations - always going to different dim levels, at different times. I wasn’t sure if those would be reliably detected, and I wanted to find a way to get the data into Sense. Plus the challenge to develop it!

SenseLink builds on the work done to decipher the TP-Link protocol. It properly formats and encodes a response like a TP-Link HS110 plug, and reports that back to your Sense monitor when it sends out a broadcast request for plug data. Whatever device you have running this needs to be able to receive the UDP broadcasts from your Sense monitor, so it likely needs to be on the same subnet.

I’ve generated a few Docker images for both x86 and ARM, and have tested it on a RaspberryPi myself.

Hopefully the community finds this useful! I’m certainly open to suggestions/ideas/pull requests to improve it further.

But remember this is an unofficial tool, and Sense is not responsible for anything that happens as a result of using it, nor can they provide any support.

15 Likes

You should check out this Hubitat community developed application. It can control the TP-Link plugs and poll them for energy usage. It uses RawSocket instead of UDP packets.

Currently, the only thing I use it for is using IFTTT, if Sense sees my TV turn on, Hubitat will turn off the plug running the air purifier and then turn it back on 5 seconds later. Would be pretty cool if I could emulate a TP-110 though for some of my devices.

1 Like

Thanks for the heads up! It looks like that is to provide control and status monitoring of Kasa plugs, so you can integrate them with Hubitat?

To clarify, SenseLink kind of works in the opposite direction. It emulates the power monitoring aspect of the HS110 plugs only. It essentially tells your Sense monitor there’s a plug on the network reporting the power usage value you define, but it’s purely virtual.

SenseLink doesn’t allow control or monitoring of actual TP-Link plugs.

Regarding the UDP topic, that’s the way TP-Link integration works with Sense so it’s pretty much the only way to do it. I’ve read elsewhere on the forums about how that works better for them anyway. Your Sense can sent out a broadcast to the entire subnet once, and get a reply from all devices that might respond. In the case of SenseLink, your one device (an Rpi, let’s say) will send one response for each virtual plug you configure.

2 Likes

Really cool ! Great use of an existing interface. If I hadn’t already pretty much instrumented my house to to the limit with HS110s and HS300s, I would try to adapt this interface, likely with my main UPS, which kicks out regular power usage stats.

One caveat for all users - Sense has a 20 smartplug limit, and I’m guessing that goes for this new input vector as well. Get past that and the Sense monitor might go flakey on you due to processing and communication limits. While a 2 second polling period is appropriate for an HS110, it might be overkill for many home hubs that don’t update data that frequently (only on changes of switches, etc).

2 Likes

@kevin1 Thanks! Good point on the 20 device limit, I’ll add that to the README as well.

Re: the 2-second polling period, I’ve actually designed this such that the Sense sampling period (the 2 sec) doesn’t have to match the “data source” sampling period.

For example, the HASS Websockets API only send out update “events” when an attribute changes, but Sense wants an update (semi-)reliably every 2 seconds. SenseLink stores the incoming update values from the Websockets API internally, and then uses the last-update value to respond to Sense when polled (every 2 seconds).

So in short the data source can update as frequently or infrequently as necessary, at whatever rate is most appropriate to the data source, and is not tied to the 2 sec Sense polling period.

2 Likes

@cbpowel, do you already have future integration ideas lined up for SenseLink?

A suggestion, integrate it into some other cheap energy monitor that is capable of 240V monitoring, like TED or iotawatt.

The iotawatt device can already integrate with Home Assistant, so maybe no extra development work is necessary except for clearly documenting a recipe on how to get iotawatt->pvoutput->home assistant->senselink->sense working robustly. That said, embedding senselink directly within the iotawatt source code and bypassing home assistant would make it much easier for the average person to be able to make use of it.

All I had planned for the short term was to add HTTP endpoint and MQTT data sources, just as very generic interface options that people use with maybe a little more integration work on their end (i.e. you figure out how to publish the data, and just tell SenseLink how to look for it). I’d also like to look into what other smart plugs might be good candidates to “translate” via SmartLink, to stand in for the diminishing supply of actual HS110s.

Integrating with iotawatt would be pretty interesting! I agree that skipping the Home Assistant step reduces some complexity if you have no other use for it, but you’ll already need a device running SenseLink anyway. But it looks like HASS just queries the iotawatt API and parses the JSON response, which would be doable directly as well.

1 Like

To help with the 20 device limit, maybe you could do an aggregate device type (all lights)?

1 Like

Yeah true, an aggregate-type plug could be added. I feel like it could get complex, because you’d still have to define the individual plugs and how they get their data, and then also indicate that they’re part of the aggregate plug. But I think I can see a way to manage that. Good idea, I’ll try to add it at some point!

Yeah, I feel like you could group them then expose the end result. I do something similar inside home assistant using template sensors. However this is pretty messy. A docker that manages all this and then sends it to sense would be VERY handy. Even if the output wasn’t necessarily even sense but MQTT I could clean up my config a lot.

Below is the mess I do to group and track inside home assistant itself… As you can see its messy. :frowning:

#Outside Lights Power Meter
  - platform: template
    sensors:
      outside_lights_power:
        friendly_name: Outside Lights
        device_class: power
        value_template: >- 
        {{ ((state_attr('light.outside_garage_1', 'brightness')|float/255|float)*10 + 
           (state_attr('light.outside_garage_2', 'brightness')|float/255|float)*10 +
           (state_attr('light.outside_porch_1', 'brightness')|float/255|float)*8 +
           (state_attr('light.outside_porch_2', 'brightness')|float/255|float)*8 +
           (state_attr('light.outside_lamp_post', 'brightness')|float/255|float)*8 +
           (state_attr('light.lookout', 'brightness')|float/255|float)*10 +
           is_state('switch.outside_deck_light','on') *8 +
           is_state('switch.outside_patio_light','on') * 20 +
           (state_attr('light.sitting_room', 'brightness')|float/255|float)*8 +
           (state_attr('light.garden1', 'brightness')|float/255|float)*8 +
           (state_attr('light.garden2', 'brightness')|float/255|float)*8)
           |round(1)  }}
        unit_of_measurement: 'W'
        entity_id: 
          - sensor.time
          - light.outside_garage_1
          - light.outside_garage_2
          - light.outside_porch_1
          - light.outside_porch_2
          - light.outside_lamp_post
          - light.lookout
          - light.outside_deck_light
          - light.sitting_room
        attribute_templates:
          device_room: outside
          hourly_kwh: "{{states('sensor.outside_lights_hourly_kwh')}}"
          daily_kwh: "{{states('sensor.outside_lights_daily_kwh')}}"
          montly_kwh: "{{states('sensor.outside_lights_monthly_kwh')}}"
          yearly_kwh: "{{states('sensor.outside_lights_yearly_kwh')}}"
          avg_kwh_day: "{{ (states('sensor.outside_lights_yearly_kwh')|float /(((as_timestamp(now()) - as_timestamp(state_attr('sensor.outside_lights_yearly_kwh', 'last_reset')))/86400)) )| round(2) }}"
          est_cost_per_year: "{{ (((states('sensor.outside_lights_yearly_kwh')|float /(((as_timestamp(now()) - as_timestamp(state_attr('sensor.outside_lights_yearly_kwh','last_reset')))/86400)))*365)*0.10)|round(2) }}"
          pct_of_total: "{{ ((((states('sensor.outside_lights_yearly_kwh')|float /(((as_timestamp(now()) - as_timestamp(state_attr('sensor.outside_lights_yearly_kwh', 'last_reset')))/86400)) )| round(2) *365) /(((states('sensor.total_yearly_kwh')|float /(((as_timestamp(now()) - as_timestamp(state_attr('sensor.total_yearly_kwh', 'last_reset')))/86400)) )| round(2)) * 365))*100) |round(2) }}"
          device_state: "{% if states('sensor.outside_lights_power')|float > 0.05 %} on 
{% else %} off {% endif %}"
          device_type: light
          icon_template: "{% if states('sensor.outside_lights_power')|float > 0.05 %} mdi:power-plug {% else %} mdi:power-plug-off {% endif %}"
  - platform: integration
    source: sensor.outside_lights_power
    name: outside_lights_kwh
    unit_prefix: k
    round: 6      

But it does give some nice details. I would prefer this to be in sense since the app is so well designed.


For sure - an MQTT data source is first on my todo list, because it’s pretty simple. That could be a stopgap for something like an aggregate plug, if you have a good way to publish the data in MQTT. I suppose HASS effectively does that automatically because you’ve set up that template already.

1 Like

I was actually thinking the opposite, MQTT as a data sink for the energy analysis your program does. It could publish back to home assistant via mqtt, sense (via tplink), or any of the other home automation platform via mqtt.

Ah gotcha, yeah that would be doable too. Most python MQTT client libraries support publishing too, so seemingly it’d be an easy step to publish the plug data back out.

I don’t think I’m quite ready to sign up for also tracking the day/month/year totals, but I suppose the real TPLink plugs do have that feature! However Sense does track that for us, so I’d prefer to lean on their implementation.

1 Like

HS110 emulation software would go along great with an esp8266/esp32 and an inexpensive current monitoring device such as a PZEM-004T. Even if it’s not possible to run it off an esp device, one could create an power monitoring plug with an esp device running esphome connected to a PZEM-004T, report to HA and have your software send the data to sense. Awesome. I’ll have to look into your code tomorrow. I have a PZEM-004T and just received more esp8266 NodeMCUs to play with.

2 Likes

Yeah definitely! I have some ESP8266s units myself, so I’m interested in what you find. I should have a SenseLink update posted tomorrow that lets you use a HA attribute/state directly (thanks to a recommendation on reddit). The intent being almost exactly for this, using data from a power monitoring device.

1 Like

@KHouse75 Well I couldn’t leave well enough alone - I ported (kind of) SenseLink to ESPHome:

It’s a little more limited right now because you can only report a single sensor, but I plan to improve on that.

I didn’t actually have any ESP-based energy monitoring devices, but I just received some cheap energy monitoring plugs (Efun SH331W) so I’ll try it out on those in the new couple days. So far I was just running it on ESP8266 NodeMCUs as well.

2 Likes

Awesome work! I was so busy with work over the last several days that I have not had a chance to take a look at this until now. Now I need to find where I put my PZEM-004T so I can give ESPSense a try tomorrow.

1 Like

@cbpowell

I had a few minutes of free time and I’m trying to compile your code but I’m running into the following errors. Which library versions are you using for ESPAsyncUDP and ArduinoJson?

In file included from src/main.cpp:19:0:
src/espsense.h: In member function 'void ESPSense::parse_packet(AsyncUDPPacket&)':
src/espsense.h:62:5: error: 'json' has not been declared
     json::parse_json(result,[&](JsonObject &req) {
     ^
src/espsense.h: In lambda function:
src/espsense.h:64:19: error: 'ArduinoJson::JsonVariant' has no member named 'success'
       if (request.success()) {
                   ^
*** [/data/esp_sense_01/.pioenvs/esp_sense_01/src/main.cpp.o] Error 1

Hmm good question - I’ll check a little later today. As far as I know I just have the “latest” ESPHome version installed via pip, and nothing else special about the build environment.

However, I might have included ArduinoJson-esphome at some point and it’s just left over in the build folder!

As a quick thought, maybe try adding it as another library to include under ESPAsyncUDP in the YAML? Just with “- ArduinoJson-esphome” (edit2: this should be " - ArduinoJson-esphomelib")

Edit: also I’m pretty sure it’s v5 of the ArduinoJson-esphome library, which isn’t the latest. If I’ve somehow got that lingering around, but v6 is what ESPHome pulls now and it’s not compatible, definitely need to update that!

I’ll give it a try that way. I have it integrated with Home Assistant so I was compiling it there.