SenseLink - TP-Link Smart Plug Emulation

Anyone know if the device limit was removed for “SenseLink” devices? I noticed last time i changed a device in senselink that sense said “we noticed a change with your Senselink devices.” And updated accordingly. I have a few aggregate devices like lights i would like to separate if so.

That’d be news to me! SenseLink does identify itself in the UDP responses, so that Sense could in theory differentiate from real TP-Link plugs if they wanted for data quality (unsure offhand what Emulated Kasa does).

What did you change on your end that preceded the message?

I added a new device and restarted the container. I was fairly certain it wasn’t going to be detected since I’ve already been above the limit for some time, but was pleasantly surprised when it was picked up AND sense knew it was a senselink device.

1 Like

So while it’d be awesome to get a project shoutout in the app, we determined it’s probably because SenseLink reports its own name in the model field and Sense is presumably grabbing that string to populate the popup and timeline messages. Normally you’d see “HS110s” or “KP115s” there for a real TP-Link plug.

Still neat to see though!

3 Likes

Thanks for this project, @cbpowell! I was able to use it as a basis for my smart-ish home to teach Sense about more devices. Basically every light in the house is a dimmer (and hence not detectable by Sense) connected to a home automation system (an HAI OmniPro II from 2008, so it’s a bit dated). I’m already running some software on my Mac that lets me bridge between that and HomeKit. There’s no API into that app, but it can log device state into a file. So I hacked my way through SenseLink to get it to read the current scene for a device from the appropriate log file, and then read a corresponding wattage for that scene+device from the config.yml.

I also had to work around another issue; I don’t think Docker on the Mac supports network_mode: host, so I had to connect it to port 9999, and also hard-coded SenseLink to respond to the static IP I assigned for the Sense, because responding to the broadcast inside the NAT created by docker-compose just ends up sending the response to the NAT gateway, it doesn’t make it back out to Sense. But if anybody knows more than I do about setting up Docker, I’m still interested in finding ways for SenseLink to ‘correctly’ respond to the broadcast packet.

Now, if anybody needs me, I’ll be wandering around my house, turning different lights on and off repeatedly to measure how much power they take…

3 Likes

FWIW, I did this in the Power Meter at my parent’s house. Their biggest offenders were the cluster hanging lights and recessed floodlights, which were both swapped out for LED’s.

2 Likes

I was commenting on the TesSense thread (my app to charge your Tesla in response to free energy detected by Sense) that I wanted to have that energy amount show in Sense and this app was recommended as a starting point.

Unfortunately I have been unable to get it working on my Mac, I am not even sure what I am supposed to do to get it running. Did anyone ever do a little step by step to see this working?

Hey @israndy I can probably help out - first off just to double check, are you using SenseLink or the emulated_kasa derivative?

Assuming you’re using SenseLink, are you running it directly via Python or as a Docker container?

As @kynan notes above, Docker for Mac doesn’t support the host network mode. The resulting effect is that while SenseLink will get incoming broadcast requests, the associated requesting address IP is wrong due to the Docker network translation and SenseLink will send the response to the wrong place*. So for testing on your Mac I would recommend running it directly via Python (as described here, which is basically what I do when development testing). For a long-running instance, Docker on something like an always-on Linux host (i.e. RPi) is the best way.

But regardless of the method you use to run it, you still need three things:

  1. The device running SenseLink is on the same network subnet as your Sense, so that it can receive the UDP broadcast from your Sense. (Alternatively, you could maybe use your router to reflect the requests to another subnet, but that’s a whole other topic).
  2. The device running SenseLink needs to have port 9999 open to receive the incoming UDP broadcast.
  3. SenseLink needs to be configured per the docs to define “plugs” and their data sources. At the moment the data source can be a HomeAssistant entity, an MQTT entity, or just a static value.

I do have a Tesla but I don’t have a Tesla HPWC, so for #3 I’m not familiar with the best way to “publish” the HPWC power consumption data in a way that SenseLink can repeat it over to Sense. Some quick googling shows it’s probably possible to get it into HomeAssistant, which could be the lowest-friction approach. Then you can source data from there by specifying it in your SenseLink config. That’s what I did with my Juicebox 40 charger, although I have my HomeAssistant spitting out values to MQTT and SenseLink pulls from MQTT.

Hope this helps - let me know if you have more questions! Feel free to PM as well.

3 Likes

I was hoping that I could just sit down and launch this and see an effect in Sense, then I could figure out what what happening based on the way I ran it and I would customize it until I understood what it was doing and eventually I would clone the parts of the code I need so my app, which already is talking to my Tesla, could send the info it has to Sense.

There are directions as you stated, but they say: SenseLink can be started directly via the command line: python3 ./SenseLink.py -c “/path/to/your/config.yml”

That’s great, but I am expected it would come with a demo, like a DemoConfig.yml, since it didn’t I just used the config.yml that it came with and I spews errors at me:

Traceback (most recent call last):
File “/Users/israndy/Documents/Xcode/Python3/SenseLink-master/senselink.py”, line 263, in
main()
File “/Users/israndy/Documents/Xcode/Python3/SenseLink-master/senselink.py”, line 259, in main
loop.run_until_complete(tasks)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py”, line 642, in run_until_complete
return future.result()
File “/Users/israndy/Documents/Xcode/Python3/SenseLink-master/senselink.py”, line 168, in start
self.create_instances()
File “/Users/israndy/Documents/Xcode/Python3/SenseLink-master/senselink.py”, line 49, in create_instances
config = yaml.load(self.config, Loader=yaml.FullLoader)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/init.py”, line 81, in load
return loader.get_single_data()
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/constructor.py”, line 49, in get_single_data
node = self.get_single_node()
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 36, in get_single_node
document = self.compose_document()
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 55, in compose_document
node = self.compose_node(None, None)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 84, in compose_node
node = self.compose_mapping_node(anchor)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 133, in compose_mapping_node
item_value = self.compose_node(node, item_key)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 82, in compose_node
node = self.compose_sequence_node(anchor)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 111, in compose_sequence_node
node.value.append(self.compose_node(node, index))
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 84, in compose_node
node = self.compose_mapping_node(anchor)
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/composer.py”, line 127, in compose_mapping_node
while not self.check_event(MappingEndEvent):
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/parser.py”, line 98, in check_event
self.current_event = self.state()
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/yaml/parser.py”, line 438, in parse_block_mapping_key
raise ParserError(“while parsing a block mapping”, self.marks[-1],
yaml.parser.ParserError: while parsing a block mapping
in “config.yml”, line 52, column 3
expected , but found ‘-’
in “config.yml”, line 87, column 3

Well by nature of emulating a TP-Link plug you do inherently need to define what data the “plug” should send to Sense, so I didn’t approach it as a one-size-fits-all setup since people will have different use cases.

The config_example.yml file provides examples of how to write a configuration, but not a working configuration in itself (there are a number of configuration parameters set to things like “your.HASS.API.URL.here”, the traceback you got is a result of those). I see how that could be unclear though, I’ll look to add some notes to the files and readme about that.

A stock demo config would add a bogus plug to people’s device list that can’t (currently) be removed without resetting their TP-Link integration. That said, you could reuse the MAC of a demo plug on an eventual real usage to transition it over from leftover cruft.

As for a working sample config, here’s one that defines a basic static plug - I’ll add this to the repo as well with some warnings about reusing the MAC later: democonfig.yml · GitHub

You can run SenseLink in your own code as shown in the example_usage.py file as well, which might be of interest for your use case.

Excellent, I’ll give it a shot. Great to hear that I can call your app w/o figuring it out line by line. I assume that I am going to be able to create a dynamic virtual TP-Link not just a static one, Like the info Sense is getting from the TP-Link my Plasma TV is plugged into.

Is the MAC address you sent one that I can use? Should I try to find a unique one?

Sounds good! Feel free to use that MAC or pick a random one - really it doesn’t matter what MAC is used, as long as it’s unique within your own network (and really just that it’s unique within all your local TP-Link plugs, real or emulated).

And correct, for your use case you’ll want to configure a dynamic plug with data being sourced from either HomeAssistant or MQTT. The SenseLink code is set up such that adding other data sources types should be fairly easy as well, I just haven’t yet had the need/time myself.

Got it working. It is weird that you don’t put anything on the StdOut, but OK, I saw the device show up in my Sense and I hit Ctrl-C and the device stopped showing usage. I changed the .yml file and ran it again and the new value showed up, and I quit again.

I’ll start fishing thru the docs and see if I can figure out what you have done as I just want to pass updated values as often as once a minute from my app that controls the Tesla.

1 Like

Nice!

You can set the log level to adjust what gets spit out (a benefit of using logging over just printing!).

So as your looking to adapt it, keep in mind the standard mode of operation for TP-Link integration is the monitor sends out a request (broadcast) about every 2 seconds if I recall correctly, and the plugs respond with their power data. The real plugs presumably poll their sensors in near real time and respond, but SenseLink decouples the data update rate from the request rate. The DataSource class in SenseLink manages that by acting an intermediate to cache the power value from whatever source, and provide that when they get the request.

From my testing, if a plug doesn’t provide responses for a certain interval Sense just assumes it’s off (network connectivity can induce this too, as I think @kevin1 is tracking and researching). So you will need to reply to the Sense requests at least most of the time, rather than just sending a data point to Sense when your app gets a value update.

It has occurred to me before that it might possible to just hit Sense with “unprompted” responses at a reasonable interval, but I haven’t taken the time to dig into that.

1 Like

Yeah, I am mired in your code, I see the build-response code, but it’s gonna be some time before I can figure out all the cruft I can remove and still have that piece work. Would be great if I could just blast the current current and voltage every time my code loops (or two seconds if there isn’t much going on). I will certainly try that first.

1 Like

@kynan I’ve added a feature that mimics your approach of hard-coding the Sense IP address - great idea to get around the Docker host networking issue!

The v1.4.0 release also adds:

  • A plug type better suited for use when running SenseLink as part of other Python code
  • The ability to set a skip_rate value for plugs, which causes SenseLink to skip/defer incoming requests from your Sense monitor.

The idea of skip_rate is to reduce the response processing load on your monitor for plugs with mostly-steady power values, and thereby perhaps allow a higher plug count (I absolutely haven’t verified that though, and if it’s a bad assumption definitely let me know!)

1 Like

@israndy I took a stab at merging TesSense in with SenseLink such that the TesSense loop directly updates the power value of a new mutable type plug.

See this gist: TesSenseLink.py · GitHub

I had to wrap TesSense in an async call such that it works in concert with SenseLink (which is heavily async-based). A couple things to note:

  1. I put in a few lines where TesSense updates the plug value, but there might be more appropriate places. Essentially use mutable_plug.power = some_charge_power with a real/calculated power value (in watts) anywhere you’d want to set a new value. That value can be set independently at any time.
  2. I had to comment out a LOT of the functional parts for testing since I don’t have solar and didn’t want to muck around with my charging too much, but as far as I can tell everything should still work!
  3. See this comment on the Gist for a little more info as well.

Overall, I do think it would be cleaner to run TesSense and SenseLink independently, and just have SenseLink only responsible for getting the charger output power (whatever it may be, as commanded by TesSense or otherwise). But that said, this is the closest implementation to a stand-alone Tesla-to-SenseLink setup!

SCHWEET!

I had NOT made a ton of headway with reading your code. Been learning a lot about AsyncIO but haven’t gotten my hands dirty yet. Ended up writing an app to help me understand how SenseAPI works and got stymied when it came to the Async stuff he added to it recently.

I’ll work on integrating what you did with my current model of the code and see if I get it working. Glad I tickled your interest.

2 Likes

OK, so I have been trying to get it to work all day. It obviously isn’t a stand alone app, the way it was. I looked at all the the files on your GitHub and copy and pasted the code out of each into the files on my SenseLink download folder. I tried to run it and got all sorts of complaints. Mostly related to the way modules were called. Had to remove the period in front of each and change the case, but I finally got it to run for a second, but it chokes on the line

File "/Users/israndy/Documents/SenseLink/tessenselink.py", line 145, in run_tes_sense

mutable_plug.power = charge_power

AttributeError: can't set attribute

I don’t have any ability to debug this and this was pretty much the crux of what you were working on, hopefully its something easy

EDIT: OK, thanks for getting back to me on GitHub. That was the trick, changing it to mutable_plug.data_source.power. I played around and found that I can ALSO change the mutable_plug.data_source.voltage setting and it will calculate the Amps, not sure why they are a calculation instead of letting me set it directly. Glad the HS110 protocol lets me send 249 volts to Sense and it doesn’t blow up.

I have made several updates to my copy of TesSense in the meantime so it took a few minutes to get it working on my copy, but I am now running a working copy of TesSense that is linked to Sense. Now I need to go BACK thru my code and make it work with the normal SenseAPI, now that he has added your SenseLink code to his distro. More learning how to program in Python.

Glad you got it working!

Regarding calculation of amps, currently the only thing Sense cares about in the response is the power/watts value. As such SenseLink takes the approach of fully trusting the watt value that’s provided and then calculating amps from the default or specified voltage. Granted in your case you’re dealing more closely with amps that are being set to the charger, so there could be improvements there where you could provide power or amps+volts.

I can confirm that it works fine in concert with the sense_api package, and you don’t have to use/worry about the SenseLink version wrapped up in there. If you don’t specifically utilize it, you won’t see any conflicts. I forked your repo and incorporated the Gist to make what appears to be a fully working version as far as I can test (without solar). I’ll send over a pull request so you can check it out (no obligations to merge it though!).