mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-05-16 11:31:24 +00:00
Use ruff and ruff format (#534)
Replaces the previously used linting and code formatting tools with ruff.
This commit is contained in:
parent
0061668c9f
commit
c431dbb832
8
.flake8
8
.flake8
@ -1,8 +0,0 @@
|
|||||||
[flake8]
|
|
||||||
exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.py
|
|
||||||
max-line-length = 88
|
|
||||||
per-file-ignores =
|
|
||||||
kasa/tests/*.py:D100,D101,D102,D103,D104,F401
|
|
||||||
docs/source/conf.py:D100,D103
|
|
||||||
ignore = D105, D107, E203, E501, W503
|
|
||||||
max-complexity = 18
|
|
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@ -26,18 +26,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip poetry
|
python -m pip install --upgrade pip poetry
|
||||||
poetry install
|
poetry install
|
||||||
- name: "Run pyupgrade"
|
- name: "Linting and code formating (ruff)"
|
||||||
run: |
|
run: |
|
||||||
poetry run pre-commit run pyupgrade --all-files
|
poetry run pre-commit run ruff --all-files
|
||||||
- name: "Code formating (black)"
|
|
||||||
run: |
|
|
||||||
poetry run pre-commit run black --all-files
|
|
||||||
- name: "Code formating (flake8)"
|
|
||||||
run: |
|
|
||||||
poetry run pre-commit run flake8 --all-files
|
|
||||||
- name: "Order of imports (isort)"
|
|
||||||
run: |
|
|
||||||
poetry run pre-commit run isort --all-files
|
|
||||||
- name: "Typing checks (mypy)"
|
- name: "Typing checks (mypy)"
|
||||||
run: |
|
run: |
|
||||||
poetry run pre-commit run mypy --all-files
|
poetry run pre-commit run mypy --all-files
|
||||||
|
@ -9,28 +9,12 @@ repos:
|
|||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v3.4.0
|
rev: v0.1.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: ruff
|
||||||
args: ['--py38-plus']
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
|
- id: ruff-format
|
||||||
- repo: https://github.com/python/black
|
|
||||||
rev: 23.3.0
|
|
||||||
hooks:
|
|
||||||
- id: black
|
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/flake8
|
|
||||||
rev: 6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
additional_dependencies: [flake8-docstrings]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-isort
|
|
||||||
rev: v5.10.1
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
additional_dependencies: [toml]
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.3.0
|
rev: v1.3.0
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Script that checks if README.md is missing devices that have fixtures."""
|
"""Script that checks if README.md is missing devices that have fixtures."""
|
||||||
from kasa.tests.conftest import ALL_DEVICES, BULBS, DIMMERS, LIGHT_STRIPS, PLUGS, STRIPS
|
from kasa.tests.conftest import ALL_DEVICES, BULBS, DIMMERS, LIGHT_STRIPS, PLUGS, STRIPS
|
||||||
|
|
||||||
readme = open("README.md").read()
|
with open("README.md") as f:
|
||||||
|
readme = f.read()
|
||||||
|
|
||||||
typemap = {
|
typemap = {
|
||||||
"light strips": LIGHT_STRIPS,
|
"light strips": LIGHT_STRIPS,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""This script generates devinfo files for the test suite.
|
"""This script generates devinfo files for the test suite.
|
||||||
|
|
||||||
If you have new, yet unsupported device or a device with no devinfo file under kasa/tests/fixtures,
|
If you have new, yet unsupported device or a device with no devinfo file under
|
||||||
feel free to run this script and create a PR to add the file to the repository.
|
kasa/tests/fixtures, feel free to run this script and create a PR to add the file
|
||||||
|
to the repository.
|
||||||
|
|
||||||
Executing this script will several modules and methods one by one,
|
Executing this script will several modules and methods one by one,
|
||||||
and finally execute a query to query all of them at once.
|
and finally execute a query to query all of them at once.
|
||||||
@ -84,13 +85,13 @@ def cli(host, debug):
|
|||||||
|
|
||||||
for test_call in items:
|
for test_call in items:
|
||||||
|
|
||||||
async def _run_query():
|
async def _run_query(test_call):
|
||||||
protocol = TPLinkSmartHomeProtocol(host)
|
protocol = TPLinkSmartHomeProtocol(host)
|
||||||
return await protocol.query({test_call.module: {test_call.method: None}})
|
return await protocol.query({test_call.module: {test_call.method: None}})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
click.echo(f"Testing {test_call}..", nl=False)
|
click.echo(f"Testing {test_call}..", nl=False)
|
||||||
info = asyncio.run(_run_query())
|
info = asyncio.run(_run_query(test_call))
|
||||||
resp = info[test_call.module]
|
resp = info[test_call.module]
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
click.echo(click.style(f"FAIL {ex}", fg="red"))
|
click.echo(click.style(f"FAIL {ex}", fg="red"))
|
||||||
|
@ -15,7 +15,7 @@ from kasa.protocol import TPLinkSmartHomeProtocol
|
|||||||
def read_payloads_from_file(file):
|
def read_payloads_from_file(file):
|
||||||
"""Read the given pcap file and yield json payloads."""
|
"""Read the given pcap file and yield json payloads."""
|
||||||
pcap = dpkt.pcap.Reader(file)
|
pcap = dpkt.pcap.Reader(file)
|
||||||
for ts, pkt in pcap:
|
for _ts, pkt in pcap:
|
||||||
eth = Ethernet(pkt)
|
eth = Ethernet(pkt)
|
||||||
if eth.type != ETH_TYPE_IP:
|
if eth.type != ETH_TYPE_IP:
|
||||||
continue
|
continue
|
||||||
@ -44,9 +44,8 @@ def read_payloads_from_file(file):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
json_payload = json.loads(decrypted)
|
json_payload = json.loads(decrypted)
|
||||||
except (
|
except Exception as ex:
|
||||||
Exception
|
# this can happen when the response is split into multiple tcp segments
|
||||||
) as ex: # this can happen when the response is split into multiple tcp segments
|
|
||||||
echo(f"[red]Unable to parse payload '{decrypted}', ignoring: {ex}[/red]")
|
echo(f"[red]Unable to parse payload '{decrypted}', ignoring: {ex}[/red]")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -91,7 +90,8 @@ def parse_pcap(file):
|
|||||||
context_str = f" [ctx: {context}]" if context else ""
|
context_str = f" [ctx: {context}]" if context else ""
|
||||||
|
|
||||||
echo(
|
echo(
|
||||||
f"[{is_success}] {direction}{context_str} {module}.{cmd}: {pf(response)}"
|
f"[{is_success}] {direction}{context_str} {module}.{cmd}:"
|
||||||
|
f" {pf(response)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo(pf(seen_items))
|
echo(pf(seen_items))
|
||||||
|
@ -59,13 +59,13 @@ async def main(addrs, rounds):
|
|||||||
|
|
||||||
if test_gathered:
|
if test_gathered:
|
||||||
print("=== Testing using gather on all devices ===")
|
print("=== Testing using gather on all devices ===")
|
||||||
for i in range(rounds):
|
for _i in range(rounds):
|
||||||
data.append(await _update_concurrently(devs))
|
data.append(await _update_concurrently(devs))
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
for i in range(rounds):
|
for _i in range(rounds):
|
||||||
data.append(await _update_sequentially(devs))
|
data.append(await _update_sequentially(devs))
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ async def main(addrs, rounds):
|
|||||||
futs = []
|
futs = []
|
||||||
data = []
|
data = []
|
||||||
locks = {dev: asyncio.Lock() for dev in devs}
|
locks = {dev: asyncio.Lock() for dev in devs}
|
||||||
for i in range(rounds):
|
for _i in range(rounds):
|
||||||
for dev in devs:
|
for dev in devs:
|
||||||
futs.append(asyncio.ensure_future(_update(dev, locks[dev])))
|
futs.append(asyncio.ensure_future(_update(dev, locks[dev])))
|
||||||
|
|
||||||
|
10
kasa/cli.py
10
kasa/cli.py
@ -203,7 +203,8 @@ async def cli(
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# The configuration should be converted to use dictConfig, but this keeps mypy happy for now
|
# The configuration should be converted to use dictConfig,
|
||||||
|
# but this keeps mypy happy for now
|
||||||
logging.basicConfig(**logging_config) # type: ignore
|
logging.basicConfig(**logging_config) # type: ignore
|
||||||
|
|
||||||
if ctx.invoked_subcommand == "discover":
|
if ctx.invoked_subcommand == "discover":
|
||||||
@ -278,7 +279,8 @@ async def join(dev: SmartDevice, ssid, password, keytype):
|
|||||||
echo(f"Asking the device to connect to {ssid}..")
|
echo(f"Asking the device to connect to {ssid}..")
|
||||||
res = await dev.wifi_join(ssid, password, keytype=keytype)
|
res = await dev.wifi_join(ssid, password, keytype=keytype)
|
||||||
echo(
|
echo(
|
||||||
f"Response: {res} - if the device is not able to join the network, it will revert back to its previous state."
|
f"Response: {res} - if the device is not able to join the network, "
|
||||||
|
f"it will revert back to its previous state."
|
||||||
)
|
)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@ -347,9 +349,9 @@ async def discover(ctx, timeout, show_unsupported):
|
|||||||
|
|
||||||
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
||||||
"""Discover a device identified by its alias."""
|
"""Discover a device identified by its alias."""
|
||||||
for attempt in range(1, attempts):
|
for _attempt in range(1, attempts):
|
||||||
found_devs = await Discover.discover(target=target, timeout=timeout)
|
found_devs = await Discover.discover(target=target, timeout=timeout)
|
||||||
for ip, dev in found_devs.items():
|
for _ip, dev in found_devs.items():
|
||||||
if dev.alias.lower() == alias.lower():
|
if dev.alias.lower() == alias.lower():
|
||||||
host = dev.host
|
host = dev.host
|
||||||
return host
|
return host
|
||||||
|
@ -87,7 +87,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
|||||||
req = json_dumps(Discover.DISCOVERY_QUERY)
|
req = json_dumps(Discover.DISCOVERY_QUERY)
|
||||||
_LOGGER.debug("[DISCOVERY] %s >> %s", self.target, Discover.DISCOVERY_QUERY)
|
_LOGGER.debug("[DISCOVERY] %s >> %s", self.target, Discover.DISCOVERY_QUERY)
|
||||||
encrypted_req = TPLinkSmartHomeProtocol.encrypt(req)
|
encrypted_req = TPLinkSmartHomeProtocol.encrypt(req)
|
||||||
for i in range(self.discovery_packets):
|
for _i in range(self.discovery_packets):
|
||||||
self.transport.sendto(encrypted_req[4:], self.target) # type: ignore
|
self.transport.sendto(encrypted_req[4:], self.target) # type: ignore
|
||||||
self.transport.sendto(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore
|
self.transport.sendto(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore
|
||||||
|
|
||||||
@ -169,7 +169,8 @@ class Discover:
|
|||||||
>>> [dev.alias for dev in found_devices]
|
>>> [dev.alias for dev in found_devices]
|
||||||
['TP-LINK_Power Strip_CF69']
|
['TP-LINK_Power Strip_CF69']
|
||||||
|
|
||||||
Discovery can also be targeted to a specific broadcast address instead of the 255.255.255.255:
|
Discovery can also be targeted to a specific broadcast address instead of
|
||||||
|
the default 255.255.255.255:
|
||||||
|
|
||||||
>>> asyncio.run(Discover.discover(target="192.168.8.255"))
|
>>> asyncio.run(Discover.discover(target="192.168.8.255"))
|
||||||
|
|
||||||
@ -207,14 +208,19 @@ class Discover:
|
|||||||
Sends discovery message to 255.255.255.255:9999 in order
|
Sends discovery message to 255.255.255.255:9999 in order
|
||||||
to detect available supported devices in the local network,
|
to detect available supported devices in the local network,
|
||||||
and waits for given timeout for answers from devices.
|
and waits for given timeout for answers from devices.
|
||||||
If you have multiple interfaces, you can use target parameter to specify the network for discovery.
|
If you have multiple interfaces,
|
||||||
|
you can use *target* parameter to specify the network for discovery.
|
||||||
|
|
||||||
If given, `on_discovered` coroutine will get awaited with a :class:`SmartDevice`-derived object as parameter.
|
If given, `on_discovered` coroutine will get awaited with
|
||||||
|
a :class:`SmartDevice`-derived object as parameter.
|
||||||
|
|
||||||
The results of the discovery are returned as a dict of :class:`SmartDevice`-derived objects keyed with IP addresses.
|
The results of the discovery are returned as a dict of
|
||||||
The devices are already initialized and all but emeter-related properties can be accessed directly.
|
:class:`SmartDevice`-derived objects keyed with IP addresses.
|
||||||
|
The devices are already initialized and all but emeter-related properties
|
||||||
|
can be accessed directly.
|
||||||
|
|
||||||
:param target: The target address where to send the broadcast discovery queries if multi-homing (e.g. 192.168.xxx.255).
|
:param target: The target address where to send the broadcast discovery
|
||||||
|
queries if multi-homing (e.g. 192.168.xxx.255).
|
||||||
:param on_discovered: coroutine to execute on discovery
|
:param on_discovered: coroutine to execute on discovery
|
||||||
:param timeout: How long to wait for responses, defaults to 5
|
:param timeout: How long to wait for responses, defaults to 5
|
||||||
:param discovery_packets: Number of discovery packets to broadcast
|
:param discovery_packets: Number of discovery packets to broadcast
|
||||||
@ -232,7 +238,7 @@ class Discover:
|
|||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
),
|
),
|
||||||
local_addr=("0.0.0.0", 0),
|
local_addr=("0.0.0.0", 0), # noqa: S104
|
||||||
)
|
)
|
||||||
protocol = cast(_DiscoverProtocol, protocol)
|
protocol = cast(_DiscoverProtocol, protocol)
|
||||||
|
|
||||||
@ -275,7 +281,7 @@ class Discover:
|
|||||||
credentials=credentials,
|
credentials=credentials,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
),
|
),
|
||||||
local_addr=("0.0.0.0", 0),
|
local_addr=("0.0.0.0", 0), # noqa: S104
|
||||||
)
|
)
|
||||||
protocol = cast(_DiscoverProtocol, protocol)
|
protocol = cast(_DiscoverProtocol, protocol)
|
||||||
|
|
||||||
@ -284,10 +290,10 @@ class Discover:
|
|||||||
|
|
||||||
async with asyncio_timeout(timeout):
|
async with asyncio_timeout(timeout):
|
||||||
await event.wait()
|
await event.wait()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError as ex:
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Timed out getting discovery response for {host}"
|
f"Timed out getting discovery response for {host}"
|
||||||
)
|
) from ex
|
||||||
finally:
|
finally:
|
||||||
transport.close()
|
transport.close()
|
||||||
|
|
||||||
|
@ -48,9 +48,13 @@ class EmeterStatus(dict):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<EmeterStatus power={self.power} voltage={self.voltage} current={self.current} total={self.total}>"
|
return (
|
||||||
|
f"<EmeterStatus power={self.power} voltage={self.voltage}"
|
||||||
|
f" current={self.current} total={self.total}>"
|
||||||
|
)
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item):
|
||||||
|
"""Return value in wanted units."""
|
||||||
valid_keys = [
|
valid_keys = [
|
||||||
"voltage_mv",
|
"voltage_mv",
|
||||||
"power_mw",
|
"power_mw",
|
||||||
@ -65,7 +69,7 @@ class EmeterStatus(dict):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# 1. if requested data is available, return it
|
# 1. if requested data is available, return it
|
||||||
if item in super().keys():
|
if item in super().keys(): # noqa: SIM118
|
||||||
return super().__getitem__(item)
|
return super().__getitem__(item)
|
||||||
# otherwise decide how to convert it
|
# otherwise decide how to convert it
|
||||||
else:
|
else:
|
||||||
@ -74,7 +78,7 @@ class EmeterStatus(dict):
|
|||||||
if "_" in item: # upscale
|
if "_" in item: # upscale
|
||||||
return super().__getitem__(item[: item.find("_")]) * 1000
|
return super().__getitem__(item[: item.find("_")]) * 1000
|
||||||
else: # downscale
|
else: # downscale
|
||||||
for i in super().keys():
|
for i in super().keys(): # noqa: SIM118
|
||||||
if i.startswith(item):
|
if i.startswith(item):
|
||||||
return self.__getitem__(i) / 1000
|
return self.__getitem__(i) / 1000
|
||||||
|
|
||||||
|
@ -44,13 +44,19 @@ class Emeter(Usage):
|
|||||||
return await self.call("get_realtime")
|
return await self.call("get_realtime")
|
||||||
|
|
||||||
async def get_daystat(self, *, year=None, month=None, kwh=True) -> Dict:
|
async def get_daystat(self, *, year=None, month=None, kwh=True) -> Dict:
|
||||||
"""Return daily stats for the given year & month as a dictionary of {day: energy, ...}."""
|
"""Return daily stats for the given year & month.
|
||||||
|
|
||||||
|
The return value is a dictionary of {day: energy, ...}.
|
||||||
|
"""
|
||||||
data = await self.get_raw_daystat(year=year, month=month)
|
data = await self.get_raw_daystat(year=year, month=month)
|
||||||
data = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
|
data = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def get_monthstat(self, *, year=None, kwh=True) -> Dict:
|
async def get_monthstat(self, *, year=None, kwh=True) -> Dict:
|
||||||
"""Return monthly stats for the given year as a dictionary of {month: energy, ...}."""
|
"""Return monthly stats for the given year.
|
||||||
|
|
||||||
|
The return value is a dictionary of {month: energy, ...}.
|
||||||
|
"""
|
||||||
data = await self.get_raw_monthstat(year=year)
|
data = await self.get_raw_monthstat(year=year)
|
||||||
data = self._convert_stat_data(data["month_list"], entry_key="month", kwh=kwh)
|
data = self._convert_stat_data(data["month_list"], entry_key="month", kwh=kwh)
|
||||||
return data
|
return data
|
||||||
|
@ -57,7 +57,8 @@ class Module(ABC):
|
|||||||
"""Return the module specific raw data from the last update."""
|
"""Return the module specific raw data from the last update."""
|
||||||
if self._module not in self._device._last_update:
|
if self._module not in self._device._last_update:
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"You need to call update() prior accessing module data for '{self._module}'"
|
f"You need to call update() prior accessing module data"
|
||||||
|
f" for '{self._module}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._device._last_update[self._module]
|
return self._device._last_update[self._module]
|
||||||
@ -80,4 +81,7 @@ class Module(ABC):
|
|||||||
return self._device._create_request(self._module, query, params)
|
return self._device._create_request(self._module, query, params)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Module {self.__class__.__name__} ({self._module}) for {self._device.host}>"
|
return (
|
||||||
|
f"<Module {self.__class__.__name__} ({self._module})"
|
||||||
|
f" for {self._device.host}>"
|
||||||
|
)
|
||||||
|
@ -73,13 +73,19 @@ class Usage(Module):
|
|||||||
return await self.call("get_monthstat", {"year": year})
|
return await self.call("get_monthstat", {"year": year})
|
||||||
|
|
||||||
async def get_daystat(self, *, year=None, month=None) -> Dict:
|
async def get_daystat(self, *, year=None, month=None) -> Dict:
|
||||||
"""Return daily stats for the given year & month as a dictionary of {day: time, ...}."""
|
"""Return daily stats for the given year & month.
|
||||||
|
|
||||||
|
The return value is a dictionary of {day: time, ...}.
|
||||||
|
"""
|
||||||
data = await self.get_raw_daystat(year=year, month=month)
|
data = await self.get_raw_daystat(year=year, month=month)
|
||||||
data = self._convert_stat_data(data["day_list"], entry_key="day")
|
data = self._convert_stat_data(data["day_list"], entry_key="day")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def get_monthstat(self, *, year=None) -> Dict:
|
async def get_monthstat(self, *, year=None) -> Dict:
|
||||||
"""Return monthly stats for the given year as a dictionary of {month: time, ...}."""
|
"""Return monthly stats for the given year.
|
||||||
|
|
||||||
|
The return value is a dictionary of {month: time, ...}.
|
||||||
|
"""
|
||||||
data = await self.get_raw_monthstat(year=year)
|
data = await self.get_raw_monthstat(year=year)
|
||||||
data = self._convert_stat_data(data["month_list"], entry_key="month")
|
data = self._convert_stat_data(data["month_list"], entry_key="month")
|
||||||
return data
|
return data
|
||||||
|
@ -60,7 +60,7 @@ class TPLinkSmartHomeProtocol:
|
|||||||
"""
|
"""
|
||||||
if isinstance(request, dict):
|
if isinstance(request, dict):
|
||||||
request = json_dumps(request)
|
request = json_dumps(request)
|
||||||
assert isinstance(request, str)
|
assert isinstance(request, str) # noqa: S101
|
||||||
|
|
||||||
async with self.query_lock:
|
async with self.query_lock:
|
||||||
return await self._query(request, retry_count, self.timeout)
|
return await self._query(request, retry_count, self.timeout)
|
||||||
@ -77,8 +77,8 @@ class TPLinkSmartHomeProtocol:
|
|||||||
|
|
||||||
async def _execute_query(self, request: str) -> Dict:
|
async def _execute_query(self, request: str) -> Dict:
|
||||||
"""Execute a query on the device and wait for the response."""
|
"""Execute a query on the device and wait for the response."""
|
||||||
assert self.writer is not None
|
assert self.writer is not None # noqa: S101
|
||||||
assert self.reader is not None
|
assert self.reader is not None # noqa: S101
|
||||||
debug_log = _LOGGER.isEnabledFor(logging.DEBUG)
|
debug_log = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
if debug_log:
|
if debug_log:
|
||||||
@ -116,11 +116,11 @@ class TPLinkSmartHomeProtocol:
|
|||||||
# Most of the time we will already be connected if the device is online
|
# Most of the time we will already be connected if the device is online
|
||||||
# and the connect call will do nothing and return right away
|
# and the connect call will do nothing and return right away
|
||||||
#
|
#
|
||||||
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and ConnectionRefusedError)
|
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and
|
||||||
# we do not want to keep trying since many connection open/close operations
|
# ConnectionRefusedError) we do not want to keep trying since many
|
||||||
# in the same time frame can block the event loop. This is especially
|
# connection open/close operations in the same time frame can block
|
||||||
# import when there are multiple tplink devices being polled.
|
# the event loop.
|
||||||
#
|
# This is especially import when there are multiple tplink devices being polled.
|
||||||
for retry in range(retry_count + 1):
|
for retry in range(retry_count + 1):
|
||||||
try:
|
try:
|
||||||
await self._connect(timeout)
|
await self._connect(timeout)
|
||||||
@ -128,26 +128,28 @@ class TPLinkSmartHomeProtocol:
|
|||||||
await self.close()
|
await self.close()
|
||||||
raise SmartDeviceException(
|
raise SmartDeviceException(
|
||||||
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
||||||
)
|
) from 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}:{self.port}: {ex}"
|
f"Unable to connect to the device:"
|
||||||
)
|
f" {self.host}:{self.port}: {ex}"
|
||||||
|
) from ex
|
||||||
continue
|
continue
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
await self.close()
|
await self.close()
|
||||||
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}:{self.port}: {ex}"
|
f"Unable to connect to the device:"
|
||||||
)
|
f" {self.host}:{self.port}: {ex}"
|
||||||
|
) from ex
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
assert self.reader is not None
|
assert self.reader is not None # noqa: S101
|
||||||
assert self.writer is not None
|
assert self.writer is not None # noqa: S101
|
||||||
async with asyncio_timeout(timeout):
|
async with asyncio_timeout(timeout):
|
||||||
return await self._execute_query(request)
|
return await self._execute_query(request)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
@ -59,7 +59,8 @@ class TurnOnBehavior(BaseModel):
|
|||||||
"""Model to present a single turn on behavior.
|
"""Model to present a single turn on behavior.
|
||||||
|
|
||||||
:param int preset: the index number of wanted preset.
|
:param int preset: the index number of wanted preset.
|
||||||
:param BehaviorMode mode: last status or preset mode. If you are changing existing settings, you should not set this manually.
|
:param BehaviorMode mode: last status or preset mode.
|
||||||
|
If you are changing existing settings, you should not set this manually.
|
||||||
|
|
||||||
To change the behavior, it is only necessary to change the :attr:`preset` field
|
To change the behavior, it is only necessary to change the :attr:`preset` field
|
||||||
to contain either the preset index, or ``None`` for the last known state.
|
to contain either the preset index, or ``None`` for the last known state.
|
||||||
@ -121,9 +122,11 @@ class SmartBulb(SmartDevice):
|
|||||||
This will allow accessing the properties using the exposed properties.
|
This will allow accessing the properties using the exposed properties.
|
||||||
|
|
||||||
All changes to the device are done using awaitable methods,
|
All changes to the device are done using awaitable methods,
|
||||||
which will not change the cached values, but you must await :func:`update()` separately.
|
which will not change the cached values,
|
||||||
|
so you must await :func:`update()` to fetch updates values from the device.
|
||||||
|
|
||||||
Errors reported by the device are raised as :class:`SmartDeviceExceptions <kasa.exceptions.SmartDeviceException>`,
|
Errors reported by the device are raised as
|
||||||
|
:class:`SmartDeviceExceptions <kasa.exceptions.SmartDeviceException>`,
|
||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@ -159,7 +162,7 @@ class SmartBulb(SmartDevice):
|
|||||||
>>> bulb.brightness
|
>>> bulb.brightness
|
||||||
50
|
50
|
||||||
|
|
||||||
Bulbs supporting color temperature can be queried to know which range is accepted:
|
Bulbs supporting color temperature can be queried for the supported range:
|
||||||
|
|
||||||
>>> bulb.valid_temperature_range
|
>>> bulb.valid_temperature_range
|
||||||
ColorTempRange(min=2500, max=9000)
|
ColorTempRange(min=2500, max=9000)
|
||||||
@ -175,9 +178,18 @@ class SmartBulb(SmartDevice):
|
|||||||
>>> bulb.hsv
|
>>> bulb.hsv
|
||||||
HSV(hue=180, saturation=100, value=80)
|
HSV(hue=180, saturation=100, value=80)
|
||||||
|
|
||||||
If you don't want to use the default transitions, you can pass `transition` in milliseconds.
|
If you don't want to use the default transitions,
|
||||||
This applies to all transitions (:func:`turn_on`, :func:`turn_off`, :func:`set_hsv`, :func:`set_color_temp`, :func:`set_brightness`) if supported by the device.
|
you can pass `transition` in milliseconds.
|
||||||
Light strips (e.g., KL420L5) do not support this feature, but silently ignore the parameter.
|
All methods changing the state of the device support this parameter:
|
||||||
|
|
||||||
|
* :func:`turn_on`
|
||||||
|
* :func:`turn_off`
|
||||||
|
* :func:`set_hsv`
|
||||||
|
* :func:`set_color_temp`
|
||||||
|
* :func:`set_brightness`
|
||||||
|
|
||||||
|
Light strips (e.g., KL420L5) do not support this feature,
|
||||||
|
but silently ignore the parameter.
|
||||||
The following changes the brightness over a period of 10 seconds:
|
The following changes the brightness over a period of 10 seconds:
|
||||||
|
|
||||||
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
|
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
|
||||||
@ -187,7 +199,8 @@ class SmartBulb(SmartDevice):
|
|||||||
>>> bulb.presets
|
>>> bulb.presets
|
||||||
[SmartBulbPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), SmartBulbPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
[SmartBulbPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), SmartBulbPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), SmartBulbPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
||||||
|
|
||||||
To modify an existing preset, pass :class:`~kasa.smartbulb.SmartBulbPreset` instance to :func:`save_preset` method:
|
To modify an existing preset, pass :class:`~kasa.smartbulb.SmartBulbPreset`
|
||||||
|
instance to :func:`save_preset` method:
|
||||||
|
|
||||||
>>> preset = bulb.presets[0]
|
>>> preset = bulb.presets[0]
|
||||||
>>> preset.brightness
|
>>> preset.brightness
|
||||||
@ -197,7 +210,7 @@ class SmartBulb(SmartDevice):
|
|||||||
>>> bulb.presets[0].brightness
|
>>> bulb.presets[0].brightness
|
||||||
100
|
100
|
||||||
|
|
||||||
"""
|
""" # noqa: E501
|
||||||
|
|
||||||
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
||||||
SET_LIGHT_METHOD = "transition_light_state"
|
SET_LIGHT_METHOD = "transition_light_state"
|
||||||
@ -362,9 +375,7 @@ class SmartBulb(SmartDevice):
|
|||||||
|
|
||||||
def _raise_for_invalid_brightness(self, value):
|
def _raise_for_invalid_brightness(self, value):
|
||||||
if not isinstance(value, int) or not (0 <= value <= 100):
|
if not isinstance(value, int) or not (0 <= value <= 100):
|
||||||
raise ValueError(
|
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
|
||||||
"Invalid brightness value: {} " "(valid range: 0-100%)".format(value)
|
|
||||||
)
|
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
async def set_hsv(
|
async def set_hsv(
|
||||||
@ -386,14 +397,11 @@ class SmartBulb(SmartDevice):
|
|||||||
raise SmartDeviceException("Bulb does not support color.")
|
raise SmartDeviceException("Bulb does not support color.")
|
||||||
|
|
||||||
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
||||||
raise ValueError(
|
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
|
||||||
"Invalid hue value: {} " "(valid range: 0-360)".format(hue)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
|
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Invalid saturation value: {} "
|
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
|
||||||
"(valid range: 0-100%)".format(saturation)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
light_state = {
|
light_state = {
|
||||||
@ -433,8 +441,9 @@ class SmartBulb(SmartDevice):
|
|||||||
valid_temperature_range = self.valid_temperature_range
|
valid_temperature_range = self.valid_temperature_range
|
||||||
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
|
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Temperature should be between {} "
|
"Temperature should be between {} and {}, was {}".format(
|
||||||
"and {}, was {}".format(*valid_temperature_range, temp)
|
*valid_temperature_range, temp
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
light_state = {"color_temp": temp}
|
light_state = {"color_temp": temp}
|
||||||
|
@ -101,8 +101,8 @@ def _parse_features(features: str) -> Set[str]:
|
|||||||
class SmartDevice:
|
class SmartDevice:
|
||||||
"""Base class for all supported device types.
|
"""Base class for all supported device types.
|
||||||
|
|
||||||
You don't usually want to construct this class which implements the shared common interfaces.
|
You don't usually want to initialize this class manually,
|
||||||
The recommended way is to either use the discovery functionality, or construct one of the subclasses:
|
but either use :class:`Discover` class, or use one of the subclasses:
|
||||||
|
|
||||||
* :class:`SmartPlug`
|
* :class:`SmartPlug`
|
||||||
* :class:`SmartBulb`
|
* :class:`SmartBulb`
|
||||||
@ -145,7 +145,8 @@ class SmartDevice:
|
|||||||
>>> dev.mac
|
>>> dev.mac
|
||||||
01:23:45:67:89:ab
|
01:23:45:67:89:ab
|
||||||
|
|
||||||
When initialized using discovery or using a subclass, you can check the type of the device:
|
When initialized using discovery or using a subclass,
|
||||||
|
you can check the type of the device:
|
||||||
|
|
||||||
>>> dev.is_bulb
|
>>> dev.is_bulb
|
||||||
False
|
False
|
||||||
@ -154,7 +155,8 @@ class SmartDevice:
|
|||||||
>>> dev.is_plug
|
>>> dev.is_plug
|
||||||
True
|
True
|
||||||
|
|
||||||
You can also get the hardware and software as a dict, or access the full device response:
|
You can also get the hardware and software as a dict,
|
||||||
|
or access the full device response:
|
||||||
|
|
||||||
>>> dev.hw_info
|
>>> dev.hw_info
|
||||||
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
|
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
|
||||||
@ -175,7 +177,8 @@ class SmartDevice:
|
|||||||
>>> dev.is_on
|
>>> dev.is_on
|
||||||
True
|
True
|
||||||
|
|
||||||
Some devices provide energy consumption meter, and regular update will already fetch some information:
|
Some devices provide energy consumption meter,
|
||||||
|
and regular update will already fetch some information:
|
||||||
|
|
||||||
>>> dev.has_emeter
|
>>> dev.has_emeter
|
||||||
True
|
True
|
||||||
@ -184,7 +187,8 @@ class SmartDevice:
|
|||||||
>>> dev.emeter_today
|
>>> dev.emeter_today
|
||||||
>>> dev.emeter_this_month
|
>>> dev.emeter_this_month
|
||||||
|
|
||||||
You can also query the historical data (note that these needs to be awaited), keyed with month/day:
|
You can also query the historical data (note that these needs to be awaited),
|
||||||
|
keyed with month/day:
|
||||||
|
|
||||||
>>> asyncio.run(dev.get_emeter_monthly(year=2016))
|
>>> asyncio.run(dev.get_emeter_monthly(year=2016))
|
||||||
{11: 1.089, 12: 1.582}
|
{11: 1.089, 12: 1.582}
|
||||||
@ -214,9 +218,9 @@ class SmartDevice:
|
|||||||
self.credentials = credentials
|
self.credentials = credentials
|
||||||
_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
|
||||||
# accessors. the @updated_required decorator does not ensure mypy that these
|
# checks in accessors. the @updated_required decorator does not ensure
|
||||||
# are not accessed incorrectly.
|
# mypy that these are not accessed incorrectly.
|
||||||
self._last_update: Any = None
|
self._last_update: Any = None
|
||||||
self._sys_info: Any = None # TODO: this is here to avoid changing tests
|
self._sys_info: Any = None # TODO: this is here to avoid changing tests
|
||||||
self._features: Set[str] = set()
|
self._features: Set[str] = set()
|
||||||
@ -230,8 +234,6 @@ class SmartDevice:
|
|||||||
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
||||||
return
|
return
|
||||||
|
|
||||||
assert name not in self.modules
|
|
||||||
|
|
||||||
_LOGGER.debug("Adding module %s", module)
|
_LOGGER.debug("Adding module %s", module)
|
||||||
self.modules[name] = module
|
self.modules[name] = module
|
||||||
|
|
||||||
@ -751,4 +753,8 @@ class SmartDevice:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self._last_update is None:
|
if self._last_update is None:
|
||||||
return f"<{self._device_type} at {self.host} - update() needed>"
|
return f"<{self._device_type} at {self.host} - update() needed>"
|
||||||
return f"<{self._device_type} model {self.model} at {self.host} ({self.alias}), is_on: {self.is_on} - dev specific: {self.state_information}>"
|
return (
|
||||||
|
f"<{self._device_type} model {self.model} at {self.host}"
|
||||||
|
f" ({self.alias}), is_on: {self.is_on}"
|
||||||
|
f" - dev specific: {self.state_information}>"
|
||||||
|
)
|
||||||
|
@ -41,7 +41,8 @@ class SmartDimmer(SmartPlug):
|
|||||||
This will allow accessing the properties using the exposed properties.
|
This will allow accessing the properties using the exposed properties.
|
||||||
|
|
||||||
All changes to the device are done using awaitable methods,
|
All changes to the device are done using awaitable methods,
|
||||||
which will not change the cached values, but you must await :func:`update()` separately.
|
which will not change the cached values,
|
||||||
|
but you must await :func:`update()` separately.
|
||||||
|
|
||||||
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
@ -74,7 +75,7 @@ class SmartDimmer(SmartPlug):
|
|||||||
super().__init__(host, port=port, credentials=credentials, timeout=timeout)
|
super().__init__(host, port=port, credentials=credentials, timeout=timeout)
|
||||||
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
|
||||||
self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
|
self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
|
||||||
self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
|
self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
|
||||||
|
|
||||||
@ -187,7 +188,8 @@ class SmartDimmer(SmartPlug):
|
|||||||
"""Set action to perform on button click/hold.
|
"""Set action to perform on button click/hold.
|
||||||
|
|
||||||
:param action_type ActionType: whether to control double click or hold action.
|
:param action_type ActionType: whether to control double click or hold action.
|
||||||
:param action ButtonAction: what should the button do (nothing, instant, gentle, change preset)
|
:param action ButtonAction: what should the button do
|
||||||
|
(nothing, instant, gentle, change preset)
|
||||||
:param index int: in case of preset change, the preset to select
|
:param index int: in case of preset change, the preset to select
|
||||||
"""
|
"""
|
||||||
action_type_setter = f"set_{action_type}"
|
action_type_setter = f"set_{action_type}"
|
||||||
|
@ -11,10 +11,10 @@ class SmartLightStrip(SmartBulb):
|
|||||||
"""Representation of a TP-Link Smart light strip.
|
"""Representation of a TP-Link Smart light strip.
|
||||||
|
|
||||||
Light strips work similarly to bulbs, but use a different service for controlling,
|
Light strips work similarly to bulbs, but use a different service for controlling,
|
||||||
and expose some extra information (such as length and active effect).
|
and expose some extra information (such as length and active effect).
|
||||||
This class extends :class:`SmartBulb` interface.
|
This class extends :class:`SmartBulb` interface.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> import asyncio
|
>>> import asyncio
|
||||||
>>> strip = SmartLightStrip("127.0.0.1")
|
>>> strip = SmartLightStrip("127.0.0.1")
|
||||||
>>> asyncio.run(strip.update())
|
>>> asyncio.run(strip.update())
|
||||||
@ -105,9 +105,11 @@ class SmartLightStrip(SmartBulb):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set an effect on the device.
|
"""Set an effect on the device.
|
||||||
|
|
||||||
If brightness or transition is defined, its value will be used instead of the effect-specific default.
|
If brightness or transition is defined,
|
||||||
|
its value will be used instead of the effect-specific default.
|
||||||
|
|
||||||
See :meth:`effect_list` for available effects, or use :meth:`set_custom_effect` for custom effects.
|
See :meth:`effect_list` for available effects,
|
||||||
|
or use :meth:`set_custom_effect` for custom effects.
|
||||||
|
|
||||||
:param str effect: The effect to set
|
:param str effect: The effect to set
|
||||||
:param int brightness: The wanted brightness
|
:param int brightness: The wanted brightness
|
||||||
|
@ -16,7 +16,8 @@ class SmartPlug(SmartDevice):
|
|||||||
This will allow accessing the properties using the exposed properties.
|
This will allow accessing the properties using the exposed properties.
|
||||||
|
|
||||||
All changes to the device are done using awaitable methods,
|
All changes to the device are done using awaitable methods,
|
||||||
which will not change the cached values, but you must await :func:`update()` separately.
|
which will not change the cached values,
|
||||||
|
but you must await :func:`update()` separately.
|
||||||
|
|
||||||
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
|
@ -40,7 +40,8 @@ class SmartStrip(SmartDevice):
|
|||||||
This will allow accessing the properties using the exposed properties.
|
This will allow accessing the properties using the exposed properties.
|
||||||
|
|
||||||
All changes to the device are done using awaitable methods,
|
All changes to the device are done using awaitable methods,
|
||||||
which will not change the cached values, but you must await :func:`update()` separately.
|
which will not change the cached values,
|
||||||
|
but you must await :func:`update()` separately.
|
||||||
|
|
||||||
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
Errors reported by the device are raised as :class:`SmartDeviceException`\s,
|
||||||
and should be handled by the user of the library.
|
and should be handled by the user of the library.
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from voluptuous import Coerce # type: ignore
|
from voluptuous import (
|
||||||
from voluptuous import REMOVE_EXTRA, All, Any, Invalid, Optional, Range, Schema
|
REMOVE_EXTRA,
|
||||||
|
All,
|
||||||
|
Any,
|
||||||
|
Coerce, # type: ignore
|
||||||
|
Invalid,
|
||||||
|
Optional,
|
||||||
|
Range,
|
||||||
|
Schema,
|
||||||
|
)
|
||||||
|
|
||||||
from ..protocol import TPLinkSmartHomeProtocol
|
from ..protocol import TPLinkSmartHomeProtocol
|
||||||
|
|
||||||
@ -305,7 +313,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
|
|
||||||
self.proto = proto
|
self.proto = proto
|
||||||
|
|
||||||
def set_alias(self, x, child_ids=[]):
|
def set_alias(self, x, child_ids=None):
|
||||||
|
if child_ids is None:
|
||||||
|
child_ids = []
|
||||||
_LOGGER.debug("Setting alias to %s, child_ids: %s", x["alias"], child_ids)
|
_LOGGER.debug("Setting alias to %s, child_ids: %s", x["alias"], child_ids)
|
||||||
if child_ids:
|
if child_ids:
|
||||||
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
||||||
@ -314,7 +324,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
else:
|
else:
|
||||||
self.proto["system"]["get_sysinfo"]["alias"] = x["alias"]
|
self.proto["system"]["get_sysinfo"]["alias"] = x["alias"]
|
||||||
|
|
||||||
def set_relay_state(self, x, child_ids=[]):
|
def set_relay_state(self, x, child_ids=None):
|
||||||
|
if child_ids is None:
|
||||||
|
child_ids = []
|
||||||
_LOGGER.debug("Setting relay state to %s", x["state"])
|
_LOGGER.debug("Setting relay state to %s", x["state"])
|
||||||
|
|
||||||
if not child_ids and "children" in self.proto["system"]["get_sysinfo"]:
|
if not child_ids and "children" in self.proto["system"]["get_sysinfo"]:
|
||||||
@ -362,12 +374,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
_LOGGER.debug("Current light state: %s", light_state)
|
_LOGGER.debug("Current light state: %s", light_state)
|
||||||
new_state = light_state
|
new_state = light_state
|
||||||
|
|
||||||
if state_changes["on_off"] == 1: # turn on requested
|
# turn on requested, if we were off, use the dft_on_state as a base
|
||||||
if not light_state[
|
if state_changes["on_off"] == 1 and not light_state["on_off"]:
|
||||||
"on_off"
|
_LOGGER.debug("Bulb was off, using dft_on_state")
|
||||||
]: # if we were off, use the dft_on_state as a base
|
new_state = light_state["dft_on_state"]
|
||||||
_LOGGER.debug("Bulb was off, using dft_on_state")
|
|
||||||
new_state = light_state["dft_on_state"]
|
|
||||||
|
|
||||||
# override the existing settings
|
# override the existing settings
|
||||||
new_state.update(state_changes)
|
new_state.update(state_changes)
|
||||||
@ -384,7 +394,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
self.proto["system"]["get_sysinfo"]["light_state"] = new_state
|
self.proto["system"]["get_sysinfo"]["light_state"] = new_state
|
||||||
|
|
||||||
def set_preferred_state(self, new_state, *args):
|
def set_preferred_state(self, new_state, *args):
|
||||||
"""Implementation of set_preferred_state."""
|
"""Implement set_preferred_state."""
|
||||||
self.proto["system"]["get_sysinfo"]["preferred_state"][
|
self.proto["system"]["get_sysinfo"]["preferred_state"][
|
||||||
new_state["index"]
|
new_state["index"]
|
||||||
] = new_state
|
] = new_state
|
||||||
@ -459,11 +469,11 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
child_ids = []
|
child_ids = []
|
||||||
|
|
||||||
def get_response_for_module(target):
|
def get_response_for_module(target):
|
||||||
if target not in proto.keys():
|
if target not in proto:
|
||||||
return error(msg="target not found")
|
return error(msg="target not found")
|
||||||
|
|
||||||
def get_response_for_command(cmd):
|
def get_response_for_command(cmd):
|
||||||
if cmd not in proto[target].keys():
|
if cmd not in proto[target]:
|
||||||
return error(msg=f"command {cmd} not found")
|
return error(msg=f"command {cmd} not found")
|
||||||
|
|
||||||
params = request[target][cmd]
|
params = request[target][cmd]
|
||||||
|
@ -156,7 +156,7 @@ async def test_credentials(discovery_data: dict, mocker):
|
|||||||
mocker.patch("kasa.cli.state", new=_state)
|
mocker.patch("kasa.cli.state", new=_state)
|
||||||
|
|
||||||
# Get the type string parameter from the discovery_info
|
# Get the type string parameter from the discovery_info
|
||||||
for cli_device_type in {
|
for cli_device_type in { # noqa: B007
|
||||||
i
|
i
|
||||||
for i in TYPE_TO_CLASS
|
for i in TYPE_TO_CLASS
|
||||||
if TYPE_TO_CLASS[i] == Discover._get_device_class(discovery_data)
|
if TYPE_TO_CLASS[i] == Discover._get_device_class(discovery_data)
|
||||||
|
@ -71,7 +71,7 @@ async def test_discover_single(discovery_data: dict, mocker, custom_port):
|
|||||||
x = await Discover.discover_single(host, port=custom_port)
|
x = await Discover.discover_single(host, 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 or 9999
|
assert x.port == custom_port or x.port == 9999
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("custom_port", [123, None])
|
@pytest.mark.parametrize("custom_port", [123, None])
|
||||||
@ -82,7 +82,7 @@ async def test_connect_single(discovery_data: dict, mocker, custom_port):
|
|||||||
|
|
||||||
dev = await Discover.connect_single(host, port=custom_port)
|
dev = await Discover.connect_single(host, port=custom_port)
|
||||||
assert issubclass(dev.__class__, SmartDevice)
|
assert issubclass(dev.__class__, SmartDevice)
|
||||||
assert dev.port == custom_port or 9999
|
assert dev.port == custom_port or dev.port == 9999
|
||||||
|
|
||||||
|
|
||||||
async def test_connect_single_query_fails(discovery_data: dict, mocker):
|
async def test_connect_single_query_fails(discovery_data: dict, mocker):
|
||||||
|
@ -28,9 +28,10 @@ async def test_state_info(dev):
|
|||||||
|
|
||||||
@pytest.mark.requires_dummy
|
@pytest.mark.requires_dummy
|
||||||
async def test_invalid_connection(dev):
|
async def test_invalid_connection(dev):
|
||||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
with patch.object(
|
||||||
with pytest.raises(SmartDeviceException):
|
FakeTransportProtocol, "query", side_effect=SmartDeviceException
|
||||||
await dev.update()
|
), pytest.raises(SmartDeviceException):
|
||||||
|
await dev.update()
|
||||||
|
|
||||||
|
|
||||||
@has_emeter
|
@has_emeter
|
||||||
|
@ -54,12 +54,6 @@ coverage = {version = "*", extras = ["toml"]}
|
|||||||
docs = ["sphinx", "sphinx_rtd_theme", "sphinxcontrib-programoutput", "myst-parser", "docutils"]
|
docs = ["sphinx", "sphinx_rtd_theme", "sphinxcontrib-programoutput", "myst-parser", "docutils"]
|
||||||
speedups = ["orjson", "kasa-crypt"]
|
speedups = ["orjson", "kasa-crypt"]
|
||||||
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
known_first_party = "kasa"
|
|
||||||
known_third_party = ["asyncclick", "pytest", "setuptools", "voluptuous"]
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = ["kasa"]
|
source = ["kasa"]
|
||||||
branch = true
|
branch = true
|
||||||
@ -72,15 +66,6 @@ exclude_lines = [
|
|||||||
"def __repr__"
|
"def __repr__"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.interrogate]
|
|
||||||
ignore-init-method = true
|
|
||||||
ignore-magic = true
|
|
||||||
ignore-private = true
|
|
||||||
ignore-semiprivate = true
|
|
||||||
fail-under = 100
|
|
||||||
exclude = ['kasa/tests/*']
|
|
||||||
verbose = 2
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
markers = [
|
||||||
"requires_dummy: test requires dummy data to pass, skipped on real devices",
|
"requires_dummy: test requires dummy data to pass, skipped on real devices",
|
||||||
@ -95,3 +80,39 @@ ignore-path-errors = ["docs/source/index.rst;D000"]
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py38"
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle
|
||||||
|
"D", # pydocstyle
|
||||||
|
"F", # pyflakes
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"I", # isort
|
||||||
|
"S", # bandit
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"D105", # Missing docstring in magic method
|
||||||
|
"D107", # Missing docstring in `__init__`
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.pydocstyle]
|
||||||
|
convention = "pep257"
|
||||||
|
|
||||||
|
[tool.ruff.per-file-ignores]
|
||||||
|
"kasa/tests/*.py" = [
|
||||||
|
"D100",
|
||||||
|
"D101",
|
||||||
|
"D102",
|
||||||
|
"D103",
|
||||||
|
"D104",
|
||||||
|
"F401",
|
||||||
|
"S101", # allow asserts
|
||||||
|
"E501", # ignore line-too-longs
|
||||||
|
]
|
||||||
|
"docs/source/conf.py" = [
|
||||||
|
"D100",
|
||||||
|
"D103",
|
||||||
|
]
|
||||||
|
8
tox.ini
8
tox.ini
@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist=py37,py38,flake8,lint,coverage
|
envlist=py37,py38,lint,coverage
|
||||||
skip_missing_interpreters = True
|
skip_missing_interpreters = True
|
||||||
isolated_build = True
|
isolated_build = True
|
||||||
|
|
||||||
@ -31,12 +31,6 @@ commands =
|
|||||||
coverage report
|
coverage report
|
||||||
coverage html
|
coverage html
|
||||||
|
|
||||||
[testenv:flake8]
|
|
||||||
deps=
|
|
||||||
flake8
|
|
||||||
flake8-docstrings
|
|
||||||
commands=flake8 kasa
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
deps = pre-commit
|
deps = pre-commit
|
||||||
skip_install = true
|
skip_install = true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user