| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """Authentication class for KASA username / passwords.""" | ||
| from hashlib import md5 | ||
|
|
||
|
|
||
| class Auth: | ||
| """Authentication for Kasa KLAP authentication.""" | ||
|
|
||
| def __init__(self, user: str = "", password: str = ""): | ||
| self.user = user | ||
| self.password = password | ||
| self.md5user = md5(user.encode()).digest() | ||
| self.md5password = md5(password.encode()).digest() | ||
| self.md5auth = md5(self.md5user + self.md5password).digest() | ||
|
|
||
| def authenticator(self): | ||
| """Return the KLAP authenticator for these credentials.""" | ||
| return self.md5auth | ||
|
|
||
| def owner(self): | ||
| """Return the MD5 hash of the username in this object.""" | ||
| return self.md5user |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,14 @@ | ||||||||||||||||||||||||
| """Discovery module for TP-Link Smart Home devices.""" | ||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||
| import binascii | ||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||
| import socket | ||||||||||||||||||||||||
| from typing import Awaitable, Callable, Dict, Optional, Type, cast | ||||||||||||||||||||||||
| from typing import Awaitable, Callable, Dict, Mapping, Optional, Type, Union, cast | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from kasa.auth import Auth | ||||||||||||||||||||||||
| from kasa.klapprotocol import TPLinkKLAP | ||||||||||||||||||||||||
| from kasa.protocol import TPLinkSmartHomeProtocol | ||||||||||||||||||||||||
| from kasa.smartbulb import SmartBulb | ||||||||||||||||||||||||
| from kasa.smartdevice import SmartDevice, SmartDeviceException | ||||||||||||||||||||||||
| Expand Down Expand Up | @@ -35,13 +39,17 @@ def __init__( | |||||||||||||||||||||||
| target: str = "255.255.255.255", | ||||||||||||||||||||||||
| discovery_packets: int = 3, | ||||||||||||||||||||||||
| interface: Optional[str] = None, | ||||||||||||||||||||||||
| authentication: Optional[Auth] = None, | ||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||
| self.transport = None | ||||||||||||||||||||||||
| self.discovery_packets = discovery_packets | ||||||||||||||||||||||||
| self.interface = interface | ||||||||||||||||||||||||
| self.on_discovered = on_discovered | ||||||||||||||||||||||||
| self.target = (target, Discover.DISCOVERY_PORT) | ||||||||||||||||||||||||
| self.new_target = (target, Discover.NEW_DISCOVERY_PORT) | ||||||||||||||||||||||||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
| self.discovered_devices = {} | ||||||||||||||||||||||||
| self.authentication = authentication | ||||||||||||||||||||||||
| self.emptyUser = hashlib.md5().digest() | ||||||||||||||||||||||||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
Not worth to introduce a new variable for this, it can be created when needed.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def connection_made(self, transport) -> None: | ||||||||||||||||||||||||
| """Set socket options for broadcasting.""" | ||||||||||||||||||||||||
| Expand All | @@ -61,26 +69,49 @@ def do_discover(self) -> None: | |||||||||||||||||||||||
| req = json.dumps(Discover.DISCOVERY_QUERY) | ||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] %s >> %s", self.target, Discover.DISCOVERY_QUERY) | ||||||||||||||||||||||||
| encrypted_req = TPLinkSmartHomeProtocol.encrypt(req) | ||||||||||||||||||||||||
| new_req = binascii.unhexlify("020000010000000000000000463cb5d3") | ||||||||||||||||||||||||
| for i in range(self.discovery_packets): | ||||||||||||||||||||||||
| self.transport.sendto(encrypted_req[4:], self.target) # type: ignore | ||||||||||||||||||||||||
| self.transport.sendto(new_req, self.new_target) # type: ignore | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def datagram_received(self, data, addr) -> None: | ||||||||||||||||||||||||
| """Handle discovery responses.""" | ||||||||||||||||||||||||
| ip, port = addr | ||||||||||||||||||||||||
| if ip in self.discovered_devices: | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| info = json.loads(TPLinkSmartHomeProtocol.decrypt(data)) | ||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| if port == 9999: | ||||||||||||||||||||||||
| info = json.loads(TPLinkSmartHomeProtocol.decrypt(data)) | ||||||||||||||||||||||||
| device_class = Discover._get_device_class(info) | ||||||||||||||||||||||||
| except SmartDeviceException as ex: | ||||||||||||||||||||||||
| _LOGGER.debug("Unable to find device type from %s: %s", info, ex) | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
| device = device_class(ip) | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| info = json.loads(data[16:]) | ||||||||||||||||||||||||
| device_class = Discover._get_new_device_class(info) | ||||||||||||||||||||||||
| owner = Discover._get_new_owner(info) | ||||||||||||||||||||||||
| if owner is not None: | ||||||||||||||||||||||||
| owner_bin = bytes.fromhex(owner) | ||||||||||||||||||||||||
|
Comment thread
Comment on lines
+88
to
+92
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityPlease separate the klap handling into its own, internal method. Maybe this (and the related methods) should be part of the klapprotocol module, preferably just offering an interface like KLAPProtocol.get_device_for_auth(payload, auth)? In the current form it is hard to write unittests for its behavior.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _LOGGER.debug( | ||||||||||||||||||||||||
| "[DISCOVERY] Device owner is %s, empty owner is %s", | ||||||||||||||||||||||||
| owner_bin, | ||||||||||||||||||||||||
| self.emptyUser, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if owner is None or owner == "" or owner_bin == self.emptyUser: | ||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] Device %s has no owner", ip) | ||||||||||||||||||||||||
| device = device_class(ip, Auth()) | ||||||||||||||||||||||||
| elif ( | ||||||||||||||||||||||||
| self.authentication is not None | ||||||||||||||||||||||||
| and owner_bin == self.authentication.owner() | ||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] Device %s has authenticated owner", ip) | ||||||||||||||||||||||||
| device = device_class(ip, self.authentication) | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] Found %s with unknown owner %s", ip, owner) | ||||||||||||||||||||||||
| return | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| _LOGGER.debug("[DISCOVERY] %s << %s", ip, info) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| device = device_class(ip) | ||||||||||||||||||||||||
| device.update_from_discover_info(info) | ||||||||||||||||||||||||
| asyncio.ensure_future(device.update()) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| self.discovered_devices[ip] = device | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Expand Down Expand Up | @@ -132,6 +163,8 @@ class Discover: | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| DISCOVERY_PORT = 9999 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| NEW_DISCOVERY_PORT = 20002 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| DISCOVERY_QUERY = { | ||||||||||||||||||||||||
| "system": {"get_sysinfo": None}, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Expand All | @@ -144,7 +177,8 @@ async def discover( | |||||||||||||||||||||||
| timeout=5, | ||||||||||||||||||||||||
| discovery_packets=3, | ||||||||||||||||||||||||
| interface=None, | ||||||||||||||||||||||||
| ) -> DeviceDict: | ||||||||||||||||||||||||
| authentication=None, | ||||||||||||||||||||||||
| ) -> Mapping[str, Union[SmartDevice, Dict]]: | ||||||||||||||||||||||||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityMmh, why is this change necessary, what's wrong with DeviceDict? The interface should be as clear as possible to make, and that's the aim of returning that instead of some other structure.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
| """Discover supported devices. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Sends discovery message to 255.255.255.255:9999 in order | ||||||||||||||||||||||||
| Expand All | @@ -171,6 +205,7 @@ async def discover( | |||||||||||||||||||||||
| on_discovered=on_discovered, | ||||||||||||||||||||||||
| discovery_packets=discovery_packets, | ||||||||||||||||||||||||
| interface=interface, | ||||||||||||||||||||||||
| authentication=authentication, | ||||||||||||||||||||||||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityThe authentication needs to be documented in the docstring.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| local_addr=("0.0.0.0", 0), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| Expand All | @@ -187,20 +222,29 @@ async def discover( | |||||||||||||||||||||||
| return protocol.discovered_devices | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| async def discover_single(host: str) -> SmartDevice: | ||||||||||||||||||||||||
| async def discover_single(host: str, authentication=None) -> SmartDevice: | ||||||||||||||||||||||||
| """Discover a single device by the given IP address. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| :param host: Hostname of device to query | ||||||||||||||||||||||||
| :rtype: SmartDevice | ||||||||||||||||||||||||
| :return: Object for querying/controlling found device. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| protocol = TPLinkSmartHomeProtocol(host) | ||||||||||||||||||||||||
| if authentication is None: | ||||||||||||||||||||||||
| protocol = TPLinkSmartHomeProtocol(host) | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| protocol = TPLinkKLAP(host, authentication) | ||||||||||||||||||||||||
| # protocol = TPLinkSmartHomeProtocol(host) | ||||||||||||||||||||||||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
Sorry, something went wrong.
All reactions
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityRemove.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| info = await protocol.query(Discover.DISCOVERY_QUERY) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| device_class = Discover._get_device_class(info) | ||||||||||||||||||||||||
| dev = device_class(host) | ||||||||||||||||||||||||
| await dev.update() | ||||||||||||||||||||||||
| if device_class is not None: | ||||||||||||||||||||||||
| if authentication is None: | ||||||||||||||||||||||||
| dev = device_class(host) | ||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||
| dev = device_class(host, authentication) | ||||||||||||||||||||||||
| await dev.update() | ||||||||||||||||||||||||
| return dev | ||||||||||||||||||||||||
|
Comment thread
Comment on lines
+241
to
+247
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
It's better to keep the original code, and just add keyword-only authentication parameter to SmartDevice class.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return dev | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Expand Down Expand Up | @@ -231,3 +275,43 @@ def _get_device_class(info: dict) -> Type[SmartDevice]: | |||||||||||||||||||||||
| return SmartBulb | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| raise SmartDeviceException("Unknown device type: %s" % type_) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def _get_new_device_class(info: dict) -> Type[SmartDevice]: | ||||||||||||||||||||||||
| """Find SmartDevice subclass given new discovery payload.""" | ||||||||||||||||||||||||
| if "result" not in info: | ||||||||||||||||||||||||
| raise SmartDeviceException("No 'result' in discovery response") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if "device_type" not in info["result"]: | ||||||||||||||||||||||||
| raise SmartDeviceException("No 'device_type' in discovery result") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| dtype = info["result"]["device_type"] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if dtype == "IOT.SMARTPLUGSWITCH": | ||||||||||||||||||||||||
| return SmartPlug | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| raise SmartDeviceException("Unknown device type: %s", dtype) | ||||||||||||||||||||||||
|
Comment thread
Comment on lines
+280
to
+293
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityI don't like the fact that there are two separate paths for the discovery, we should find a way to somehow generalize the code to handle model detection for both discovery protocols.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def _get_new_owner(info: dict) -> Optional[str]: | ||||||||||||||||||||||||
| """Find owner given new-style discovery payload.""" | ||||||||||||||||||||||||
| if "result" not in info: | ||||||||||||||||||||||||
| raise SmartDeviceException("No 'result' in discovery response") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if "owner" not in info["result"]: | ||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return info["result"]["owner"] | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||||||||||
| logging.basicConfig(level=logging.INFO) | ||||||||||||||||||||||||
| loop = asyncio.get_event_loop() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| async def _on_device(dev): | ||||||||||||||||||||||||
| await dev.update() | ||||||||||||||||||||||||
| _LOGGER.info("Got device: %s", dev) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| devices = loop.run_until_complete(Discover.discover(on_discovered=_on_device)) | ||||||||||||||||||||||||
| for ip, dev in devices.items(): | ||||||||||||||||||||||||
| print(f"[{ip}] {dev}") | ||||||||||||||||||||||||
|
Comment thread
Comment on lines
+307
to
+317
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
Let's remove this. If needed, we should improve the kasa discover instead of this.
Sorry, something went wrong.
All reactions
|
||||||||||||||||||||||||
| Back | FazBrowse Home | New Git URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityThe authentication should probably be passed to all of these? Please rebase and skip the authentication for the deprecated flags (see https://github.com/python-kasa/python-kasa/blob/master/kasa/cli.py#L99).
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.