mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-05-02 12:41:18 +00:00
Make device port configurable (#471)
This commit is contained in:
parent
6199521269
commit
9b039d8374
11
kasa/cli.py
11
kasa/cli.py
@ -99,6 +99,12 @@ def json_formatter_cb(result, **kwargs):
|
|||||||
required=False,
|
required=False,
|
||||||
help="The host name or IP address of the device to connect to.",
|
help="The host name or IP address of the device to connect to.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--port",
|
||||||
|
envvar="KASA_PORT",
|
||||||
|
required=False,
|
||||||
|
help="The port of the device to connect to.",
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--alias",
|
"--alias",
|
||||||
envvar="KASA_NAME",
|
envvar="KASA_NAME",
|
||||||
@ -125,7 +131,7 @@ def json_formatter_cb(result, **kwargs):
|
|||||||
)
|
)
|
||||||
@click.version_option(package_name="python-kasa")
|
@click.version_option(package_name="python-kasa")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
async def cli(ctx, host, alias, target, debug, type, json):
|
async def cli(ctx, host, port, alias, target, debug, type, json):
|
||||||
"""A tool for controlling TP-Link smart home devices.""" # noqa
|
"""A tool for controlling TP-Link smart home devices.""" # noqa
|
||||||
# no need to perform any checks if we are just displaying the help
|
# no need to perform any checks if we are just displaying the help
|
||||||
if sys.argv[-1] == "--help":
|
if sys.argv[-1] == "--help":
|
||||||
@ -179,7 +185,7 @@ async def cli(ctx, host, alias, target, debug, type, json):
|
|||||||
dev = TYPE_TO_CLASS[type](host)
|
dev = TYPE_TO_CLASS[type](host)
|
||||||
else:
|
else:
|
||||||
echo("No --type defined, discovering..")
|
echo("No --type defined, discovering..")
|
||||||
dev = await Discover.discover_single(host)
|
dev = await Discover.discover_single(host, port=port)
|
||||||
|
|
||||||
await dev.update()
|
await dev.update()
|
||||||
ctx.obj = dev
|
ctx.obj = dev
|
||||||
@ -275,6 +281,7 @@ async def state(dev: SmartDevice):
|
|||||||
"""Print out device state and versions."""
|
"""Print out device state and versions."""
|
||||||
echo(f"[bold]== {dev.alias} - {dev.model} ==[/bold]")
|
echo(f"[bold]== {dev.alias} - {dev.model} ==[/bold]")
|
||||||
echo(f"\tHost: {dev.host}")
|
echo(f"\tHost: {dev.host}")
|
||||||
|
echo(f"\tPort: {dev.port}")
|
||||||
echo(f"\tDevice state: {dev.is_on}")
|
echo(f"\tDevice state: {dev.is_on}")
|
||||||
if dev.is_strip:
|
if dev.is_strip:
|
||||||
echo("\t[bold]== Plugs ==[/bold]")
|
echo("\t[bold]== Plugs ==[/bold]")
|
||||||
|
@ -193,19 +193,19 @@ class Discover:
|
|||||||
return protocol.discovered_devices
|
return protocol.discovered_devices
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def discover_single(host: str) -> SmartDevice:
|
async def discover_single(host: str, *, port: Optional[int] = None) -> SmartDevice:
|
||||||
"""Discover a single device by the given IP address.
|
"""Discover a single device by the given IP address.
|
||||||
|
|
||||||
:param host: Hostname of device to query
|
:param host: Hostname of device to query
|
||||||
:rtype: SmartDevice
|
:rtype: SmartDevice
|
||||||
:return: Object for querying/controlling found device.
|
:return: Object for querying/controlling found device.
|
||||||
"""
|
"""
|
||||||
protocol = TPLinkSmartHomeProtocol(host)
|
protocol = TPLinkSmartHomeProtocol(host, port=port)
|
||||||
|
|
||||||
info = await protocol.query(Discover.DISCOVERY_QUERY)
|
info = await protocol.query(Discover.DISCOVERY_QUERY)
|
||||||
|
|
||||||
device_class = Discover._get_device_class(info)
|
device_class = Discover._get_device_class(info)
|
||||||
dev = device_class(host)
|
dev = device_class(host, port=port)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
|
|
||||||
return dev
|
return dev
|
||||||
|
@ -33,9 +33,10 @@ class TPLinkSmartHomeProtocol:
|
|||||||
DEFAULT_TIMEOUT = 5
|
DEFAULT_TIMEOUT = 5
|
||||||
BLOCK_SIZE = 4
|
BLOCK_SIZE = 4
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
"""Create a protocol object."""
|
"""Create a protocol object."""
|
||||||
self.host = host
|
self.host = host
|
||||||
|
self.port = port or TPLinkSmartHomeProtocol.DEFAULT_PORT
|
||||||
self.reader: Optional[asyncio.StreamReader] = None
|
self.reader: Optional[asyncio.StreamReader] = None
|
||||||
self.writer: Optional[asyncio.StreamWriter] = None
|
self.writer: Optional[asyncio.StreamWriter] = None
|
||||||
self.query_lock: Optional[asyncio.Lock] = None
|
self.query_lock: Optional[asyncio.Lock] = None
|
||||||
@ -78,7 +79,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
if self.writer:
|
if self.writer:
|
||||||
return
|
return
|
||||||
self.reader = self.writer = None
|
self.reader = self.writer = None
|
||||||
task = asyncio.open_connection(self.host, TPLinkSmartHomeProtocol.DEFAULT_PORT)
|
task = asyncio.open_connection(self.host, self.port)
|
||||||
self.reader, self.writer = await asyncio.wait_for(task, timeout=timeout)
|
self.reader, self.writer = await asyncio.wait_for(task, timeout=timeout)
|
||||||
|
|
||||||
async def _execute_query(self, request: str) -> Dict:
|
async def _execute_query(self, request: str) -> Dict:
|
||||||
@ -133,13 +134,13 @@ class TPLinkSmartHomeProtocol:
|
|||||||
except ConnectionRefusedError as ex:
|
except ConnectionRefusedError as ex:
|
||||||
await self.close()
|
await self.close()
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Unable to connect to the device: {self.host}: {ex}"
|
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
||||||
)
|
)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
await self.close()
|
await self.close()
|
||||||
if ex.errno in _NO_RETRY_ERRORS or retry >= retry_count:
|
if ex.errno in _NO_RETRY_ERRORS or retry >= retry_count:
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Unable to connect to the device: {self.host}: {ex}"
|
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@ -147,7 +148,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
if retry >= retry_count:
|
if retry >= retry_count:
|
||||||
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
|
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Unable to connect to the device: {self.host}: {ex}"
|
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -162,7 +163,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
if retry >= retry_count:
|
if retry >= retry_count:
|
||||||
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
|
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Unable to query the device {self.host}: {ex}"
|
f"Unable to query the device {self.host}:{self.port}: {ex}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -199,8 +199,8 @@ class SmartBulb(SmartDevice):
|
|||||||
SET_LIGHT_METHOD = "transition_light_state"
|
SET_LIGHT_METHOD = "transition_light_state"
|
||||||
emeter_type = "smartlife.iot.common.emeter"
|
emeter_type = "smartlife.iot.common.emeter"
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
super().__init__(host=host)
|
super().__init__(host=host, port=port)
|
||||||
self._device_type = DeviceType.Bulb
|
self._device_type = DeviceType.Bulb
|
||||||
self.add_module("schedule", Schedule(self, "smartlife.iot.common.schedule"))
|
self.add_module("schedule", Schedule(self, "smartlife.iot.common.schedule"))
|
||||||
self.add_module("usage", Usage(self, "smartlife.iot.common.schedule"))
|
self.add_module("usage", Usage(self, "smartlife.iot.common.schedule"))
|
||||||
|
@ -191,14 +191,15 @@ class SmartDevice:
|
|||||||
|
|
||||||
emeter_type = "emeter"
|
emeter_type = "emeter"
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
"""Create a new SmartDevice instance.
|
"""Create a new SmartDevice instance.
|
||||||
|
|
||||||
:param str host: host name or ip address on which the device listens
|
:param str host: host name or ip address on which the device listens
|
||||||
"""
|
"""
|
||||||
self.host = host
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
|
||||||
self.protocol = TPLinkSmartHomeProtocol(host)
|
self.protocol = TPLinkSmartHomeProtocol(host, port=port)
|
||||||
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
|
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
|
||||||
self._device_type = DeviceType.Unknown
|
self._device_type = DeviceType.Unknown
|
||||||
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
|
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
|
||||||
|
@ -62,8 +62,8 @@ class SmartDimmer(SmartPlug):
|
|||||||
|
|
||||||
DIMMER_SERVICE = "smartlife.iot.dimmer"
|
DIMMER_SERVICE = "smartlife.iot.dimmer"
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
super().__init__(host)
|
super().__init__(host, port=port)
|
||||||
self._device_type = DeviceType.Dimmer
|
self._device_type = DeviceType.Dimmer
|
||||||
# TODO: need to be verified if it's okay to call these on HS220 w/o these
|
# TODO: need to be verified if it's okay to call these on HS220 w/o these
|
||||||
# TODO: need to be figured out what's the best approach to detect support for these
|
# TODO: need to be figured out what's the best approach to detect support for these
|
||||||
|
@ -41,8 +41,8 @@ class SmartLightStrip(SmartBulb):
|
|||||||
LIGHT_SERVICE = "smartlife.iot.lightStrip"
|
LIGHT_SERVICE = "smartlife.iot.lightStrip"
|
||||||
SET_LIGHT_METHOD = "set_light_state"
|
SET_LIGHT_METHOD = "set_light_state"
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
super().__init__(host)
|
super().__init__(host, port=port)
|
||||||
self._device_type = DeviceType.LightStrip
|
self._device_type = DeviceType.LightStrip
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Module for smart plugs (HS100, HS110, ..)."""
|
"""Module for smart plugs (HS100, HS110, ..)."""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from kasa.modules import Antitheft, Cloud, Schedule, Time, Usage
|
from kasa.modules import Antitheft, Cloud, Schedule, Time, Usage
|
||||||
from kasa.smartdevice import DeviceType, SmartDevice, requires_update
|
from kasa.smartdevice import DeviceType, SmartDevice, requires_update
|
||||||
@ -37,8 +37,8 @@ class SmartPlug(SmartDevice):
|
|||||||
For more examples, see the :class:`SmartDevice` class.
|
For more examples, see the :class:`SmartDevice` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
super().__init__(host)
|
super().__init__(host, port=port)
|
||||||
self._device_type = DeviceType.Plug
|
self._device_type = DeviceType.Plug
|
||||||
self.add_module("schedule", Schedule(self, "schedule"))
|
self.add_module("schedule", Schedule(self, "schedule"))
|
||||||
self.add_module("usage", Usage(self, "schedule"))
|
self.add_module("usage", Usage(self, "schedule"))
|
||||||
|
@ -79,8 +79,8 @@ class SmartStrip(SmartDevice):
|
|||||||
For more examples, see the :class:`SmartDevice` class.
|
For more examples, see the :class:`SmartDevice` class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str, *, port: Optional[int] = None) -> None:
|
||||||
super().__init__(host=host)
|
super().__init__(host=host, port=port)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self._device_type = DeviceType.Strip
|
self._device_type = DeviceType.Strip
|
||||||
self.add_module("antitheft", Antitheft(self, "anti_theft"))
|
self.add_module("antitheft", Antitheft(self, "anti_theft"))
|
||||||
|
@ -52,12 +52,14 @@ async def test_type_unknown():
|
|||||||
Discover._get_device_class(invalid_info)
|
Discover._get_device_class(invalid_info)
|
||||||
|
|
||||||
|
|
||||||
async def test_discover_single(discovery_data: dict, mocker):
|
@pytest.mark.parametrize("custom_port", [123, None])
|
||||||
|
async def test_discover_single(discovery_data: dict, mocker, custom_port):
|
||||||
"""Make sure that discover_single returns an initialized SmartDevice instance."""
|
"""Make sure that discover_single returns an initialized SmartDevice instance."""
|
||||||
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", return_value=discovery_data)
|
mocker.patch("kasa.TPLinkSmartHomeProtocol.query", return_value=discovery_data)
|
||||||
x = await Discover.discover_single("127.0.0.1")
|
x = await Discover.discover_single("127.0.0.1", port=custom_port)
|
||||||
assert issubclass(x.__class__, SmartDevice)
|
assert issubclass(x.__class__, SmartDevice)
|
||||||
assert x._sys_info is not None
|
assert x._sys_info is not None
|
||||||
|
assert x.port == custom_port
|
||||||
|
|
||||||
|
|
||||||
INVALIDS = [
|
INVALIDS = [
|
||||||
|
@ -129,6 +129,36 @@ async def test_protocol_logging(mocker, caplog, log_level):
|
|||||||
assert "success" not in caplog.text
|
assert "success" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("custom_port", [123, None])
|
||||||
|
async def test_protocol_custom_port(mocker, custom_port):
|
||||||
|
encrypted = TPLinkSmartHomeProtocol.encrypt('{"great":"success"}')[
|
||||||
|
TPLinkSmartHomeProtocol.BLOCK_SIZE :
|
||||||
|
]
|
||||||
|
|
||||||
|
async def _mock_read(byte_count):
|
||||||
|
nonlocal encrypted
|
||||||
|
if byte_count == TPLinkSmartHomeProtocol.BLOCK_SIZE:
|
||||||
|
return struct.pack(">I", len(encrypted))
|
||||||
|
if byte_count == len(encrypted):
|
||||||
|
return encrypted
|
||||||
|
raise ValueError(f"No mock for {byte_count}")
|
||||||
|
|
||||||
|
def aio_mock_writer(_, port):
|
||||||
|
reader = mocker.patch("asyncio.StreamReader")
|
||||||
|
writer = mocker.patch("asyncio.StreamWriter")
|
||||||
|
if custom_port is None:
|
||||||
|
assert port == 9999
|
||||||
|
else:
|
||||||
|
assert port == custom_port
|
||||||
|
mocker.patch.object(reader, "readexactly", _mock_read)
|
||||||
|
return reader, writer
|
||||||
|
|
||||||
|
protocol = TPLinkSmartHomeProtocol("127.0.0.1", port=custom_port)
|
||||||
|
mocker.patch("asyncio.open_connection", side_effect=aio_mock_writer)
|
||||||
|
response = await protocol.query({})
|
||||||
|
assert response == {"great": "success"}
|
||||||
|
|
||||||
|
|
||||||
def test_encrypt():
|
def test_encrypt():
|
||||||
d = json.dumps({"foo": 1, "bar": 2})
|
d = json.dumps({"foo": 1, "bar": 2})
|
||||||
encrypted = TPLinkSmartHomeProtocol.encrypt(d)
|
encrypted = TPLinkSmartHomeProtocol.encrypt(d)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user