Skip to content

Add support for the protocol used by TAPO devices and some newer KASA devices. #552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 30, 2023

Conversation

sdb9696
Copy link
Collaborator

@sdb9696 sdb9696 commented Nov 23, 2023

Related to #499 and #548

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really cool, great work @sdb9696! Just wanted to give some quick feedback.

  • We should find a nice level of abstraction for tapo devices in general, I ordered a test bulb to help with testing and dev but I'm not going to have much time to tinker until holidays.
  • I am wondering how much of this is similar to the klap protocol, i.e., should it make sense to merge them or at least find a way to share some code?
  • I presume some of the code is sourced from other projects, are there license incompatibilities?

@rytilahti rytilahti added the enhancement New feature or request label Nov 27, 2023
@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 27, 2023

This is really cool, great work @sdb9696! Just wanted to give some quick feedback.

  • We should find a nice level of abstraction for tapo devices in general, I ordered a test bulb to help with testing and dev but I'm not going to have much time to tinker until holidays.

Yes I was thinking the same. However I wanted to keep this PR as close to your initial #499 as possible so we didn't start off with a giant PR.

  • I am wondering how much of this is similar to the klap protocol, i.e., should it make sense to merge them or at least find a way to share some code?

There is some overlap, particularly around the retry logic which could be any httpx connection. Again as above I wanted to keep this PR as simple as possible. My suggestion would be to get this working for the Kasa EP25 and a TAPO plug, and then do some refactoring and code re-use in a separate PR. I'm happy to do it the other way round if you want but I thought it would be easier for you to keep things ticking along if you have less to review each time.

  • I presume some of the code is sourced from other projects, are there license incompatibilities?

Some of the code, i.e. the Cipher, KeyPair and the SnowflakeId came from https://github.com/petretiandrea/plugp100 which seems to have the same GNU GPL 3 license as this library so I don't think there are any incompatibilities. Let me know if you agree.

Anyway, let me know how you want to proceed, i.e. with incremental PRs or incremental commits to this PR.

@rytilahti
Copy link
Member

rytilahti commented Nov 27, 2023

  • We should find a nice level of abstraction for tapo devices in general, I ordered a test bulb to help with testing and dev but I'm not going to have much time to tinker until holidays.

Yes I was thinking the same. However I wanted to keep this PR as close to your initial #499 as possible so we didn't start off with a giant PR.

So I don't have a clear opinion on this. What I was thinking was that it would be great to have a base class straight from the beginning, as that will likely happen in the near future anyway and we know that there exists common functionalities among the devices.

Another thing that caught my mind was that maybe it's also a good idea to contain the tapo functionality inside its own package (e.g., inside python-kasa/kasa/tapo) from the get-go to avoid moving them around soon-ish. There will likely be much more files coming in when other device categories get added, so having them separated from kasa-specific code makes sense. The base class does not need to have much, just that it exists there as a place to easily start adding common functionalities when we get that far.

There is some overlap, particularly around the retry logic which could be any httpx connection. Again as above I wanted to keep this PR as simple as possible. My suggestion would be to get this working for the Kasa EP25 and a TAPO plug, and then do some refactoring and code re-use in a separate PR. I'm happy to do it the other way round if you want but I thought it would be easier for you to keep things ticking along if you have less to review each time.

Sounds reasonable to me, maybe some parts of the common functionalities can be refactored to the protocol base class to avoid inconsistencies among protocol handling, but that can wait for now.

Some of the code, i.e. the Cipher, KeyPair and the SnowflakeId came from petretiandrea/plugp100 which seems to have the same GNU GPL 3 license as this library so I don't think there are any incompatibilities. Let me know if you agree.

Yupp, that's fine. Please add a docstring note with a link to the plugp100 repository and mention the compatible, I think that should be good enough :-)

@rytilahti
Copy link
Member

On another note, maybe the protocol itself should just be renamed and remain in the base package? Given it's not just used for tapo devices, but as an transport & encryption layer for the newer kasa-branded, too.


def _md5(payload: bytes) -> bytes:
digest = hashes.Hash(hashes.MD5()) # noqa: S303
digest.update(payload)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data

[Sensitive data (password)](1) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. [Sensitive data (id)](2) is used in a hashing algorithm (MD5) that is insecure. [Sensitive data (id)](3) is used in a hashing algorithm (MD5) that is insecure.

def _sha1(payload: bytes) -> str:
sha1_algo = hashlib.sha1() # noqa: S324
sha1_algo.update(payload)

Check failure

Code scanning / CodeQL

Use of a broken or weak cryptographic hashing algorithm on sensitive data

[Sensitive data (password)](1) is used in a hashing algorithm (SHA1) that is insecure for password hashing, since it is not a computationally expensive hash function. [Sensitive data (id)](2) is used in a hashing algorithm (SHA1) that is insecure. [Sensitive data (id)](3) is used in a hashing algorithm (SHA1) that is insecure.
@staticmethod
def create_key_pair(key_size: int = 1024):
"""Create a key pair."""
private_key = rsa.generate_private_key(public_exponent=65537, key_size=key_size)

Check failure

Code scanning / CodeQL

Use of weak cryptographic key

Creation of an RSA key uses [1024](1) bits, which is below 2048 and considered breakable.
Copy link

codecov bot commented Nov 28, 2023

Codecov Report

Attention: 257 lines in your changes are missing coverage. Please review.

Comparison is base (9de3f69) 82.29% compared to head (94981d1) 75.84%.

Files Patch % Lines
kasa/aesprotocol.py 25.00% 201 Missing ⚠️
kasa/tapo/tapodevice.py 54.32% 37 Missing ⚠️
kasa/tapo/tapoplug.py 56.75% 16 Missing ⚠️
kasa/discover.py 85.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #552      +/-   ##
==========================================
- Coverage   82.29%   75.84%   -6.45%     
==========================================
  Files          30       33       +3     
  Lines        2468     2861     +393     
  Branches      694      747      +53     
==========================================
+ Hits         2031     2170     +139     
- Misses        379      634     +255     
+ Partials       58       57       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 28, 2023

So I don't have a clear opinion on this. What I was thinking was that it would be great to have a base class straight from the beginning, as that will likely happen in the near future anyway and we know that there exists common functionalities among the devices.

Ok that is done. Base class of TapoDevice(SmartDevice) and TapoPlug derives from TapoDevice (not SmartPlug)

Another thing that caught my mind was that maybe it's also a good idea to contain the tapo functionality inside its own package (e.g., inside python-kasa/kasa/tapo) from the get-go to avoid moving them around soon-ish. There will likely be much more files coming in when other device categories get added, so having them separated from kasa-specific code makes sense.

That's done

Yupp, that's fine. Please add a docstring note with a link to the plugp100 repository and mention the compatible, I think that should be good enough :-)

That's done

@ezekieldas could you please check this still works with your EP25?

@sdb9696 sdb9696 force-pushed the add_tapo_support branch 2 times, most recently from f5e320d to d5200a7 Compare November 28, 2023 13:47
@ezekieldas
Copy link

This looks fantastic: https://pastebin.com/xRAvpT9u

Perhaps non-issue but there is that datetime.timedelta ValueError at the end.

@korishev
Copy link

I have several of these new EP25 plugs and can verify that this at least appears to be working:

  1. Hardware Version: 2.6
  2. Firmware Version: 1.0.1 Build 230614 - Rel. 150219

I am getting a similar ValueError to @ezekieldas at the end of output.

Happy to help test, debug, etc. (and send one or two devices to devs for testing. If they don't work with Home Assistant, they are useless to me)

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 28, 2023

The ValueError should be fixed if you take the latest version of this PR updated just now

@sdb9696 sdb9696 marked this pull request as ready for review November 28, 2023 15:55
@ezekieldas
Copy link

Yes, so I did another checkout of pull552 and this did correct the ValueError:

        [ ... ]
	== Device specific information ==
	is_hw_v2: False
	overheated: False
	signal_level: 3
	auto_off_status: off
	auto_off_remain_time: 0
	SSID: foobarXYZ
	On since: 2023-11-18 05:04:27

	== Modules ==

@bdraco
Copy link
Member

bdraco commented Nov 28, 2023

I'm going to order a device to have something to test with. Any idea if the P125M will work even though it supports matter? I can't see to find any of them that don't support matter for sale and will ship to Hawaii

@ezekieldas
Copy link

ezekieldas commented Nov 28, 2023

Any idea if the P125M will work even though it supports matter?

I haven't used or researched these, nor am I familiar with matter. However kp125m is listed on the integrations page. See the comment: supported via Matter, but without energy monitoring features

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 28, 2023

I’m not sure I’m afraid. From looking at decompiled Kasa & TAPO .apk I’d say they support both but it’s difficult to be sure. I guess the worst case is we end up with a Matter protocol ;)

@bdraco
Copy link
Member

bdraco commented Nov 28, 2023

I ordered the EP25 and the kp125m

I figure one of them will be useful for testing

@rytilahti
Copy link
Member

rytilahti commented Nov 28, 2023

To my understanding, based on some issue comments), the matter-based devices are using the "tapo" protocol (unsure if it's just the transport or also for the command set) as a secondary communications protocol. The emeter features do not work over the matter at the moment, as I think only the recently announced upcoming 1.2 (or 1.3?) has support for such.

For EP25, it seems that the new hw version (v4?) is the one that has moved to use the AES encryption, and I'm quite confident others will follow as it just makes sense :-)

edit: KP125M seems to use KLAP & tapo commands as per #450 (comment)

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I received my test bulb today and my pending deadline was postponed a bit, so I had some time to play around with it :-)

Here are some comments, I'll create a draft PR based on this PR that adds support for tapo bulbs. Some notes based on my experiments:

  • Adding support for the bulbs felt really easy, I think the effort put into building a good foundation and abstractions were useful! :-)
  • get_<x>_info and set_<x>_info come in pairs, so when get_device_info report on brightness, it can be set using set_device_info much like device_on.
  • hue, saturation and brightness are stored in separate fields, so I suppose multi assignments are possible. I haven't yet tested this out, but will do so prior creating the PR.
  • Some communications feel rather sluggish, due to multiple queries (device state, then the emeter & the usage). Maybe these could be condensed into a single request? Just a thought..

@rytilahti
Copy link
Member

sdb9696#14 adds support for bulbs. Maybe it would make more sense to base it on top of python-kasa master instead of the branch for the PR at hand to keep the merges small, but github ui's diff view makes it easier to see how few changes were required for basic functionalities :-)

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 29, 2023

Unbelievably, overnight my Tapo P110 updated it's firmware to the KLAP encryption. It's now running sw_version 1.3.0 Build 230905 Rel.152200.

{'device_id': '**', 'owner': '**', 'device_type': 'SMART.TAPOPLUG', 'device_model': 'P110(UK)', 'ip': '**', 'mac': '**', 'is_support_iot_cloud': True, 'obd_src': 'tplink', 'factory_default': False, 'mgt_encrypt_schm': {'is_support_https': False, 'encrypt_type': 'KLAP', 'http_port': 80, 'lv': 2}}}

As a result I set to looking at what is needed to support this configuration. I've created a draft PR to support TAPO/KLAP, but I don't know if I've broken the AES transport in the process. I no longer have a device with this configuration :(.

@ezekieldas would you mind testing your EP25 with this new PR to see if it still works with your device.

@rytilahti there are more comments in the other PR regarding the approach. I've also addressed all your previous review comments in this PR.

@ezekieldas
Copy link

Here are the results from #557: https://pastebin.com/XPpDRS3e It ends with an AuthenticationException.

Just to be certain, I compared same args/creds used in this test with the previous test on #552, and that remains successful.

Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind adding some tests to make it easier to review the code?
Also, should this get merged before it makes sense to review #557?

Comment on lines +31 to +33
self._energy = await self.protocol.query("get_energy_usage")
self._emeter = await self.protocol.query("get_current_power")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bulb seems to support these commands, too, but I'm not sure if we can detect the availability using component_nego or some other way.

Here's the response for the from the bulb (kasa -d --type tapobulb --username $KASA_TAPO_EMAIL --password $KASA_TAPO_PASSWORD --host 192.168.xx.xx raw-command "" component_nego):

<< {'component_list': [{'id': 'device', 'ver_code': 2},
                    {'id': 'firmware', 'ver_code': 2},
                    {'id': 'quick_setup', 'ver_code': 3},
                    {'id': 'inherit', 'ver_code': 1},
                    {'id': 'time', 'ver_code': 1},
                    {'id': 'wireless', 'ver_code': 1},
                    {'id': 'schedule', 'ver_code': 2},
                    {'id': 'countdown', 'ver_code': 2},
                    {'id': 'antitheft', 'ver_code': 1},
                    {'id': 'account', 'ver_code': 1},
                    {'id': 'synchronize', 'ver_code': 1},
                    {'id': 'sunrise_sunset', 'ver_code': 1},
                    {'id': 'cloud_connect', 'ver_code': 1},
                    {'id': 'default_states', 'ver_code': 1},
                    {'id': 'preset', 'ver_code': 1},
                    {'id': 'brightness', 'ver_code': 1},
                    {'id': 'color', 'ver_code': 1},
                    {'id': 'color_temperature', 'ver_code': 1},
                    {'id': 'auto_light', 'ver_code': 1},
                    {'id': 'on_off_gradually', 'ver_code': 1},
                    {'id': 'device_local_time', 'ver_code': 1},
                    {'id': 'light_effect', 'ver_code': 1},
                    {'id': 'iot_cloud', 'ver_code': 1},
                    {'id': 'bulb_quick_control', 'ver_code': 1}]}
{"component_list": }

I updated the list to the draft bulb PR, so it's easily available.

@rytilahti
Copy link
Member

Please add a debug output for the query, that'll make it easier to debug issues when a query is failing :-)

diff --git a/kasa/aesprotocol.py b/kasa/aesprotocol.py
index f74ae54..f357de4 100644
--- a/kasa/aesprotocol.py
+++ b/kasa/aesprotocol.py
@@ -304,6 +304,12 @@ class TPLinkAes(TPLinkProtocol):
         raise SmartDeviceException("Query reached somehow to unreachable")
 
     async def _execute_query(self, request: Union[str, Dict], retry_count: int) -> Dict:
+        _LOGGER.debug(
+            "%s >> %s",
+            self.host,
+            _LOGGER.isEnabledFor(logging.DEBUG) and pf(request),
+        )
+
         if not self.http_client:
             self.http_client = httpx.AsyncClient()

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 29, 2023

Here are the results from #557: https://pastebin.com/XPpDRS3e It ends with an AuthenticationException.

Just to be certain, I compared same args/creds used in this test with the previous test on #552, and that remains successful.

Hi @ezekieldas. I think I've fixed it, could you try again?

@ezekieldas
Copy link

It definitely looks fixed! https://pastebin.com/66Z1XGR5

I do a quick scan (human, me) of the output each time, and here I see the val for timezone looking a bit funky (see line 156):
Time: 2023-11-29 07:36:36-08:00 (tz: {'timezone': datetime.timezone(datetime.timedelta(days=-1, seconds=57600))}

...but other than that, it looks good!

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 29, 2023

  • Some communications feel rather sluggish, due to multiple queries (device state, then the emeter & the usage). Maybe these could be condensed into a single request? Just a thought..

I've just figured out how to do multiple requests in one go. I'll add support for this in a later PR if that's ok?

@rytilahti
Copy link
Member

I've just figured out how to do multiple requests in one go. I'll add support for this in a later PR if that's ok?

That's fine, let's get preliminary support in and we can then iterate and optimize 👍

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 29, 2023

It definitely looks fixed! https://pastebin.com/66Z1XGR5

I do a quick scan (human, me) of the output each time, and here I see the val for timezone looking a bit funky (see line 156): Time: 2023-11-29 07:36:36-08:00 (tz: {'timezone': datetime.timezone(datetime.timedelta(days=-1, seconds=57600))}

...but other than that, it looks good!

Thank you very much!

@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 29, 2023

Would you mind adding some tests to make it easier to review the code? Also, should this get merged before it makes sense to review #557?

The trouble is I can no longer test this PR with a device myself because my P110 upgraded to KLAP overnight. Also the changes in #557 are going to make it much easier to write the FakeTransport for testing the TAPO protocol and test using the "new" fixtures with TAPO responses. I don't suppose we could hold off on the tests until #557? It will save me a lot of time and tests is my number 1 priority at the moment (as you may have guessed from #556)

@rytilahti
Copy link
Member

Sounds reasonable! I'll go through this one later tonight and see if there is something that should be handled before merging this. But I think in the end we will just merge this as is and iterate on later PRs 👍

@rytilahti
Copy link
Member

rytilahti commented Nov 29, 2023

@sdb9696 could you update the title and the description to be more descriptive? Doesn't need to be anything fancy, but given the size and the importance of this change, it would be great to have descriptive ones for anyone reading the changelogs/commit logs. I will now check if there's something that should be changed prior the merge.

@sdb9696 sdb9696 changed the title Add Tapo protocol Add support for the protocol used by TAPO devices and some newer KASA devices. Nov 30, 2023
@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 30, 2023

Now updated with changes or replies. N.B. I have squashed all the commits except the latest to make rebasing #557 on top of this PR easier.

@sdb9696 sdb9696 requested a review from rytilahti November 30, 2023 11:06
Copy link
Member

@rytilahti rytilahti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to go, thanks @sdb9696! 💯

@rytilahti rytilahti merged commit 63d64ad into python-kasa:master Nov 30, 2023
@sdb9696 sdb9696 mentioned this pull request Nov 30, 2023
@sdb9696
Copy link
Collaborator Author

sdb9696 commented Nov 30, 2023

Great, thanks @rytilahti! Onto #557 :)

@sdb9696 sdb9696 deleted the add_tapo_support branch December 6, 2023 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants