mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 10:44:04 +00:00
Use ruff and ruff format (#534)
Replaces the previously used linting and code formatting tools with ruff.
This commit is contained in:
10
kasa/cli.py
10
kasa/cli.py
@@ -203,7 +203,8 @@ async def cli(
|
||||
except ImportError:
|
||||
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
|
||||
|
||||
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}..")
|
||||
res = await dev.wifi_join(ssid, password, keytype=keytype)
|
||||
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
|
||||
@@ -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):
|
||||
"""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)
|
||||
for ip, dev in found_devs.items():
|
||||
for _ip, dev in found_devs.items():
|
||||
if dev.alias.lower() == alias.lower():
|
||||
host = dev.host
|
||||
return host
|
||||
|
@@ -87,7 +87,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
|
||||
req = json_dumps(Discover.DISCOVERY_QUERY)
|
||||
_LOGGER.debug("[DISCOVERY] %s >> %s", self.target, Discover.DISCOVERY_QUERY)
|
||||
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(Discover.DISCOVERY_QUERY_2, self.target_2) # type: ignore
|
||||
|
||||
@@ -169,7 +169,8 @@ class Discover:
|
||||
>>> [dev.alias for dev in found_devices]
|
||||
['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"))
|
||||
|
||||
@@ -207,14 +208,19 @@ class Discover:
|
||||
Sends discovery message to 255.255.255.255:9999 in order
|
||||
to detect available supported devices in the local network,
|
||||
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 devices are already initialized and all but emeter-related properties can be accessed directly.
|
||||
The results of the discovery are returned as a dict of
|
||||
: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 timeout: How long to wait for responses, defaults to 5
|
||||
:param discovery_packets: Number of discovery packets to broadcast
|
||||
@@ -232,7 +238,7 @@ class Discover:
|
||||
credentials=credentials,
|
||||
timeout=timeout,
|
||||
),
|
||||
local_addr=("0.0.0.0", 0),
|
||||
local_addr=("0.0.0.0", 0), # noqa: S104
|
||||
)
|
||||
protocol = cast(_DiscoverProtocol, protocol)
|
||||
|
||||
@@ -275,7 +281,7 @@ class Discover:
|
||||
credentials=credentials,
|
||||
timeout=timeout,
|
||||
),
|
||||
local_addr=("0.0.0.0", 0),
|
||||
local_addr=("0.0.0.0", 0), # noqa: S104
|
||||
)
|
||||
protocol = cast(_DiscoverProtocol, protocol)
|
||||
|
||||
@@ -284,10 +290,10 @@ class Discover:
|
||||
|
||||
async with asyncio_timeout(timeout):
|
||||
await event.wait()
|
||||
except asyncio.TimeoutError:
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise SmartDeviceException(
|
||||
f"Timed out getting discovery response for {host}"
|
||||
)
|
||||
) from ex
|
||||
finally:
|
||||
transport.close()
|
||||
|
||||
|
@@ -48,9 +48,13 @@ class EmeterStatus(dict):
|
||||
return None
|
||||
|
||||
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):
|
||||
"""Return value in wanted units."""
|
||||
valid_keys = [
|
||||
"voltage_mv",
|
||||
"power_mw",
|
||||
@@ -65,7 +69,7 @@ class EmeterStatus(dict):
|
||||
]
|
||||
|
||||
# 1. if requested data is available, return it
|
||||
if item in super().keys():
|
||||
if item in super().keys(): # noqa: SIM118
|
||||
return super().__getitem__(item)
|
||||
# otherwise decide how to convert it
|
||||
else:
|
||||
@@ -74,7 +78,7 @@ class EmeterStatus(dict):
|
||||
if "_" in item: # upscale
|
||||
return super().__getitem__(item[: item.find("_")]) * 1000
|
||||
else: # downscale
|
||||
for i in super().keys():
|
||||
for i in super().keys(): # noqa: SIM118
|
||||
if i.startswith(item):
|
||||
return self.__getitem__(i) / 1000
|
||||
|
||||
|
@@ -44,13 +44,19 @@ class Emeter(Usage):
|
||||
return await self.call("get_realtime")
|
||||
|
||||
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 = self._convert_stat_data(data["day_list"], entry_key="day", kwh=kwh)
|
||||
return data
|
||||
|
||||
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 = self._convert_stat_data(data["month_list"], entry_key="month", kwh=kwh)
|
||||
return data
|
||||
|
@@ -57,7 +57,8 @@ class Module(ABC):
|
||||
"""Return the module specific raw data from the last update."""
|
||||
if self._module not in self._device._last_update:
|
||||
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]
|
||||
@@ -80,4 +81,7 @@ class Module(ABC):
|
||||
return self._device._create_request(self._module, query, params)
|
||||
|
||||
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})
|
||||
|
||||
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 = self._convert_stat_data(data["day_list"], entry_key="day")
|
||||
return data
|
||||
|
||||
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 = self._convert_stat_data(data["month_list"], entry_key="month")
|
||||
return data
|
||||
|
@@ -60,7 +60,7 @@ class TPLinkSmartHomeProtocol:
|
||||
"""
|
||||
if isinstance(request, dict):
|
||||
request = json_dumps(request)
|
||||
assert isinstance(request, str)
|
||||
assert isinstance(request, str) # noqa: S101
|
||||
|
||||
async with self.query_lock:
|
||||
return await self._query(request, retry_count, self.timeout)
|
||||
@@ -77,8 +77,8 @@ class TPLinkSmartHomeProtocol:
|
||||
|
||||
async def _execute_query(self, request: str) -> Dict:
|
||||
"""Execute a query on the device and wait for the response."""
|
||||
assert self.writer is not None
|
||||
assert self.reader is not None
|
||||
assert self.writer is not None # noqa: S101
|
||||
assert self.reader is not None # noqa: S101
|
||||
debug_log = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||
|
||||
if debug_log:
|
||||
@@ -116,11 +116,11 @@ class TPLinkSmartHomeProtocol:
|
||||
# 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
|
||||
#
|
||||
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and ConnectionRefusedError)
|
||||
# we do not want to keep trying since many connection open/close operations
|
||||
# in the same time frame can block the event loop. This is especially
|
||||
# import when there are multiple tplink devices being polled.
|
||||
#
|
||||
# However, if we get an unrecoverable error (_NO_RETRY_ERRORS and
|
||||
# ConnectionRefusedError) we do not want to keep trying since many
|
||||
# connection open/close operations in the same time frame can block
|
||||
# the event loop.
|
||||
# This is especially import when there are multiple tplink devices being polled.
|
||||
for retry in range(retry_count + 1):
|
||||
try:
|
||||
await self._connect(timeout)
|
||||
@@ -128,26 +128,28 @@ class TPLinkSmartHomeProtocol:
|
||||
await self.close()
|
||||
raise SmartDeviceException(
|
||||
f"Unable to connect to the device: {self.host}:{self.port}: {ex}"
|
||||
)
|
||||
) from ex
|
||||
except OSError as ex:
|
||||
await self.close()
|
||||
if ex.errno in _NO_RETRY_ERRORS or retry >= retry_count:
|
||||
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
|
||||
except Exception as ex:
|
||||
await self.close()
|
||||
if retry >= retry_count:
|
||||
_LOGGER.debug("Giving up on %s after %s retries", self.host, retry)
|
||||
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
|
||||
|
||||
try:
|
||||
assert self.reader is not None
|
||||
assert self.writer is not None
|
||||
assert self.reader is not None # noqa: S101
|
||||
assert self.writer is not None # noqa: S101
|
||||
async with asyncio_timeout(timeout):
|
||||
return await self._execute_query(request)
|
||||
except Exception as ex:
|
||||
|
@@ -59,7 +59,8 @@ class TurnOnBehavior(BaseModel):
|
||||
"""Model to present a single turn on behavior.
|
||||
|
||||
: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 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.
|
||||
|
||||
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.
|
||||
|
||||
Examples:
|
||||
@@ -159,7 +162,7 @@ class SmartBulb(SmartDevice):
|
||||
>>> bulb.brightness
|
||||
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
|
||||
ColorTempRange(min=2500, max=9000)
|
||||
@@ -175,9 +178,18 @@ class SmartBulb(SmartDevice):
|
||||
>>> bulb.hsv
|
||||
HSV(hue=180, saturation=100, value=80)
|
||||
|
||||
If you don't want to use the default transitions, you can pass `transition` in milliseconds.
|
||||
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.
|
||||
Light strips (e.g., KL420L5) do not support this feature, but silently ignore the parameter.
|
||||
If you don't want to use the default transitions,
|
||||
you can pass `transition` in milliseconds.
|
||||
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:
|
||||
|
||||
>>> asyncio.run(bulb.set_brightness(100, transition=10_000))
|
||||
@@ -187,7 +199,8 @@ class SmartBulb(SmartDevice):
|
||||
>>> 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)]
|
||||
|
||||
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.brightness
|
||||
@@ -197,7 +210,7 @@ class SmartBulb(SmartDevice):
|
||||
>>> bulb.presets[0].brightness
|
||||
100
|
||||
|
||||
"""
|
||||
""" # noqa: E501
|
||||
|
||||
LIGHT_SERVICE = "smartlife.iot.smartbulb.lightingservice"
|
||||
SET_LIGHT_METHOD = "transition_light_state"
|
||||
@@ -362,9 +375,7 @@ class SmartBulb(SmartDevice):
|
||||
|
||||
def _raise_for_invalid_brightness(self, value):
|
||||
if not isinstance(value, int) or not (0 <= value <= 100):
|
||||
raise ValueError(
|
||||
"Invalid brightness value: {} " "(valid range: 0-100%)".format(value)
|
||||
)
|
||||
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
|
||||
|
||||
@requires_update
|
||||
async def set_hsv(
|
||||
@@ -386,14 +397,11 @@ class SmartBulb(SmartDevice):
|
||||
raise SmartDeviceException("Bulb does not support color.")
|
||||
|
||||
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
||||
raise ValueError(
|
||||
"Invalid hue value: {} " "(valid range: 0-360)".format(hue)
|
||||
)
|
||||
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
|
||||
|
||||
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
|
||||
raise ValueError(
|
||||
"Invalid saturation value: {} "
|
||||
"(valid range: 0-100%)".format(saturation)
|
||||
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
|
||||
)
|
||||
|
||||
light_state = {
|
||||
@@ -433,8 +441,9 @@ class SmartBulb(SmartDevice):
|
||||
valid_temperature_range = self.valid_temperature_range
|
||||
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
|
||||
raise ValueError(
|
||||
"Temperature should be between {} "
|
||||
"and {}, was {}".format(*valid_temperature_range, temp)
|
||||
"Temperature should be between {} and {}, was {}".format(
|
||||
*valid_temperature_range, temp
|
||||
)
|
||||
)
|
||||
|
||||
light_state = {"color_temp": temp}
|
||||
|
@@ -101,8 +101,8 @@ def _parse_features(features: str) -> Set[str]:
|
||||
class SmartDevice:
|
||||
"""Base class for all supported device types.
|
||||
|
||||
You don't usually want to construct this class which implements the shared common interfaces.
|
||||
The recommended way is to either use the discovery functionality, or construct one of the subclasses:
|
||||
You don't usually want to initialize this class manually,
|
||||
but either use :class:`Discover` class, or use one of the subclasses:
|
||||
|
||||
* :class:`SmartPlug`
|
||||
* :class:`SmartBulb`
|
||||
@@ -145,7 +145,8 @@ class SmartDevice:
|
||||
>>> dev.mac
|
||||
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
|
||||
False
|
||||
@@ -154,7 +155,8 @@ class SmartDevice:
|
||||
>>> dev.is_plug
|
||||
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
|
||||
{'sw_ver': '1.2.5 Build 171213 Rel.101523',
|
||||
@@ -175,7 +177,8 @@ class SmartDevice:
|
||||
>>> dev.is_on
|
||||
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
|
||||
True
|
||||
@@ -184,7 +187,8 @@ class SmartDevice:
|
||||
>>> dev.emeter_today
|
||||
>>> 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))
|
||||
{11: 1.089, 12: 1.582}
|
||||
@@ -214,9 +218,9 @@ class SmartDevice:
|
||||
self.credentials = credentials
|
||||
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
|
||||
self._device_type = DeviceType.Unknown
|
||||
# TODO: typing Any is just as using Optional[Dict] would require separate checks in
|
||||
# accessors. the @updated_required decorator does not ensure mypy that these
|
||||
# are not accessed incorrectly.
|
||||
# TODO: typing Any is just as using Optional[Dict] would require separate
|
||||
# checks in accessors. the @updated_required decorator does not ensure
|
||||
# mypy that these are not accessed incorrectly.
|
||||
self._last_update: Any = None
|
||||
self._sys_info: Any = None # TODO: this is here to avoid changing tests
|
||||
self._features: Set[str] = set()
|
||||
@@ -230,8 +234,6 @@ class SmartDevice:
|
||||
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
||||
return
|
||||
|
||||
assert name not in self.modules
|
||||
|
||||
_LOGGER.debug("Adding module %s", module)
|
||||
self.modules[name] = module
|
||||
|
||||
@@ -751,4 +753,8 @@ class SmartDevice:
|
||||
def __repr__(self):
|
||||
if self._last_update is None:
|
||||
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.
|
||||
|
||||
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,
|
||||
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)
|
||||
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 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("ambient", AmbientLight(self, "smartlife.iot.LAS"))
|
||||
|
||||
@@ -187,7 +188,8 @@ class SmartDimmer(SmartPlug):
|
||||
"""Set action to perform on button click/hold.
|
||||
|
||||
: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
|
||||
"""
|
||||
action_type_setter = f"set_{action_type}"
|
||||
|
@@ -11,10 +11,10 @@ class SmartLightStrip(SmartBulb):
|
||||
"""Representation of a TP-Link Smart light strip.
|
||||
|
||||
Light strips work similarly to bulbs, but use a different service for controlling,
|
||||
and expose some extra information (such as length and active effect).
|
||||
This class extends :class:`SmartBulb` interface.
|
||||
and expose some extra information (such as length and active effect).
|
||||
This class extends :class:`SmartBulb` interface.
|
||||
|
||||
Examples:
|
||||
Examples:
|
||||
>>> import asyncio
|
||||
>>> strip = SmartLightStrip("127.0.0.1")
|
||||
>>> asyncio.run(strip.update())
|
||||
@@ -105,9 +105,11 @@ class SmartLightStrip(SmartBulb):
|
||||
) -> None:
|
||||
"""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 int brightness: The wanted brightness
|
||||
|
@@ -16,7 +16,8 @@ class SmartPlug(SmartDevice):
|
||||
This will allow accessing the properties using the exposed properties.
|
||||
|
||||
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,
|
||||
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.
|
||||
|
||||
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,
|
||||
and should be handled by the user of the library.
|
||||
|
@@ -1,8 +1,16 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from voluptuous import Coerce # type: ignore
|
||||
from voluptuous import REMOVE_EXTRA, All, Any, Invalid, Optional, Range, Schema
|
||||
from voluptuous import (
|
||||
REMOVE_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce, # type: ignore
|
||||
Invalid,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
from ..protocol import TPLinkSmartHomeProtocol
|
||||
|
||||
@@ -305,7 +313,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
|
||||
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)
|
||||
if child_ids:
|
||||
for child in self.proto["system"]["get_sysinfo"]["children"]:
|
||||
@@ -314,7 +324,9 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
else:
|
||||
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"])
|
||||
|
||||
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)
|
||||
new_state = light_state
|
||||
|
||||
if state_changes["on_off"] == 1: # turn on requested
|
||||
if not light_state[
|
||||
"on_off"
|
||||
]: # if we were off, use the dft_on_state as a base
|
||||
_LOGGER.debug("Bulb was off, using dft_on_state")
|
||||
new_state = light_state["dft_on_state"]
|
||||
# turn on requested, if we were off, use the dft_on_state as a base
|
||||
if state_changes["on_off"] == 1 and not light_state["on_off"]:
|
||||
_LOGGER.debug("Bulb was off, using dft_on_state")
|
||||
new_state = light_state["dft_on_state"]
|
||||
|
||||
# override the existing settings
|
||||
new_state.update(state_changes)
|
||||
@@ -384,7 +394,7 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
self.proto["system"]["get_sysinfo"]["light_state"] = new_state
|
||||
|
||||
def set_preferred_state(self, new_state, *args):
|
||||
"""Implementation of set_preferred_state."""
|
||||
"""Implement set_preferred_state."""
|
||||
self.proto["system"]["get_sysinfo"]["preferred_state"][
|
||||
new_state["index"]
|
||||
] = new_state
|
||||
@@ -459,11 +469,11 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
||||
child_ids = []
|
||||
|
||||
def get_response_for_module(target):
|
||||
if target not in proto.keys():
|
||||
if target not in proto:
|
||||
return error(msg="target not found")
|
||||
|
||||
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")
|
||||
|
||||
params = request[target][cmd]
|
||||
|
@@ -156,7 +156,7 @@ async def test_credentials(discovery_data: dict, mocker):
|
||||
mocker.patch("kasa.cli.state", new=_state)
|
||||
|
||||
# Get the type string parameter from the discovery_info
|
||||
for cli_device_type in {
|
||||
for cli_device_type in { # noqa: B007
|
||||
i
|
||||
for i in TYPE_TO_CLASS
|
||||
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)
|
||||
assert issubclass(x.__class__, SmartDevice)
|
||||
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])
|
||||
@@ -82,7 +82,7 @@ async def test_connect_single(discovery_data: dict, mocker, custom_port):
|
||||
|
||||
dev = await Discover.connect_single(host, port=custom_port)
|
||||
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):
|
||||
|
@@ -28,9 +28,10 @@ async def test_state_info(dev):
|
||||
|
||||
@pytest.mark.requires_dummy
|
||||
async def test_invalid_connection(dev):
|
||||
with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
|
||||
with pytest.raises(SmartDeviceException):
|
||||
await dev.update()
|
||||
with patch.object(
|
||||
FakeTransportProtocol, "query", side_effect=SmartDeviceException
|
||||
), pytest.raises(SmartDeviceException):
|
||||
await dev.update()
|
||||
|
||||
|
||||
@has_emeter
|
||||
|
Reference in New Issue
Block a user