piekstra / tplink-cloud-api

A Python library to remotely control TP-Link smart home devices using their cloud service - no need to be on the same network as your devices
GNU General Public License v3.0
41 stars 11 forks source link

Unable to get the current code working #63

Closed israndy closed 2 years ago

israndy commented 2 years ago

I have tried all day to get any part of this API working, perhaps you are in the progress of making a change, but nothing I have done got results. I am sure I am an idiot, but would love to know what I am doing wrong.

I started by downloading the repo from here, I then tried to get the Example.py to run. I was able to get as far as the creation of the device_manager. With Verbose set to True I do see that it has connected to my account and sees all my TPLink devices. Any other command I issue after that fails.

I then tried to change everything out and used the pip3 install command to copy your module to my Mac running MacOS 12.3 and Python 3.10.3. I then copied the code from your ReadMe under "For more advanced usage, you can gather tasks so they all run at once such as in the following example which can fetch a large number of devices' system info very quickly:", above the section "Retrieve devices" into a new python document in a new folder.

Adding my Username and Password I get the following error messages when I run the script, they look to be the same complaint I got all day, don't know what it means:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 986, in _wrap_create_connection
    return await self._loop.create_connection(*args, **kwargs)  # type: ignore[return-value]  # noqa
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/base_events.py", line 1089, in create_connection
    transport, protocol = await self._create_connection_transport(
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/base_events.py", line 1119, in _create_connection_transport
    await waiter
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/sslproto.py", line 534, in data_received
    ssldata, appdata = self._sslpipe.feed_ssldata(data)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/sslproto.py", line 188, in feed_ssldata
    self._sslobj.do_handshake()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/ssl.py", line 974, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/israndy/Documents/Xcode/Python3/TesSense/tplink.py", line 24, in <module>
    asyncio.run(fetch_all_devices_sys_info())
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/asyncio/base_events.py", line 646, in run_until_complete
    return future.result()
  File "/Users/israndy/Documents/Xcode/Python3/TesSense/tplink.py", line 11, in fetch_all_devices_sys_info
    devices = await device_manager.get_devices()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/device_manager.py", line 67, in get_devices
    devices_children = await asyncio.gather(*children_gather_tasks)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/hs300.py", line 40, in get_children_async
    sys_info = await self.get_sys_info()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/hs300.py", line 54, in get_sys_info
    sys_info = await self._get_sys_info()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/device.py", line 89, in _get_sys_info
    return await self._pass_through_request('system', 'get_sysinfo', None)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/device.py", line 62, in _pass_through_request
    response = await self._client.pass_through_request(
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/device_client.py", line 63, in pass_through_request
    response = await self._request_post(body)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/tplinkcloud/device_client.py", line 37, in _request_post
    async with session.post(
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/client.py", line 1138, in __aenter__
    self._resp = await self._coro
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/client.py", line 535, in _request
    conn = await self._connector.connect(
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 542, in connect
    proto = await self._create_connection(req, traces, timeout)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 907, in _create_connection
    _, proto = await self._create_direct_connection(req, traces, timeout)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 1206, in _create_direct_connection
    raise last_exc
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 1175, in _create_direct_connection
    transp, proto = await self._wrap_create_connection(
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/aiohttp/connector.py", line 988, in _wrap_create_connection
    raise ClientConnectorCertificateError(req.connection_key, exc) from exc
aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host use1-wap.tplinkcloud.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)')]
piekstra commented 2 years ago

Hi @israndy ,

Thanks for giving my library a try. I believe this issue is specific to Python 3.10 on MacOS. It should work fine using version 3.9

I will investigate this tomorrow if I have time to look for any changes I can make to support 3.10 out of the box.

piekstra commented 2 years ago

This seems to be related

https://github.com/aio-libs/aiohttp/issues/6327

israndy commented 2 years ago

I checked and there was a .4 update to python, I installed it and noticed in the python folder a command file to install certificates. I did it and ran the script again, and this time it ran cleanly.

The Example.py file appears to show that much of the API is not finished being cut over to asyncio, but at least the top of it runs cleanly.

israndy commented 2 years ago

Of course it worked until I installed MacOS 12.3.1 this morning. I even reinstalled Python and re-ran the certificate command and it's still not working... Odd.

Deleted my python 3.10 and reinstalled python 3.9, like you suggested, thx

israndy commented 2 years ago

OK, under Python 3.9 I am able to get every call in your library working with my Kasa collection. Next trick is making an app with this capability. I started working on it, but whenever I try to collect the realtime and day and month Emeter readings I get diagnostic text from your library (emeter_device.py):

if realtime_data is not None and realtime_data.get('err_code') == 0:
     print(f"Found: {realtime_data}")
     return CurrentPower(realtime_data)

Is this for a reason? I cannot figure a good way around it w/o making my own copy of your library, but I wanna support your lib and bring you more users.

Should I post this as a separate issue?

piekstra commented 2 years ago

@israndy funny you found that! I had opened a PR to address it but apparently never merged it. I'll make that update now.

piekstra commented 2 years ago

Version 4.1.2 of the library is now published and removes the print statement - it was vestigial code from debugging.

piekstra commented 2 years ago

As for making an app, FWIW, I had started up some work on a React app to expose certain functionality:

https://github.com/piekstra/tplink-kasa-ui

It made use of a simple Python backend API that simply leveraged this library:

https://github.com/piekstra/tplinkcloud-service

The code on both sides hasn't been touched in awhile, so probably isn't in a runnable state. I need to update the service side at some point and then start adding more features to the frontend.

Part of my vision included being able to see your list of devices in the UI, manage their power state, then click into them to edit schedules and view historical and live power data. Haven't had capacity to work on the project for awhile though, sadly.

israndy commented 2 years ago

Thanks for the quick response, downloading the update right now. I'll check out what you have as far as an API goes, but I am really just trying to tie my solar generation to when it's OK for certain devices to run. I should have enough with just the TPLink API you made.

israndy commented 2 years ago

Oh, man, I am sorry, I wasn't clear I guess. It's not just the print() statement in the realtime line, I wrote "whenever I try to collect the realtime and day and month Emeter readings I get diagnostic text", so if you are back in your code anytime, it would be great to get the other two updated as well. Thanks so much, I have learned a LOT about Python reading your code, and finally got comfortable with writing async code, that was always a mystery to me before.

Today I learned about the Pip3 --upgrade option, all very new to me

piekstra commented 2 years ago

Oh, man, I am sorry, I wasn't clear I guess. It's not just the print() statement in the realtime line, I wrote "whenever I try to collect the realtime and day and month Emeter readings I get diagnostic text", so if you are back in your code anytime, it would be great to get the other two updated as well. Thanks so much, I have learned a LOT about Python reading your code, and finally got comfortable with writing async code, that was always a mystery to me before.

Today I learned about the Pip3 --upgrade option, all very new to me

🤦

remaining print statements should be addressed in here https://github.com/piekstra/tplink-cloud-api/pull/64

israndy commented 2 years ago

Example code issues

In your readme you include the snippet:

  for device in devices:
    async def get_info(device):
      print(f'Found {device.model_type.name} device: {device.get_alias()}')
      print("SYS INFO")
      print(json.dumps(device.device_info, indent=2, default=lambda x: vars(x)
                        if hasattr(x, "__dict__") else x.name if hasattr(x, "name") else None))
      print(json.dumps(await device.get_sys_info(), indent=2, default=lambda x: vars(x)
                        if hasattr(x, "__dict__") else x.name if hasattr(x, "name") else None))
    fetch_tasks.append(get_info(device))
  await asyncio.gather(*fetch_tasks)

This is very efficient code as it allows the individual TPLink units to get back to the main app whenever they can and the entire code block isn't waiting for individual TPLinks to complete communication.

HOWEVER, since this code prints when it has information to print you end up with a lot of intermingled output from the various TPLink units.

My workaround so that all the info for each unit was printed directly under it's name was to create a list of strings, for each device I would collect the data and save it to a string and then append the entire string to the list. I could then parse the list to display the name and info collected for all the devices. I am a new programmer and don't have ANY cool coding under my belt, I am SURE there is a better solution, just wanted to point out the foible in your example code so you could take another crack at it.

if you want

israndy commented 2 years ago

I realized it's easy to fix:

y = device.device_info
z = await device.get_sys_info()
print(f'Found {device.model_type.name} device: {device.get_alias()}')
print(json.dumps(y, indent=2, default=lambda x: vars(x)
      if hasattr(x, "__dict__") else x.name if hasattr(x, "name") else None))
print("SYS INFO")
print(json.dumps(z, indent=2, default=lambda x: vars(x)
      if hasattr(x, "__dict__") else x.name if hasattr(x, "name") else None))

This keeps all the info for one device together and not interspersed with other device's information as there is no 'await' allowing another function to run

Also added

z = False
if device.model_type.name != 'HS300CHILD' :
    if device.device_info.status :
        z = await device.get_sys_info()

instead of the second line above, it removes bad info returns for inactive devices or individual outlets on energy monitoring power strips

piekstra commented 2 years ago

I'm going to close this issue for now. Please open up a pull request with the desired changes to the README for review.