Enable and convert to future annotations (#838)

This commit is contained in:
Steven B 2024-04-17 14:39:24 +01:00 committed by GitHub
parent 82d92aeea5
commit 203bd79253
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 562 additions and 462 deletions

View File

@ -8,6 +8,8 @@ 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.
""" """
from __future__ import annotations
import base64 import base64
import collections.abc import collections.abc
import json import json
@ -17,7 +19,6 @@ import traceback
from collections import defaultdict, namedtuple from collections import defaultdict, namedtuple
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
from typing import Dict, List, Union
import asyncclick as click import asyncclick as click
@ -143,7 +144,7 @@ def default_to_regular(d):
async def handle_device(basedir, autosave, device: Device, batch_size: int): async def handle_device(basedir, autosave, device: Device, batch_size: int):
"""Create a fixture for a single device instance.""" """Create a fixture for a single device instance."""
if isinstance(device, SmartDevice): if isinstance(device, SmartDevice):
fixture_results: List[FixtureResult] = await get_smart_fixtures( fixture_results: list[FixtureResult] = await get_smart_fixtures(
device, batch_size device, batch_size
) )
else: else:
@ -344,12 +345,12 @@ def _echo_error(msg: str):
async def _make_requests_or_exit( async def _make_requests_or_exit(
device: SmartDevice, device: SmartDevice,
requests: List[SmartRequest], requests: list[SmartRequest],
name: str, name: str,
batch_size: int, batch_size: int,
*, *,
child_device_id: str, child_device_id: str,
) -> Dict[str, Dict]: ) -> dict[str, dict]:
final = {} final = {}
protocol = ( protocol = (
device.protocol device.protocol
@ -362,7 +363,7 @@ async def _make_requests_or_exit(
for i in range(0, end, step): for i in range(0, end, step):
x = i x = i
requests_step = requests[x : x + step] requests_step = requests[x : x + step]
request: Union[List[SmartRequest], SmartRequest] = ( request: list[SmartRequest] | SmartRequest = (
requests_step[0] if len(requests_step) == 1 else requests_step requests_step[0] if len(requests_step) == 1 else requests_step
) )
responses = await protocol.query(SmartRequest._create_request_dict(request)) responses = await protocol.query(SmartRequest._create_request_dict(request))
@ -586,7 +587,7 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
finally: finally:
await device.protocol.close() await device.protocol.close()
device_requests: Dict[str, List[SmartRequest]] = {} device_requests: dict[str, list[SmartRequest]] = {}
for success in successes: for success in successes:
device_request = device_requests.setdefault(success.child_device_id, []) device_request = device_requests.setdefault(success.child_device_id, [])
device_request.append(success.request) device_request.append(success.request)

View File

@ -25,9 +25,10 @@ heart_beat
""" """
from __future__ import annotations
import logging import logging
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from typing import List, Optional, Union
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
class SmartRequest: class SmartRequest:
"""Class to represent a smart protocol request.""" """Class to represent a smart protocol request."""
def __init__(self, method_name: str, params: Optional["SmartRequestParams"] = None): def __init__(self, method_name: str, params: SmartRequestParams | None = None):
self.method_name = method_name self.method_name = method_name
if params: if params:
self.params = params.to_dict() self.params = params.to_dict()
@ -93,7 +94,7 @@ class SmartRequest:
class LedStatusParams(SmartRequestParams): class LedStatusParams(SmartRequestParams):
"""LED Status params.""" """LED Status params."""
led_rule: Optional[str] = None led_rule: str | None = None
@staticmethod @staticmethod
def from_bool(state: bool): def from_bool(state: bool):
@ -105,42 +106,42 @@ class SmartRequest:
class LightInfoParams(SmartRequestParams): class LightInfoParams(SmartRequestParams):
"""LightInfo params.""" """LightInfo params."""
brightness: Optional[int] = None brightness: int | None = None
color_temp: Optional[int] = None color_temp: int | None = None
hue: Optional[int] = None hue: int | None = None
saturation: Optional[int] = None saturation: int | None = None
@dataclass @dataclass
class DynamicLightEffectParams(SmartRequestParams): class DynamicLightEffectParams(SmartRequestParams):
"""LightInfo params.""" """LightInfo params."""
enable: bool enable: bool
id: Optional[str] = None id: str | None = None
@staticmethod @staticmethod
def get_raw_request( def get_raw_request(
method: str, params: Optional[SmartRequestParams] = None method: str, params: SmartRequestParams | None = None
) -> "SmartRequest": ) -> SmartRequest:
"""Send a raw request to the device.""" """Send a raw request to the device."""
return SmartRequest(method, params) return SmartRequest(method, params)
@staticmethod @staticmethod
def component_nego() -> "SmartRequest": def component_nego() -> SmartRequest:
"""Get quick setup component info.""" """Get quick setup component info."""
return SmartRequest("component_nego") return SmartRequest("component_nego")
@staticmethod @staticmethod
def get_device_info() -> "SmartRequest": def get_device_info() -> SmartRequest:
"""Get device info.""" """Get device info."""
return SmartRequest("get_device_info") return SmartRequest("get_device_info")
@staticmethod @staticmethod
def get_device_usage() -> "SmartRequest": def get_device_usage() -> SmartRequest:
"""Get device usage.""" """Get device usage."""
return SmartRequest("get_device_usage") return SmartRequest("get_device_usage")
@staticmethod @staticmethod
def device_info_list(ver_code) -> List["SmartRequest"]: def device_info_list(ver_code) -> list[SmartRequest]:
"""Get device info list.""" """Get device info list."""
if ver_code == 1: if ver_code == 1:
return [SmartRequest.get_device_info()] return [SmartRequest.get_device_info()]
@ -151,12 +152,12 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def get_auto_update_info() -> "SmartRequest": def get_auto_update_info() -> SmartRequest:
"""Get auto update info.""" """Get auto update info."""
return SmartRequest("get_auto_update_info") return SmartRequest("get_auto_update_info")
@staticmethod @staticmethod
def firmware_info_list() -> List["SmartRequest"]: def firmware_info_list() -> list[SmartRequest]:
"""Get info list.""" """Get info list."""
return [ return [
SmartRequest.get_raw_request("get_fw_download_state"), SmartRequest.get_raw_request("get_fw_download_state"),
@ -164,48 +165,48 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def qs_component_nego() -> "SmartRequest": def qs_component_nego() -> SmartRequest:
"""Get quick setup component info.""" """Get quick setup component info."""
return SmartRequest("qs_component_nego") return SmartRequest("qs_component_nego")
@staticmethod @staticmethod
def get_device_time() -> "SmartRequest": def get_device_time() -> SmartRequest:
"""Get device time.""" """Get device time."""
return SmartRequest("get_device_time") return SmartRequest("get_device_time")
@staticmethod @staticmethod
def get_child_device_list() -> "SmartRequest": def get_child_device_list() -> SmartRequest:
"""Get child device list.""" """Get child device list."""
return SmartRequest("get_child_device_list") return SmartRequest("get_child_device_list")
@staticmethod @staticmethod
def get_child_device_component_list() -> "SmartRequest": def get_child_device_component_list() -> SmartRequest:
"""Get child device component list.""" """Get child device component list."""
return SmartRequest("get_child_device_component_list") return SmartRequest("get_child_device_component_list")
@staticmethod @staticmethod
def get_wireless_scan_info( def get_wireless_scan_info(
params: Optional[GetRulesParams] = None, params: GetRulesParams | None = None,
) -> "SmartRequest": ) -> SmartRequest:
"""Get wireless scan info.""" """Get wireless scan info."""
return SmartRequest( return SmartRequest(
"get_wireless_scan_info", params or SmartRequest.GetRulesParams() "get_wireless_scan_info", params or SmartRequest.GetRulesParams()
) )
@staticmethod @staticmethod
def get_schedule_rules(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_schedule_rules(params: GetRulesParams | None = None) -> SmartRequest:
"""Get schedule rules.""" """Get schedule rules."""
return SmartRequest( return SmartRequest(
"get_schedule_rules", params or SmartRequest.GetScheduleRulesParams() "get_schedule_rules", params or SmartRequest.GetScheduleRulesParams()
) )
@staticmethod @staticmethod
def get_next_event(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_next_event(params: GetRulesParams | None = None) -> SmartRequest:
"""Get next scheduled event.""" """Get next scheduled event."""
return SmartRequest("get_next_event", params or SmartRequest.GetRulesParams()) return SmartRequest("get_next_event", params or SmartRequest.GetRulesParams())
@staticmethod @staticmethod
def schedule_info_list() -> List["SmartRequest"]: def schedule_info_list() -> list[SmartRequest]:
"""Get schedule info list.""" """Get schedule info list."""
return [ return [
SmartRequest.get_schedule_rules(), SmartRequest.get_schedule_rules(),
@ -213,38 +214,38 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def get_countdown_rules(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_countdown_rules(params: GetRulesParams | None = None) -> SmartRequest:
"""Get countdown rules.""" """Get countdown rules."""
return SmartRequest( return SmartRequest(
"get_countdown_rules", params or SmartRequest.GetRulesParams() "get_countdown_rules", params or SmartRequest.GetRulesParams()
) )
@staticmethod @staticmethod
def get_antitheft_rules(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_antitheft_rules(params: GetRulesParams | None = None) -> SmartRequest:
"""Get antitheft rules.""" """Get antitheft rules."""
return SmartRequest( return SmartRequest(
"get_antitheft_rules", params or SmartRequest.GetRulesParams() "get_antitheft_rules", params or SmartRequest.GetRulesParams()
) )
@staticmethod @staticmethod
def get_led_info(params: Optional[LedStatusParams] = None) -> "SmartRequest": def get_led_info(params: LedStatusParams | None = None) -> SmartRequest:
"""Get led info.""" """Get led info."""
return SmartRequest("get_led_info", params or SmartRequest.LedStatusParams()) return SmartRequest("get_led_info", params or SmartRequest.LedStatusParams())
@staticmethod @staticmethod
def get_auto_off_config(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_auto_off_config(params: GetRulesParams | None = None) -> SmartRequest:
"""Get auto off config.""" """Get auto off config."""
return SmartRequest( return SmartRequest(
"get_auto_off_config", params or SmartRequest.GetRulesParams() "get_auto_off_config", params or SmartRequest.GetRulesParams()
) )
@staticmethod @staticmethod
def get_delay_action_info() -> "SmartRequest": def get_delay_action_info() -> SmartRequest:
"""Get delay action info.""" """Get delay action info."""
return SmartRequest("get_delay_action_info") return SmartRequest("get_delay_action_info")
@staticmethod @staticmethod
def auto_off_list() -> List["SmartRequest"]: def auto_off_list() -> list[SmartRequest]:
"""Get energy usage.""" """Get energy usage."""
return [ return [
SmartRequest.get_auto_off_config(), SmartRequest.get_auto_off_config(),
@ -252,12 +253,12 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def get_energy_usage() -> "SmartRequest": def get_energy_usage() -> SmartRequest:
"""Get energy usage.""" """Get energy usage."""
return SmartRequest("get_energy_usage") return SmartRequest("get_energy_usage")
@staticmethod @staticmethod
def energy_monitoring_list() -> List["SmartRequest"]: def energy_monitoring_list() -> list[SmartRequest]:
"""Get energy usage.""" """Get energy usage."""
return [ return [
SmartRequest("get_energy_usage"), SmartRequest("get_energy_usage"),
@ -265,12 +266,12 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def get_current_power() -> "SmartRequest": def get_current_power() -> SmartRequest:
"""Get current power.""" """Get current power."""
return SmartRequest("get_current_power") return SmartRequest("get_current_power")
@staticmethod @staticmethod
def power_protection_list() -> List["SmartRequest"]: def power_protection_list() -> list[SmartRequest]:
"""Get power protection info list.""" """Get power protection info list."""
return [ return [
SmartRequest.get_current_power(), SmartRequest.get_current_power(),
@ -279,45 +280,45 @@ class SmartRequest:
] ]
@staticmethod @staticmethod
def get_preset_rules(params: Optional[GetRulesParams] = None) -> "SmartRequest": def get_preset_rules(params: GetRulesParams | None = None) -> SmartRequest:
"""Get preset rules.""" """Get preset rules."""
return SmartRequest("get_preset_rules", params or SmartRequest.GetRulesParams()) return SmartRequest("get_preset_rules", params or SmartRequest.GetRulesParams())
@staticmethod @staticmethod
def get_auto_light_info() -> "SmartRequest": def get_auto_light_info() -> SmartRequest:
"""Get auto light info.""" """Get auto light info."""
return SmartRequest("get_auto_light_info") return SmartRequest("get_auto_light_info")
@staticmethod @staticmethod
def get_dynamic_light_effect_rules( def get_dynamic_light_effect_rules(
params: Optional[GetRulesParams] = None, params: GetRulesParams | None = None,
) -> "SmartRequest": ) -> SmartRequest:
"""Get dynamic light effect rules.""" """Get dynamic light effect rules."""
return SmartRequest( return SmartRequest(
"get_dynamic_light_effect_rules", params or SmartRequest.GetRulesParams() "get_dynamic_light_effect_rules", params or SmartRequest.GetRulesParams()
) )
@staticmethod @staticmethod
def set_device_on(params: DeviceOnParams) -> "SmartRequest": def set_device_on(params: DeviceOnParams) -> SmartRequest:
"""Set device on state.""" """Set device on state."""
return SmartRequest("set_device_info", params) return SmartRequest("set_device_info", params)
@staticmethod @staticmethod
def set_light_info(params: LightInfoParams) -> "SmartRequest": def set_light_info(params: LightInfoParams) -> SmartRequest:
"""Set color temperature.""" """Set color temperature."""
return SmartRequest("set_device_info", params) return SmartRequest("set_device_info", params)
@staticmethod @staticmethod
def set_dynamic_light_effect_rule_enable( def set_dynamic_light_effect_rule_enable(
params: DynamicLightEffectParams, params: DynamicLightEffectParams,
) -> "SmartRequest": ) -> SmartRequest:
"""Enable dynamic light effect rule.""" """Enable dynamic light effect rule."""
return SmartRequest("set_dynamic_light_effect_rule_enable", params) return SmartRequest("set_dynamic_light_effect_rule_enable", params)
@staticmethod @staticmethod
def get_component_info_requests(component_nego_response) -> List["SmartRequest"]: def get_component_info_requests(component_nego_response) -> list[SmartRequest]:
"""Get a list of requests based on the component info response.""" """Get a list of requests based on the component info response."""
request_list: List["SmartRequest"] = [] request_list: list[SmartRequest] = []
for component in component_nego_response["component_list"]: for component in component_nego_response["component_list"]:
if ( if (
requests := get_component_requests( requests := get_component_requests(
@ -329,7 +330,7 @@ class SmartRequest:
@staticmethod @staticmethod
def _create_request_dict( def _create_request_dict(
smart_request: Union["SmartRequest", List["SmartRequest"]], smart_request: SmartRequest | list[SmartRequest],
) -> dict: ) -> dict:
"""Create request dict to be passed to SmartProtocol.query().""" """Create request dict to be passed to SmartProtocol.query()."""
if isinstance(smart_request, list): if isinstance(smart_request, list):

View File

@ -4,13 +4,15 @@ Based on the work of https://github.com/petretiandrea/plugp100
under compatible GNU GPL3 license. under compatible GNU GPL3 license.
""" """
from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import hashlib import hashlib
import logging import logging
import time import time
from enum import Enum, auto from enum import Enum, auto
from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, Optional, Tuple, cast from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, cast
from cryptography.hazmat.primitives import padding, serialization from cryptography.hazmat.primitives import padding, serialization
from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding
@ -92,19 +94,19 @@ class AesTransport(BaseTransport):
self._login_params = json_loads( self._login_params = json_loads(
base64.b64decode(self._credentials_hash.encode()).decode() # type: ignore[union-attr] base64.b64decode(self._credentials_hash.encode()).decode() # type: ignore[union-attr]
) )
self._default_credentials: Optional[Credentials] = None self._default_credentials: Credentials | None = None
self._http_client: HttpClient = HttpClient(config) self._http_client: HttpClient = HttpClient(config)
self._state = TransportState.HANDSHAKE_REQUIRED self._state = TransportState.HANDSHAKE_REQUIRED
self._encryption_session: Optional[AesEncyptionSession] = None self._encryption_session: AesEncyptionSession | None = None
self._session_expire_at: Optional[float] = None self._session_expire_at: float | None = None
self._session_cookie: Optional[Dict[str, str]] = None self._session_cookie: dict[str, str] | None = None
self._key_pair: Optional[KeyPair] = None self._key_pair: KeyPair | None = None
self._app_url = URL(f"http://{self._host}:{self._port}/app") self._app_url = URL(f"http://{self._host}:{self._port}/app")
self._token_url: Optional[URL] = None self._token_url: URL | None = None
_LOGGER.debug("Created AES transport for %s", self._host) _LOGGER.debug("Created AES transport for %s", self._host)
@ -118,14 +120,14 @@ class AesTransport(BaseTransport):
"""The hashed credentials used by the transport.""" """The hashed credentials used by the transport."""
return base64.b64encode(json_dumps(self._login_params).encode()).decode() return base64.b64encode(json_dumps(self._login_params).encode()).decode()
def _get_login_params(self, credentials: Credentials) -> Dict[str, str]: def _get_login_params(self, credentials: Credentials) -> dict[str, str]:
"""Get the login parameters based on the login_version.""" """Get the login parameters based on the login_version."""
un, pw = self.hash_credentials(self._login_version == 2, credentials) un, pw = self.hash_credentials(self._login_version == 2, credentials)
password_field_name = "password2" if self._login_version == 2 else "password" password_field_name = "password2" if self._login_version == 2 else "password"
return {password_field_name: pw, "username": un} return {password_field_name: pw, "username": un}
@staticmethod @staticmethod
def hash_credentials(login_v2: bool, credentials: Credentials) -> Tuple[str, str]: def hash_credentials(login_v2: bool, credentials: Credentials) -> tuple[str, str]:
"""Hash the credentials.""" """Hash the credentials."""
un = base64.b64encode(_sha1(credentials.username.encode()).encode()).decode() un = base64.b64encode(_sha1(credentials.username.encode()).encode()).decode()
if login_v2: if login_v2:
@ -148,7 +150,7 @@ class AesTransport(BaseTransport):
raise AuthenticationError(msg, error_code=error_code) raise AuthenticationError(msg, error_code=error_code)
raise DeviceError(msg, error_code=error_code) raise DeviceError(msg, error_code=error_code)
async def send_secure_passthrough(self, request: str) -> Dict[str, Any]: async def send_secure_passthrough(self, request: str) -> dict[str, Any]:
"""Send encrypted message as passthrough.""" """Send encrypted message as passthrough."""
if self._state is TransportState.ESTABLISHED and self._token_url: if self._state is TransportState.ESTABLISHED and self._token_url:
url = self._token_url url = self._token_url
@ -230,7 +232,7 @@ class AesTransport(BaseTransport):
ex, ex,
) from ex ) from ex
async def try_login(self, login_params: Dict[str, Any]) -> None: async def try_login(self, login_params: dict[str, Any]) -> None:
"""Try to login with supplied login_params.""" """Try to login with supplied login_params."""
login_request = { login_request = {
"method": "login_device", "method": "login_device",
@ -333,7 +335,7 @@ class AesTransport(BaseTransport):
or self._session_expire_at - time.time() <= 0 or self._session_expire_at - time.time() <= 0
) )
async def send(self, request: str) -> Dict[str, Any]: async def send(self, request: str) -> dict[str, Any]:
"""Send the request.""" """Send the request."""
if ( if (
self._state is TransportState.HANDSHAKE_REQUIRED self._state is TransportState.HANDSHAKE_REQUIRED

View File

@ -1,7 +1,9 @@
"""Module for Device base class.""" """Module for Device base class."""
from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List, NamedTuple, Optional from typing import NamedTuple, Optional
from .device import Device from .device import Device
@ -33,14 +35,14 @@ class BulbPreset(BaseModel):
brightness: int brightness: int
# These are not available for effect mode presets on light strips # These are not available for effect mode presets on light strips
hue: Optional[int] hue: Optional[int] # noqa: UP007
saturation: Optional[int] saturation: Optional[int] # noqa: UP007
color_temp: Optional[int] color_temp: Optional[int] # noqa: UP007
# Variables for effect mode presets # Variables for effect mode presets
custom: Optional[int] custom: Optional[int] # noqa: UP007
id: Optional[str] id: Optional[str] # noqa: UP007
mode: Optional[int] mode: Optional[int] # noqa: UP007
class Bulb(Device, ABC): class Bulb(Device, ABC):
@ -101,10 +103,10 @@ class Bulb(Device, ABC):
self, self,
hue: int, hue: int,
saturation: int, saturation: int,
value: Optional[int] = None, value: int | None = None,
*, *,
transition: Optional[int] = None, transition: int | None = None,
) -> Dict: ) -> dict:
"""Set new HSV. """Set new HSV.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -117,8 +119,8 @@ class Bulb(Device, ABC):
@abstractmethod @abstractmethod
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness=None, transition: Optional[int] = None self, temp: int, *, brightness=None, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -129,8 +131,8 @@ class Bulb(Device, ABC):
@abstractmethod @abstractmethod
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: Optional[int] = None self, brightness: int, *, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the brightness in percentage. """Set the brightness in percentage.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -141,5 +143,5 @@ class Bulb(Device, ABC):
@property @property
@abstractmethod @abstractmethod
def presets(self) -> List[BulbPreset]: def presets(self) -> list[BulbPreset]:
"""Return a list of available bulb setting presets.""" """Return a list of available bulb setting presets."""

View File

@ -1,5 +1,7 @@
"""python-kasa cli tool.""" """python-kasa cli tool."""
from __future__ import annotations
import ast import ast
import asyncio import asyncio
import json import json
@ -9,7 +11,7 @@ import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import singledispatch, wraps from functools import singledispatch, wraps
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any, Dict, cast from typing import Any, cast
import asyncclick as click import asyncclick as click
@ -320,7 +322,7 @@ async def cli(
global _do_echo global _do_echo
echo = _do_echo echo = _do_echo
logging_config: Dict[str, Any] = { logging_config: dict[str, Any] = {
"level": logging.DEBUG if debug > 0 else logging.INFO "level": logging.DEBUG if debug > 0 else logging.INFO
} }
try: try:

View File

@ -1,10 +1,12 @@
"""Module for Device base class.""" """Module for Device base class."""
from __future__ import annotations
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Mapping, Optional, Sequence, Union from typing import Any, Mapping, Sequence
from .credentials import Credentials from .credentials import Credentials
from .device_type import DeviceType from .device_type import DeviceType
@ -24,13 +26,13 @@ class WifiNetwork:
ssid: str ssid: str
key_type: int key_type: int
# These are available only on softaponboarding # These are available only on softaponboarding
cipher_type: Optional[int] = None cipher_type: int | None = None
bssid: Optional[str] = None bssid: str | None = None
channel: Optional[int] = None channel: int | None = None
rssi: Optional[int] = None rssi: int | None = None
# For SMART devices # For SMART devices
signal_level: Optional[int] = None signal_level: int | None = None
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -48,8 +50,8 @@ class Device(ABC):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
"""Create a new Device instance. """Create a new Device instance.
@ -68,19 +70,19 @@ class Device(ABC):
# checks in accessors. the @updated_required decorator does not ensure # checks in accessors. the @updated_required decorator does not ensure
# mypy that these are not accessed incorrectly. # mypy that these are not accessed incorrectly.
self._last_update: Any = None self._last_update: Any = None
self._discovery_info: Optional[Dict[str, Any]] = None self._discovery_info: dict[str, Any] | None = None
self.modules: Dict[str, Any] = {} self.modules: dict[str, Any] = {}
self._features: Dict[str, Feature] = {} self._features: dict[str, Feature] = {}
self._parent: Optional["Device"] = None self._parent: Device | None = None
self._children: Mapping[str, "Device"] = {} self._children: Mapping[str, Device] = {}
@staticmethod @staticmethod
async def connect( async def connect(
*, *,
host: Optional[str] = None, host: str | None = None,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
) -> "Device": ) -> Device:
"""Connect to a single device by the given hostname or device configuration. """Connect to a single device by the given hostname or device configuration.
This method avoids the UDP based discovery process and This method avoids the UDP based discovery process and
@ -120,11 +122,11 @@ class Device(ABC):
return not self.is_on return not self.is_on
@abstractmethod @abstractmethod
async def turn_on(self, **kwargs) -> Optional[Dict]: async def turn_on(self, **kwargs) -> dict | None:
"""Turn on the device.""" """Turn on the device."""
@abstractmethod @abstractmethod
async def turn_off(self, **kwargs) -> Optional[Dict]: async def turn_off(self, **kwargs) -> dict | None:
"""Turn off the device.""" """Turn off the device."""
@property @property
@ -147,12 +149,12 @@ class Device(ABC):
return self.protocol._transport._port return self.protocol._transport._port
@property @property
def credentials(self) -> Optional[Credentials]: def credentials(self) -> Credentials | None:
"""The device credentials.""" """The device credentials."""
return self.protocol._transport._credentials return self.protocol._transport._credentials
@property @property
def credentials_hash(self) -> Optional[str]: def credentials_hash(self) -> str | None:
"""The protocol specific hash of the credentials the device is using.""" """The protocol specific hash of the credentials the device is using."""
return self.protocol._transport.credentials_hash return self.protocol._transport.credentials_hash
@ -177,25 +179,25 @@ class Device(ABC):
@property @property
@abstractmethod @abstractmethod
def alias(self) -> Optional[str]: def alias(self) -> str | None:
"""Returns the device alias or nickname.""" """Returns the device alias or nickname."""
async def _raw_query(self, request: Union[str, Dict]) -> Any: async def _raw_query(self, request: str | dict) -> Any:
"""Send a raw query to the device.""" """Send a raw query to the device."""
return await self.protocol.query(request=request) return await self.protocol.query(request=request)
@property @property
def children(self) -> Sequence["Device"]: def children(self) -> Sequence[Device]:
"""Returns the child devices.""" """Returns the child devices."""
return list(self._children.values()) return list(self._children.values())
def get_child_device(self, id_: str) -> "Device": def get_child_device(self, id_: str) -> Device:
"""Return child device by its ID.""" """Return child device by its ID."""
return self._children[id_] return self._children[id_]
@property @property
@abstractmethod @abstractmethod
def sys_info(self) -> Dict[str, Any]: def sys_info(self) -> dict[str, Any]:
"""Returns the device info.""" """Returns the device info."""
@property @property
@ -248,7 +250,7 @@ class Device(ABC):
"""Return True if the device supports color changes.""" """Return True if the device supports color changes."""
return False return False
def get_plug_by_name(self, name: str) -> "Device": def get_plug_by_name(self, name: str) -> Device:
"""Return child device for the given name.""" """Return child device for the given name."""
for p in self.children: for p in self.children:
if p.alias == name: if p.alias == name:
@ -256,7 +258,7 @@ class Device(ABC):
raise KasaException(f"Device has no child with {name}") raise KasaException(f"Device has no child with {name}")
def get_plug_by_index(self, index: int) -> "Device": def get_plug_by_index(self, index: int) -> Device:
"""Return child device for the given index.""" """Return child device for the given index."""
if index + 1 > len(self.children) or index < 0: if index + 1 > len(self.children) or index < 0:
raise KasaException( raise KasaException(
@ -271,22 +273,22 @@ class Device(ABC):
@property @property
@abstractmethod @abstractmethod
def timezone(self) -> Dict: def timezone(self) -> dict:
"""Return the timezone and time_difference.""" """Return the timezone and time_difference."""
@property @property
@abstractmethod @abstractmethod
def hw_info(self) -> Dict: def hw_info(self) -> dict:
"""Return hardware info for the device.""" """Return hardware info for the device."""
@property @property
@abstractmethod @abstractmethod
def location(self) -> Dict: def location(self) -> dict:
"""Return the device location.""" """Return the device location."""
@property @property
@abstractmethod @abstractmethod
def rssi(self) -> Optional[int]: def rssi(self) -> int | None:
"""Return the rssi.""" """Return the rssi."""
@property @property
@ -305,12 +307,12 @@ class Device(ABC):
"""Return all the internal state data.""" """Return all the internal state data."""
@property @property
def state_information(self) -> Dict[str, Any]: def state_information(self) -> dict[str, Any]:
"""Return available features and their values.""" """Return available features and their values."""
return {feat.name: feat.value for feat in self._features.values()} return {feat.name: feat.value for feat in self._features.values()}
@property @property
def features(self) -> Dict[str, Feature]: def features(self) -> dict[str, Feature]:
"""Return the list of supported features.""" """Return the list of supported features."""
return self._features return self._features
@ -328,7 +330,7 @@ class Device(ABC):
@property @property
@abstractmethod @abstractmethod
def on_since(self) -> Optional[datetime]: def on_since(self) -> datetime | None:
"""Return the time that the device was turned on or None if turned off.""" """Return the time that the device was turned on or None if turned off."""
@abstractmethod @abstractmethod
@ -342,18 +344,18 @@ class Device(ABC):
@property @property
@abstractmethod @abstractmethod
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Get the emeter value for this month.""" """Get the emeter value for this month."""
@property @property
@abstractmethod @abstractmethod
def emeter_today(self) -> Union[Optional[float], Any]: def emeter_today(self) -> float | None | Any:
"""Get the emeter value for today.""" """Get the emeter value for today."""
# Return type of Any ensures consumers being shielded from the return # Return type of Any ensures consumers being shielded from the return
# type by @update_required are not affected. # type by @update_required are not affected.
@abstractmethod @abstractmethod
async def wifi_scan(self) -> List[WifiNetwork]: async def wifi_scan(self) -> list[WifiNetwork]:
"""Scan for available wifi networks.""" """Scan for available wifi networks."""
@abstractmethod @abstractmethod

View File

@ -1,8 +1,10 @@
"""Device creation via DeviceConfig.""" """Device creation via DeviceConfig."""
from __future__ import annotations
import logging import logging
import time import time
from typing import Any, Dict, Optional, Tuple, Type from typing import Any
from .aestransport import AesTransport from .aestransport import AesTransport
from .device import Device from .device import Device
@ -35,7 +37,7 @@ GET_SYSINFO_QUERY = {
} }
async def connect(*, host: Optional[str] = None, config: DeviceConfig) -> "Device": async def connect(*, host: str | None = None, config: DeviceConfig) -> Device:
"""Connect to a single device by the given hostname or device configuration. """Connect to a single device by the given hostname or device configuration.
This method avoids the UDP based discovery process and This method avoids the UDP based discovery process and
@ -72,7 +74,7 @@ async def connect(*, host: Optional[str] = None, config: DeviceConfig) -> "Devic
raise raise
async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device": async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> Device:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
if debug_enabled: if debug_enabled:
start_time = time.perf_counter() start_time = time.perf_counter()
@ -87,8 +89,8 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
) )
start_time = time.perf_counter() start_time = time.perf_counter()
device_class: Optional[Type[Device]] device_class: type[Device] | None
device: Optional[Device] = None device: Device | None = None
if isinstance(protocol, IotProtocol) and isinstance( if isinstance(protocol, IotProtocol) and isinstance(
protocol._transport, XorTransport protocol._transport, XorTransport
@ -115,13 +117,13 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
) )
def _get_device_type_from_sys_info(info: Dict[str, Any]) -> DeviceType: def _get_device_type_from_sys_info(info: dict[str, Any]) -> DeviceType:
"""Find SmartDevice subclass for device described by passed data.""" """Find SmartDevice subclass for device described by passed data."""
if "system" not in info or "get_sysinfo" not in info["system"]: if "system" not in info or "get_sysinfo" not in info["system"]:
raise KasaException("No 'system' or 'get_sysinfo' in response") raise KasaException("No 'system' or 'get_sysinfo' in response")
sysinfo: Dict[str, Any] = info["system"]["get_sysinfo"] sysinfo: dict[str, Any] = info["system"]["get_sysinfo"]
type_: Optional[str] = sysinfo.get("type", sysinfo.get("mic_type")) type_: str | None = sysinfo.get("type", sysinfo.get("mic_type"))
if type_ is None: if type_ is None:
raise KasaException("Unable to find the device type field!") raise KasaException("Unable to find the device type field!")
@ -143,7 +145,7 @@ def _get_device_type_from_sys_info(info: Dict[str, Any]) -> DeviceType:
raise UnsupportedDeviceError("Unknown device type: %s" % type_) raise UnsupportedDeviceError("Unknown device type: %s" % type_)
def get_device_class_from_sys_info(sysinfo: Dict[str, Any]) -> Type[IotDevice]: def get_device_class_from_sys_info(sysinfo: dict[str, Any]) -> type[IotDevice]:
"""Find SmartDevice subclass for device described by passed data.""" """Find SmartDevice subclass for device described by passed data."""
TYPE_TO_CLASS = { TYPE_TO_CLASS = {
DeviceType.Bulb: IotBulb, DeviceType.Bulb: IotBulb,
@ -156,9 +158,9 @@ def get_device_class_from_sys_info(sysinfo: Dict[str, Any]) -> Type[IotDevice]:
return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)] return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)]
def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]: def get_device_class_from_family(device_type: str) -> type[Device] | None:
"""Return the device class from the type name.""" """Return the device class from the type name."""
supported_device_types: Dict[str, Type[Device]] = { supported_device_types: dict[str, type[Device]] = {
"SMART.TAPOPLUG": SmartDevice, "SMART.TAPOPLUG": SmartDevice,
"SMART.TAPOBULB": SmartBulb, "SMART.TAPOBULB": SmartBulb,
"SMART.TAPOSWITCH": SmartBulb, "SMART.TAPOSWITCH": SmartBulb,
@ -173,14 +175,14 @@ def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]:
def get_protocol( def get_protocol(
config: DeviceConfig, config: DeviceConfig,
) -> Optional[BaseProtocol]: ) -> BaseProtocol | None:
"""Return the protocol from the connection name.""" """Return the protocol from the connection name."""
protocol_name = config.connection_type.device_family.value.split(".")[0] protocol_name = config.connection_type.device_family.value.split(".")[0]
protocol_transport_key = ( protocol_transport_key = (
protocol_name + "." + config.connection_type.encryption_type.value protocol_name + "." + config.connection_type.encryption_type.value
) )
supported_device_protocols: Dict[ supported_device_protocols: dict[
str, Tuple[Type[BaseProtocol], Type[BaseTransport]] str, tuple[type[BaseProtocol], type[BaseTransport]]
] = { ] = {
"IOT.XOR": (IotProtocol, XorTransport), "IOT.XOR": (IotProtocol, XorTransport),
"IOT.KLAP": (IotProtocol, KlapTransport), "IOT.KLAP": (IotProtocol, KlapTransport),

View File

@ -1,5 +1,7 @@
"""TP-Link device types.""" """TP-Link device types."""
from __future__ import annotations
from enum import Enum from enum import Enum
@ -20,7 +22,7 @@ class DeviceType(Enum):
Unknown = "unknown" Unknown = "unknown"
@staticmethod @staticmethod
def from_value(name: str) -> "DeviceType": def from_value(name: str) -> DeviceType:
"""Return device type from string value.""" """Return device type from string value."""
for device_type in DeviceType: for device_type in DeviceType:
if device_type.value == name: if device_type.value == name:

View File

@ -1,5 +1,11 @@
"""Module for holding connection parameters.""" """Module for holding connection parameters.
Note that this module does not work with from __future__ import annotations
due to it's use of type returned by fields() which becomes a string with the import.
https://bugs.python.org/issue39442
"""
# ruff: noqa: FA100
import logging import logging
from dataclasses import asdict, dataclass, field, fields, is_dataclass from dataclasses import asdict, dataclass, field, fields, is_dataclass
from enum import Enum from enum import Enum

View File

@ -1,11 +1,13 @@
"""Discovery module for TP-Link Smart Home devices.""" """Discovery module for TP-Link Smart Home devices."""
from __future__ import annotations
import asyncio import asyncio
import binascii import binascii
import ipaddress import ipaddress
import logging import logging
import socket import socket
from typing import Awaitable, Callable, Dict, List, Optional, Set, Type, cast from typing import Awaitable, Callable, Dict, Optional, Type, cast
# When support for cpython older than 3.11 is dropped # When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout # async_timeout can be replaced with asyncio.timeout
@ -38,6 +40,7 @@ _LOGGER = logging.getLogger(__name__)
OnDiscoveredCallable = Callable[[Device], Awaitable[None]] OnDiscoveredCallable = Callable[[Device], Awaitable[None]]
OnUnsupportedCallable = Callable[[UnsupportedDeviceError], Awaitable[None]]
DeviceDict = Dict[str, Device] DeviceDict = Dict[str, Device]
@ -54,17 +57,15 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
def __init__( def __init__(
self, self,
*, *,
on_discovered: Optional[OnDiscoveredCallable] = None, on_discovered: OnDiscoveredCallable | None = None,
target: str = "255.255.255.255", target: str = "255.255.255.255",
discovery_packets: int = 3, discovery_packets: int = 3,
discovery_timeout: int = 5, discovery_timeout: int = 5,
interface: Optional[str] = None, interface: str | None = None,
on_unsupported: Optional[ on_unsupported: OnUnsupportedCallable | None = None,
Callable[[UnsupportedDeviceError], Awaitable[None]] port: int | None = None,
] = None, credentials: Credentials | None = None,
port: Optional[int] = None, timeout: int | None = None,
credentials: Optional[Credentials] = None,
timeout: Optional[int] = None,
) -> None: ) -> None:
self.transport = None self.transport = None
self.discovery_packets = discovery_packets self.discovery_packets = discovery_packets
@ -78,15 +79,15 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
self.target_2 = (target, Discover.DISCOVERY_PORT_2) self.target_2 = (target, Discover.DISCOVERY_PORT_2)
self.discovered_devices = {} self.discovered_devices = {}
self.unsupported_device_exceptions: Dict = {} self.unsupported_device_exceptions: dict = {}
self.invalid_device_exceptions: Dict = {} self.invalid_device_exceptions: dict = {}
self.on_unsupported = on_unsupported self.on_unsupported = on_unsupported
self.credentials = credentials self.credentials = credentials
self.timeout = timeout self.timeout = timeout
self.discovery_timeout = discovery_timeout self.discovery_timeout = discovery_timeout
self.seen_hosts: Set[str] = set() self.seen_hosts: set[str] = set()
self.discover_task: Optional[asyncio.Task] = None self.discover_task: asyncio.Task | None = None
self.callback_tasks: List[asyncio.Task] = [] self.callback_tasks: list[asyncio.Task] = []
self.target_discovered: bool = False self.target_discovered: bool = False
self._started_event = asyncio.Event() self._started_event = asyncio.Event()
@ -148,7 +149,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
return return
self.seen_hosts.add(ip) self.seen_hosts.add(ip)
device: Optional[Device] = None device: Device | None = None
config = DeviceConfig(host=ip, port_override=self.port) config = DeviceConfig(host=ip, port_override=self.port)
if self.credentials: if self.credentials:
@ -328,9 +329,9 @@ class Discover:
host: str, host: str,
*, *,
discovery_timeout: int = 5, discovery_timeout: int = 5,
port: Optional[int] = None, port: int | None = None,
timeout: Optional[int] = None, timeout: int | None = None,
credentials: Optional[Credentials] = None, credentials: Credentials | None = None,
) -> Device: ) -> Device:
"""Discover a single device by the given IP address. """Discover a single device by the given IP address.
@ -403,7 +404,7 @@ class Discover:
raise TimeoutError(f"Timed out getting discovery response for {host}") raise TimeoutError(f"Timed out getting discovery response for {host}")
@staticmethod @staticmethod
def _get_device_class(info: dict) -> Type[Device]: def _get_device_class(info: dict) -> type[Device]:
"""Find SmartDevice subclass for device described by passed data.""" """Find SmartDevice subclass for device described by passed data."""
if "result" in info: if "result" in info:
discovery_result = DiscoveryResult(**info["result"]) discovery_result = DiscoveryResult(**info["result"])
@ -502,17 +503,18 @@ class Discover:
return device return device
class EncryptionScheme(BaseModel):
"""Base model for encryption scheme of discovery result."""
is_support_https: bool
encrypt_type: str
http_port: int
lv: Optional[int] = None # noqa: UP007
class DiscoveryResult(BaseModel): class DiscoveryResult(BaseModel):
"""Base model for discovery result.""" """Base model for discovery result."""
class EncryptionScheme(BaseModel):
"""Base model for encryption scheme of discovery result."""
is_support_https: bool
encrypt_type: str
http_port: int
lv: Optional[int] = None
device_type: str device_type: str
device_model: str device_model: str
ip: str ip: str
@ -520,11 +522,11 @@ class DiscoveryResult(BaseModel):
mgt_encrypt_schm: EncryptionScheme mgt_encrypt_schm: EncryptionScheme
device_id: str device_id: str
hw_ver: Optional[str] = None hw_ver: Optional[str] = None # noqa: UP007
owner: Optional[str] = None owner: Optional[str] = None # noqa: UP007
is_support_iot_cloud: Optional[bool] = None is_support_iot_cloud: Optional[bool] = None # noqa: UP007
obd_src: Optional[str] = None obd_src: Optional[str] = None # noqa: UP007
factory_default: Optional[bool] = None factory_default: Optional[bool] = None # noqa: UP007
def get_dict(self) -> dict: def get_dict(self) -> dict:
"""Return a dict for this discovery result. """Return a dict for this discovery result.

View File

@ -1,6 +1,8 @@
"""Module for light strip effects (LB*, KL*, KB*).""" """Module for light strip effects (LB*, KL*, KB*)."""
from typing import List, cast from __future__ import annotations
from typing import cast
EFFECT_AURORA = { EFFECT_AURORA = {
"custom": 0, "custom": 0,
@ -292,5 +294,5 @@ EFFECTS_LIST_V1 = [
EFFECT_VALENTINES, EFFECT_VALENTINES,
] ]
EFFECT_NAMES_V1: List[str] = [cast(str, effect["name"]) for effect in EFFECTS_LIST_V1] EFFECT_NAMES_V1: list[str] = [cast(str, effect["name"]) for effect in EFFECTS_LIST_V1]
EFFECT_MAPPING_V1 = {effect["name"]: effect for effect in EFFECTS_LIST_V1} EFFECT_MAPPING_V1 = {effect["name"]: effect for effect in EFFECTS_LIST_V1}

View File

@ -1,7 +1,8 @@
"""Module for emeter container.""" """Module for emeter container."""
from __future__ import annotations
import logging import logging
from typing import Optional
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -17,7 +18,7 @@ class EmeterStatus(dict):
""" """
@property @property
def voltage(self) -> Optional[float]: def voltage(self) -> float | None:
"""Return voltage in V.""" """Return voltage in V."""
try: try:
return self["voltage"] return self["voltage"]
@ -25,7 +26,7 @@ class EmeterStatus(dict):
return None return None
@property @property
def power(self) -> Optional[float]: def power(self) -> float | None:
"""Return power in W.""" """Return power in W."""
try: try:
return self["power"] return self["power"]
@ -33,7 +34,7 @@ class EmeterStatus(dict):
return None return None
@property @property
def current(self) -> Optional[float]: def current(self) -> float | None:
"""Return current in A.""" """Return current in A."""
try: try:
return self["current"] return self["current"]
@ -41,7 +42,7 @@ class EmeterStatus(dict):
return None return None
@property @property
def total(self) -> Optional[float]: def total(self) -> float | None:
"""Return total in kWh.""" """Return total in kWh."""
try: try:
return self["total"] return self["total"]

View File

@ -1,8 +1,10 @@
"""python-kasa exceptions.""" """python-kasa exceptions."""
from __future__ import annotations
from asyncio import TimeoutError as _asyncioTimeoutError from asyncio import TimeoutError as _asyncioTimeoutError
from enum import IntEnum from enum import IntEnum
from typing import Any, Optional from typing import Any
class KasaException(Exception): class KasaException(Exception):
@ -35,7 +37,7 @@ class DeviceError(KasaException):
"""Base exception for device errors.""" """Base exception for device errors."""
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self.error_code: Optional["SmartErrorCode"] = kwargs.get("error_code", None) self.error_code: SmartErrorCode | None = kwargs.get("error_code", None)
super().__init__(*args) super().__init__(*args)
def __repr__(self): def __repr__(self):

View File

@ -1,8 +1,10 @@
"""Generic interface for defining device features.""" """Generic interface for defining device features."""
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, auto from enum import Enum, auto
from typing import TYPE_CHECKING, Any, Callable, Optional, Union from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING: if TYPE_CHECKING:
from .device import Device from .device import Device
@ -23,17 +25,17 @@ class Feature:
"""Feature defines a generic interface for device features.""" """Feature defines a generic interface for device features."""
#: Device instance required for getting and setting values #: Device instance required for getting and setting values
device: "Device" device: Device
#: User-friendly short description #: User-friendly short description
name: str name: str
#: Name of the property that allows accessing the value #: Name of the property that allows accessing the value
attribute_getter: Union[str, Callable] attribute_getter: str | Callable
#: Name of the method that allows changing the value #: Name of the method that allows changing the value
attribute_setter: Optional[str] = None attribute_setter: str | None = None
#: Container storing the data, this overrides 'device' for getters #: Container storing the data, this overrides 'device' for getters
container: Any = None container: Any = None
#: Icon suggestion #: Icon suggestion
icon: Optional[str] = None icon: str | None = None
#: Type of the feature #: Type of the feature
type: FeatureType = FeatureType.Sensor type: FeatureType = FeatureType.Sensor
@ -44,7 +46,7 @@ class Feature:
maximum_value: int = 2**16 # Arbitrary max maximum_value: int = 2**16 # Arbitrary max
#: Attribute containing the name of the range getter property. #: Attribute containing the name of the range getter property.
#: If set, this property will be used to set *minimum_value* and *maximum_value*. #: If set, this property will be used to set *minimum_value* and *maximum_value*.
range_getter: Optional[str] = None range_getter: str | None = None
def __post_init__(self): def __post_init__(self):
"""Handle late-binding of members.""" """Handle late-binding of members."""

View File

@ -1,8 +1,10 @@
"""Module for HttpClientSession class.""" """Module for HttpClientSession class."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any, Dict, Optional, Tuple, Union from typing import Any, Dict
import aiohttp import aiohttp
from yarl import URL from yarl import URL
@ -48,12 +50,12 @@ class HttpClient:
self, self,
url: URL, url: URL,
*, *,
params: Optional[Dict[str, Any]] = None, params: dict[str, Any] | None = None,
data: Optional[bytes] = None, data: bytes | None = None,
json: Optional[Union[Dict, Any]] = None, json: dict | Any | None = None,
headers: Optional[Dict[str, str]] = None, headers: dict[str, str] | None = None,
cookies_dict: Optional[Dict[str, str]] = None, cookies_dict: dict[str, str] | None = None,
) -> Tuple[int, Optional[Union[Dict, bytes]]]: ) -> tuple[int, dict | bytes | None]:
"""Send an http post request to the device. """Send an http post request to the device.
If the request is provided via the json parameter json will be returned. If the request is provided via the json parameter json will be returned.
@ -103,7 +105,7 @@ class HttpClient:
return resp.status, response_data return resp.status, response_data
def get_cookie(self, cookie_name: str) -> Optional[str]: def get_cookie(self, cookie_name: str) -> str | None:
"""Return the cookie with cookie_name.""" """Return the cookie with cookie_name."""
if cookie := self.client.cookie_jar.filter_cookies(self._last_url).get( if cookie := self.client.cookie_jar.filter_cookies(self._last_url).get(
cookie_name cookie_name

View File

@ -1,9 +1,11 @@
"""Module for bulbs (LB*, KL*, KB*).""" """Module for bulbs (LB*, KL*, KB*)."""
from __future__ import annotations
import logging import logging
import re import re
from enum import Enum from enum import Enum
from typing import Dict, List, Optional, cast from typing import Optional, cast
try: try:
from pydantic.v1 import BaseModel, Field, root_validator from pydantic.v1 import BaseModel, Field, root_validator
@ -40,7 +42,7 @@ class TurnOnBehavior(BaseModel):
""" """
#: Index of preset to use, or ``None`` for the last known state. #: Index of preset to use, or ``None`` for the last known state.
preset: Optional[int] = Field(alias="index", default=None) preset: Optional[int] = Field(alias="index", default=None) # noqa: UP007
#: Wanted behavior #: Wanted behavior
mode: BehaviorMode mode: BehaviorMode
@ -193,8 +195,8 @@ class IotBulb(IotDevice, Bulb):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Bulb self._device_type = DeviceType.Bulb
@ -275,7 +277,7 @@ class IotBulb(IotDevice, Bulb):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def light_state(self) -> Dict[str, str]: def light_state(self) -> dict[str, str]:
"""Query the light state.""" """Query the light state."""
light_state = self.sys_info["light_state"] light_state = self.sys_info["light_state"]
if light_state is None: if light_state is None:
@ -298,7 +300,7 @@ class IotBulb(IotDevice, Bulb):
"""Return True if the device supports effects.""" """Return True if the device supports effects."""
return "lighting_effect_state" in self.sys_info return "lighting_effect_state" in self.sys_info
async def get_light_details(self) -> Dict[str, int]: async def get_light_details(self) -> dict[str, int]:
"""Return light details. """Return light details.
Example:: Example::
@ -325,14 +327,14 @@ class IotBulb(IotDevice, Bulb):
self.LIGHT_SERVICE, "set_default_behavior", behavior.dict(by_alias=True) self.LIGHT_SERVICE, "set_default_behavior", behavior.dict(by_alias=True)
) )
async def get_light_state(self) -> Dict[str, Dict]: async def get_light_state(self) -> dict[str, dict]:
"""Query the light state.""" """Query the light state."""
# TODO: add warning and refer to use light.state? # TODO: add warning and refer to use light.state?
return await self._query_helper(self.LIGHT_SERVICE, "get_light_state") return await self._query_helper(self.LIGHT_SERVICE, "get_light_state")
async def set_light_state( async def set_light_state(
self, state: Dict, *, transition: Optional[int] = None self, state: dict, *, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the light state.""" """Set the light state."""
if transition is not None: if transition is not None:
state["transition_period"] = transition state["transition_period"] = transition
@ -378,10 +380,10 @@ class IotBulb(IotDevice, Bulb):
self, self,
hue: int, hue: int,
saturation: int, saturation: int,
value: Optional[int] = None, value: int | None = None,
*, *,
transition: Optional[int] = None, transition: int | None = None,
) -> Dict: ) -> dict:
"""Set new HSV. """Set new HSV.
:param int hue: hue in degrees :param int hue: hue in degrees
@ -424,8 +426,8 @@ class IotBulb(IotDevice, Bulb):
@requires_update @requires_update
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness=None, transition: Optional[int] = None self, temp: int, *, brightness=None, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
:param int temp: The new color temperature, in Kelvin :param int temp: The new color temperature, in Kelvin
@ -460,8 +462,8 @@ class IotBulb(IotDevice, Bulb):
@requires_update @requires_update
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: Optional[int] = None self, brightness: int, *, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the brightness in percentage. """Set the brightness in percentage.
:param int brightness: brightness in percent :param int brightness: brightness in percent
@ -482,14 +484,14 @@ class IotBulb(IotDevice, Bulb):
light_state = self.light_state light_state = self.light_state
return bool(light_state["on_off"]) return bool(light_state["on_off"])
async def turn_off(self, *, transition: Optional[int] = None, **kwargs) -> Dict: async def turn_off(self, *, transition: int | None = None, **kwargs) -> dict:
"""Turn the bulb off. """Turn the bulb off.
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
""" """
return await self.set_light_state({"on_off": 0}, transition=transition) return await self.set_light_state({"on_off": 0}, transition=transition)
async def turn_on(self, *, transition: Optional[int] = None, **kwargs) -> Dict: async def turn_on(self, *, transition: int | None = None, **kwargs) -> dict:
"""Turn the bulb on. """Turn the bulb on.
:param int transition: transition in milliseconds. :param int transition: transition in milliseconds.
@ -513,7 +515,7 @@ class IotBulb(IotDevice, Bulb):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def presets(self) -> List[BulbPreset]: def presets(self) -> list[BulbPreset]:
"""Return a list of available bulb setting presets.""" """Return a list of available bulb setting presets."""
return [BulbPreset(**vals) for vals in self.sys_info["preferred_state"]] return [BulbPreset(**vals) for vals in self.sys_info["preferred_state"]]

View File

@ -12,12 +12,14 @@ You may obtain a copy of the license at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
""" """
from __future__ import annotations
import collections.abc import collections.abc
import functools import functools
import inspect import inspect
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Mapping, Optional, Sequence, Set from typing import Any, Mapping, Sequence
from ..device import Device, WifiNetwork from ..device import Device, WifiNetwork
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -66,7 +68,7 @@ def requires_update(f):
@functools.lru_cache @functools.lru_cache
def _parse_features(features: str) -> Set[str]: def _parse_features(features: str) -> set[str]:
"""Parse features string.""" """Parse features string."""
return set(features.split(":")) return set(features.split(":"))
@ -177,19 +179,19 @@ class IotDevice(Device):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
"""Create a new IotDevice instance.""" """Create a new IotDevice instance."""
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
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._supported_modules: Optional[Dict[str, IotModule]] = None self._supported_modules: dict[str, IotModule] | None = None
self._legacy_features: Set[str] = set() self._legacy_features: set[str] = set()
self._children: Mapping[str, "IotDevice"] = {} self._children: Mapping[str, IotDevice] = {}
@property @property
def children(self) -> Sequence["IotDevice"]: def children(self) -> Sequence[IotDevice]:
"""Return list of children.""" """Return list of children."""
return list(self._children.values()) return list(self._children.values())
@ -203,9 +205,9 @@ class IotDevice(Device):
self.modules[name] = module self.modules[name] = module
def _create_request( def _create_request(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None self, target: str, cmd: str, arg: dict | None = None, child_ids=None
): ):
request: Dict[str, Any] = {target: {cmd: arg}} request: dict[str, Any] = {target: {cmd: arg}}
if child_ids is not None: if child_ids is not None:
request = {"context": {"child_ids": child_ids}, target: {cmd: arg}} request = {"context": {"child_ids": child_ids}, target: {cmd: arg}}
@ -219,7 +221,7 @@ class IotDevice(Device):
raise KasaException("update() required prior accessing emeter") raise KasaException("update() required prior accessing emeter")
async def _query_helper( async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None self, target: str, cmd: str, arg: dict | None = None, child_ids=None
) -> Any: ) -> Any:
"""Query device, return results or raise an exception. """Query device, return results or raise an exception.
@ -256,13 +258,13 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def features(self) -> Dict[str, Feature]: def features(self) -> dict[str, Feature]:
"""Return a set of features that the device supports.""" """Return a set of features that the device supports."""
return self._features return self._features
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def supported_modules(self) -> List[str]: def supported_modules(self) -> list[str]:
"""Return a set of modules supported by the device.""" """Return a set of modules supported by the device."""
# TODO: this should rather be called `features`, but we don't want to break # TODO: this should rather be called `features`, but we don't want to break
# the API now. Maybe just deprecate it and point the users to use this? # the API now. Maybe just deprecate it and point the users to use this?
@ -274,7 +276,7 @@ class IotDevice(Device):
"""Return True if device has an energy meter.""" """Return True if device has an energy meter."""
return "ENE" in self._legacy_features return "ENE" in self._legacy_features
async def get_sys_info(self) -> Dict[str, Any]: async def get_sys_info(self) -> dict[str, Any]:
"""Retrieve system information.""" """Retrieve system information."""
return await self._query_helper("system", "get_sysinfo") return await self._query_helper("system", "get_sysinfo")
@ -363,12 +365,12 @@ class IotDevice(Device):
# responses on top of it so we remember # responses on top of it so we remember
# which modules are not supported, otherwise # which modules are not supported, otherwise
# every other update will query for them # every other update will query for them
update: Dict = self._last_update.copy() if self._last_update else {} update: dict = self._last_update.copy() if self._last_update else {}
for response in responses: for response in responses:
update = {**update, **response} update = {**update, **response}
self._last_update = update self._last_update = update
def update_from_discover_info(self, info: Dict[str, Any]) -> None: def update_from_discover_info(self, info: dict[str, Any]) -> None:
"""Update state from info from the discover call.""" """Update state from info from the discover call."""
self._discovery_info = info self._discovery_info = info
if "system" in info and (sys_info := info["system"].get("get_sysinfo")): if "system" in info and (sys_info := info["system"].get("get_sysinfo")):
@ -380,7 +382,7 @@ class IotDevice(Device):
# by the requires_update decorator # by the requires_update decorator
self._set_sys_info(info) self._set_sys_info(info)
def _set_sys_info(self, sys_info: Dict[str, Any]) -> None: def _set_sys_info(self, sys_info: dict[str, Any]) -> None:
"""Set sys_info.""" """Set sys_info."""
self._sys_info = sys_info self._sys_info = sys_info
if features := sys_info.get("feature"): if features := sys_info.get("feature"):
@ -388,7 +390,7 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def sys_info(self) -> Dict[str, Any]: def sys_info(self) -> dict[str, Any]:
""" """
Return system information. Return system information.
@ -405,7 +407,7 @@ class IotDevice(Device):
return str(sys_info["model"]) return str(sys_info["model"])
@property # type: ignore @property # type: ignore
def alias(self) -> Optional[str]: def alias(self) -> str | None:
"""Return device name (alias).""" """Return device name (alias)."""
sys_info = self._sys_info sys_info = self._sys_info
return sys_info.get("alias") if sys_info else None return sys_info.get("alias") if sys_info else None
@ -422,18 +424,18 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def timezone(self) -> Dict: def timezone(self) -> dict:
"""Return the current timezone.""" """Return the current timezone."""
return self.modules["time"].timezone return self.modules["time"].timezone
async def get_time(self) -> Optional[datetime]: async def get_time(self) -> datetime | None:
"""Return current time from the device, if available.""" """Return current time from the device, if available."""
_LOGGER.warning( _LOGGER.warning(
"Use `time` property instead, this call will be removed in the future." "Use `time` property instead, this call will be removed in the future."
) )
return await self.modules["time"].get_time() return await self.modules["time"].get_time()
async def get_timezone(self) -> Dict: async def get_timezone(self) -> dict:
"""Return timezone information.""" """Return timezone information."""
_LOGGER.warning( _LOGGER.warning(
"Use `timezone` property instead, this call will be removed in the future." "Use `timezone` property instead, this call will be removed in the future."
@ -442,7 +444,7 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def hw_info(self) -> Dict: def hw_info(self) -> dict:
"""Return hardware information. """Return hardware information.
This returns just a selection of sysinfo keys that are related to hardware. This returns just a selection of sysinfo keys that are related to hardware.
@ -464,7 +466,7 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def location(self) -> Dict: def location(self) -> dict:
"""Return geographical location.""" """Return geographical location."""
sys_info = self._sys_info sys_info = self._sys_info
loc = {"latitude": None, "longitude": None} loc = {"latitude": None, "longitude": None}
@ -482,7 +484,7 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def rssi(self) -> Optional[int]: def rssi(self) -> int | None:
"""Return WiFi signal strength (rssi).""" """Return WiFi signal strength (rssi)."""
rssi = self._sys_info.get("rssi") rssi = self._sys_info.get("rssi")
return None if rssi is None else int(rssi) return None if rssi is None else int(rssi)
@ -528,21 +530,21 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def emeter_today(self) -> Optional[float]: def emeter_today(self) -> float | None:
"""Return today's energy consumption in kWh.""" """Return today's energy consumption in kWh."""
self._verify_emeter() self._verify_emeter()
return self.modules["emeter"].emeter_today return self.modules["emeter"].emeter_today
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Return this month's energy consumption in kWh.""" """Return this month's energy consumption in kWh."""
self._verify_emeter() self._verify_emeter()
return self.modules["emeter"].emeter_this_month return self.modules["emeter"].emeter_this_month
async def get_emeter_daily( async def get_emeter_daily(
self, year: Optional[int] = None, month: Optional[int] = None, kwh: bool = True self, year: int | None = None, month: int | None = None, kwh: bool = True
) -> Dict: ) -> dict:
"""Retrieve daily statistics for a given month. """Retrieve daily statistics for a given month.
:param year: year for which to retrieve statistics (default: this year) :param year: year for which to retrieve statistics (default: this year)
@ -556,8 +558,8 @@ class IotDevice(Device):
@requires_update @requires_update
async def get_emeter_monthly( async def get_emeter_monthly(
self, year: Optional[int] = None, kwh: bool = True self, year: int | None = None, kwh: bool = True
) -> Dict: ) -> dict:
"""Retrieve monthly statistics for a given year. """Retrieve monthly statistics for a given year.
:param year: year for which to retrieve statistics (default: this year) :param year: year for which to retrieve statistics (default: this year)
@ -568,7 +570,7 @@ class IotDevice(Device):
return await self.modules["emeter"].get_monthstat(year=year, kwh=kwh) return await self.modules["emeter"].get_monthstat(year=year, kwh=kwh)
@requires_update @requires_update
async def erase_emeter_stats(self) -> Dict: async def erase_emeter_stats(self) -> dict:
"""Erase energy meter statistics.""" """Erase energy meter statistics."""
self._verify_emeter() self._verify_emeter()
return await self.modules["emeter"].erase_stats() return await self.modules["emeter"].erase_stats()
@ -588,11 +590,11 @@ class IotDevice(Device):
""" """
await self._query_helper("system", "reboot", {"delay": delay}) await self._query_helper("system", "reboot", {"delay": delay})
async def turn_off(self, **kwargs) -> Dict: async def turn_off(self, **kwargs) -> dict:
"""Turn off the device.""" """Turn off the device."""
raise NotImplementedError("Device subclass needs to implement this.") raise NotImplementedError("Device subclass needs to implement this.")
async def turn_on(self, **kwargs) -> Optional[Dict]: async def turn_on(self, **kwargs) -> dict | None:
"""Turn device on.""" """Turn device on."""
raise NotImplementedError("Device subclass needs to implement this.") raise NotImplementedError("Device subclass needs to implement this.")
@ -604,7 +606,7 @@ class IotDevice(Device):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def on_since(self) -> Optional[datetime]: def on_since(self) -> datetime | None:
"""Return pretty-printed on-time, or None if not available.""" """Return pretty-printed on-time, or None if not available."""
if "on_time" not in self._sys_info: if "on_time" not in self._sys_info:
return None return None
@ -626,7 +628,7 @@ class IotDevice(Device):
""" """
return self.mac return self.mac
async def wifi_scan(self) -> List[WifiNetwork]: # noqa: D202 async def wifi_scan(self) -> list[WifiNetwork]: # noqa: D202
"""Scan for available wifi networks.""" """Scan for available wifi networks."""
async def _scan(target): async def _scan(target):

View File

@ -1,7 +1,9 @@
"""Module for dimmers (currently only HS220).""" """Module for dimmers (currently only HS220)."""
from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Any, Dict, Optional from typing import Any
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -72,8 +74,8 @@ class IotDimmer(IotPlug):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Dimmer self._device_type = DeviceType.Dimmer
@ -112,9 +114,7 @@ class IotDimmer(IotPlug):
return int(sys_info["brightness"]) return int(sys_info["brightness"])
@requires_update @requires_update
async def set_brightness( async def set_brightness(self, brightness: int, *, transition: int | None = None):
self, brightness: int, *, transition: Optional[int] = None
):
"""Set the new dimmer brightness level in percentage. """Set the new dimmer brightness level in percentage.
:param int transition: transition duration in milliseconds. :param int transition: transition duration in milliseconds.
@ -143,7 +143,7 @@ class IotDimmer(IotPlug):
self.DIMMER_SERVICE, "set_brightness", {"brightness": brightness} self.DIMMER_SERVICE, "set_brightness", {"brightness": brightness}
) )
async def turn_off(self, *, transition: Optional[int] = None, **kwargs): async def turn_off(self, *, transition: int | None = None, **kwargs):
"""Turn the bulb off. """Turn the bulb off.
:param int transition: transition duration in milliseconds. :param int transition: transition duration in milliseconds.
@ -154,7 +154,7 @@ class IotDimmer(IotPlug):
return await super().turn_off() return await super().turn_off()
@requires_update @requires_update
async def turn_on(self, *, transition: Optional[int] = None, **kwargs): async def turn_on(self, *, transition: int | None = None, **kwargs):
"""Turn the bulb on. """Turn the bulb on.
:param int transition: transition duration in milliseconds. :param int transition: transition duration in milliseconds.
@ -202,7 +202,7 @@ class IotDimmer(IotPlug):
@requires_update @requires_update
async def set_button_action( async def set_button_action(
self, action_type: ActionType, action: ButtonAction, index: Optional[int] = None self, action_type: ActionType, action: ButtonAction, index: int | None = None
): ):
"""Set action to perform on button click/hold. """Set action to perform on button click/hold.
@ -213,7 +213,7 @@ class IotDimmer(IotPlug):
""" """
action_type_setter = f"set_{action_type}" action_type_setter = f"set_{action_type}"
payload: Dict[str, Any] = {"mode": str(action)} payload: dict[str, Any] = {"mode": str(action)}
if index is not None: if index is not None:
payload["index"] = index payload["index"] = index

View File

@ -1,6 +1,6 @@
"""Module for light strips (KL430).""" """Module for light strips (KL430)."""
from typing import Dict, List, Optional from __future__ import annotations
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -49,8 +49,8 @@ class IotLightStrip(IotBulb):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.LightStrip self._device_type = DeviceType.LightStrip
@ -63,7 +63,7 @@ class IotLightStrip(IotBulb):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def effect(self) -> Dict: def effect(self) -> dict:
"""Return effect state. """Return effect state.
Example: Example:
@ -77,7 +77,7 @@ class IotLightStrip(IotBulb):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def effect_list(self) -> Optional[List[str]]: def effect_list(self) -> list[str] | None:
"""Return built-in effects list. """Return built-in effects list.
Example: Example:
@ -90,8 +90,8 @@ class IotLightStrip(IotBulb):
self, self,
effect: str, effect: str,
*, *,
brightness: Optional[int] = None, brightness: int | None = None,
transition: Optional[int] = None, transition: int | None = None,
) -> None: ) -> None:
"""Set an effect on the device. """Set an effect on the device.
@ -118,7 +118,7 @@ class IotLightStrip(IotBulb):
@requires_update @requires_update
async def set_custom_effect( async def set_custom_effect(
self, self,
effect_dict: Dict, effect_dict: dict,
) -> None: ) -> None:
"""Set a custom effect on the device. """Set a custom effect on the device.

View File

@ -1,7 +1,8 @@
"""Module for smart plugs (HS100, HS110, ..).""" """Module for smart plugs (HS100, HS110, ..)."""
from __future__ import annotations
import logging import logging
from typing import Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -47,8 +48,8 @@ class IotPlug(IotDevice):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.Plug self._device_type = DeviceType.Plug
@ -108,8 +109,8 @@ class IotWallSwitch(IotPlug):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self._device_type = DeviceType.WallSwitch self._device_type = DeviceType.WallSwitch

View File

@ -1,9 +1,11 @@
"""Module for multi-socket devices (HS300, HS107, KP303, ..).""" """Module for multi-socket devices (HS300, HS107, KP303, ..)."""
from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, DefaultDict, Dict, Optional from typing import Any
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -23,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
def merge_sums(dicts): def merge_sums(dicts):
"""Merge the sum of dicts.""" """Merge the sum of dicts."""
total_dict: DefaultDict[int, float] = defaultdict(lambda: 0.0) total_dict: defaultdict[int, float] = defaultdict(lambda: 0.0)
for sum_dict in dicts: for sum_dict in dicts:
for day, value in sum_dict.items(): for day, value in sum_dict.items():
total_dict[day] += value total_dict[day] += value
@ -86,8 +88,8 @@ class IotStrip(IotDevice):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[BaseProtocol] = None, protocol: BaseProtocol | None = None,
) -> None: ) -> None:
super().__init__(host=host, config=config, protocol=protocol) super().__init__(host=host, config=config, protocol=protocol)
self.emeter_type = "emeter" self.emeter_type = "emeter"
@ -137,7 +139,7 @@ class IotStrip(IotDevice):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def on_since(self) -> Optional[datetime]: def on_since(self) -> datetime | None:
"""Return the maximum on-time of all outlets.""" """Return the maximum on-time of all outlets."""
if self.is_off: if self.is_off:
return None return None
@ -170,8 +172,8 @@ class IotStrip(IotDevice):
@requires_update @requires_update
async def get_emeter_daily( async def get_emeter_daily(
self, year: Optional[int] = None, month: Optional[int] = None, kwh: bool = True self, year: int | None = None, month: int | None = None, kwh: bool = True
) -> Dict: ) -> dict:
"""Retrieve daily statistics for a given month. """Retrieve daily statistics for a given month.
:param year: year for which to retrieve statistics (default: this year) :param year: year for which to retrieve statistics (default: this year)
@ -186,8 +188,8 @@ class IotStrip(IotDevice):
@requires_update @requires_update
async def get_emeter_monthly( async def get_emeter_monthly(
self, year: Optional[int] = None, kwh: bool = True self, year: int | None = None, kwh: bool = True
) -> Dict: ) -> dict:
"""Retrieve monthly statistics for a given year. """Retrieve monthly statistics for a given year.
:param year: year for which to retrieve statistics (default: this year) :param year: year for which to retrieve statistics (default: this year)
@ -197,7 +199,7 @@ class IotStrip(IotDevice):
"get_emeter_monthly", {"year": year, "kwh": kwh} "get_emeter_monthly", {"year": year, "kwh": kwh}
) )
async def _async_get_emeter_sum(self, func: str, kwargs: Dict[str, Any]) -> Dict: async def _async_get_emeter_sum(self, func: str, kwargs: dict[str, Any]) -> dict:
"""Retreive emeter stats for a time period from children.""" """Retreive emeter stats for a time period from children."""
self._verify_emeter() self._verify_emeter()
return merge_sums( return merge_sums(
@ -212,13 +214,13 @@ class IotStrip(IotDevice):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Return this month's energy consumption in kWh.""" """Return this month's energy consumption in kWh."""
return sum(plug.emeter_this_month for plug in self.children) return sum(plug.emeter_this_month for plug in self.children)
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def emeter_today(self) -> Optional[float]: def emeter_today(self) -> float | None:
"""Return this month's energy consumption in kWh.""" """Return this month's energy consumption in kWh."""
return sum(plug.emeter_today for plug in self.children) return sum(plug.emeter_today for plug in self.children)
@ -243,7 +245,7 @@ class IotStripPlug(IotPlug):
The plug inherits (most of) the system information from the parent. The plug inherits (most of) the system information from the parent.
""" """
def __init__(self, host: str, parent: "IotStrip", child_id: str) -> None: def __init__(self, host: str, parent: IotStrip, child_id: str) -> None:
super().__init__(host) super().__init__(host)
self.parent = parent self.parent = parent
@ -262,16 +264,14 @@ class IotStripPlug(IotPlug):
""" """
await self._modular_update({}) await self._modular_update({})
def _create_emeter_request( def _create_emeter_request(self, year: int | None = None, month: int | None = None):
self, year: Optional[int] = None, month: Optional[int] = None
):
"""Create a request for requesting all emeter statistics at once.""" """Create a request for requesting all emeter statistics at once."""
if year is None: if year is None:
year = datetime.now().year year = datetime.now().year
if month is None: if month is None:
month = datetime.now().month month = datetime.now().month
req: Dict[str, Any] = {} req: dict[str, Any] = {}
merge(req, self._create_request("emeter", "get_realtime")) merge(req, self._create_request("emeter", "get_realtime"))
merge(req, self._create_request("emeter", "get_monthstat", {"year": year})) merge(req, self._create_request("emeter", "get_monthstat", {"year": year}))
@ -285,16 +285,16 @@ class IotStripPlug(IotPlug):
return req return req
def _create_request( def _create_request(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None self, target: str, cmd: str, arg: dict | None = None, child_ids=None
): ):
request: Dict[str, Any] = { request: dict[str, Any] = {
"context": {"child_ids": [self.child_id]}, "context": {"child_ids": [self.child_id]},
target: {cmd: arg}, target: {cmd: arg},
} }
return request return request
async def _query_helper( async def _query_helper(
self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None self, target: str, cmd: str, arg: dict | None = None, child_ids=None
) -> Any: ) -> Any:
"""Override query helper to include the child_ids.""" """Override query helper to include the child_ids."""
return await self.parent._query_helper( return await self.parent._query_helper(
@ -335,14 +335,14 @@ class IotStripPlug(IotPlug):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def next_action(self) -> Dict: def next_action(self) -> dict:
"""Return next scheduled(?) action.""" """Return next scheduled(?) action."""
info = self._get_child_info() info = self._get_child_info()
return info["next_action"] return info["next_action"]
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def on_since(self) -> Optional[datetime]: def on_since(self) -> datetime | None:
"""Return on-time, if available.""" """Return on-time, if available."""
if self.is_off: if self.is_off:
return None return None
@ -359,7 +359,7 @@ class IotStripPlug(IotPlug):
sys_info = self.parent.sys_info sys_info = self.parent.sys_info
return f"Socket for {sys_info['model']}" return f"Socket for {sys_info['model']}"
def _get_child_info(self) -> Dict: def _get_child_info(self) -> dict:
"""Return the subdevice information for this device.""" """Return the subdevice information for this device."""
for plug in self.parent.sys_info["children"]: for plug in self.parent.sys_info["children"]:
if plug["id"] == self.child_id: if plug["id"] == self.child_id:

View File

@ -1,7 +1,8 @@
"""Implementation of the emeter module.""" """Implementation of the emeter module."""
from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Union
from ...emeterstatus import EmeterStatus from ...emeterstatus import EmeterStatus
from .usage import Usage from .usage import Usage
@ -16,7 +17,7 @@ class Emeter(Usage):
return EmeterStatus(self.data["get_realtime"]) return EmeterStatus(self.data["get_realtime"])
@property @property
def emeter_today(self) -> Optional[float]: def emeter_today(self) -> float | None:
"""Return today's energy consumption in kWh.""" """Return today's energy consumption in kWh."""
raw_data = self.daily_data raw_data = self.daily_data
today = datetime.now().day today = datetime.now().day
@ -24,7 +25,7 @@ class Emeter(Usage):
return data.get(today) return data.get(today)
@property @property
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Return this month's energy consumption in kWh.""" """Return this month's energy consumption in kWh."""
raw_data = self.monthly_data raw_data = self.monthly_data
current_month = datetime.now().month current_month = datetime.now().month
@ -42,7 +43,7 @@ class Emeter(Usage):
"""Return real-time statistics.""" """Return real-time statistics."""
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. """Return daily stats for the given year & month.
The return value is a dictionary of {day: energy, ...}. The return value is a dictionary of {day: energy, ...}.
@ -51,7 +52,7 @@ class Emeter(Usage):
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. """Return monthly stats for the given year.
The return value is a dictionary of {month: energy, ...}. The return value is a dictionary of {month: energy, ...}.
@ -62,11 +63,11 @@ class Emeter(Usage):
def _convert_stat_data( def _convert_stat_data(
self, self,
data: List[Dict[str, Union[int, float]]], data: list[dict[str, int | float]],
entry_key: str, entry_key: str,
kwh: bool = True, kwh: bool = True,
key: Optional[int] = None, key: int | None = None,
) -> Dict[Union[int, float], Union[int, float]]: ) -> dict[int | float, int | float]:
"""Return emeter information keyed with the day/month. """Return emeter information keyed with the day/month.
The incoming data is a list of dictionaries:: The incoming data is a list of dictionaries::

View File

@ -1,7 +1,8 @@
"""Implementation of the motion detection (PIR) module found in some dimmers.""" """Implementation of the motion detection (PIR) module found in some dimmers."""
from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Optional
from ...exceptions import KasaException from ...exceptions import KasaException
from ..iotmodule import IotModule from ..iotmodule import IotModule
@ -43,7 +44,7 @@ class Motion(IotModule):
return await self.call("set_enable", {"enable": int(state)}) return await self.call("set_enable", {"enable": int(state)})
async def set_range( async def set_range(
self, *, range: Optional[Range] = None, custom_range: Optional[int] = None self, *, range: Range | None = None, custom_range: int | None = None
): ):
"""Set the range for the sensor. """Set the range for the sensor.

View File

@ -1,5 +1,7 @@
"""Base implementation for all rule-based modules.""" """Base implementation for all rule-based modules."""
from __future__ import annotations
import logging import logging
from enum import Enum from enum import Enum
from typing import Dict, List, Optional from typing import Dict, List, Optional
@ -37,20 +39,20 @@ class Rule(BaseModel):
id: str id: str
name: str name: str
enable: bool enable: bool
wday: List[int] wday: List[int] # noqa: UP006
repeat: bool repeat: bool
# start action # start action
sact: Optional[Action] sact: Optional[Action] # noqa: UP007
stime_opt: TimeOption stime_opt: TimeOption
smin: int smin: int
eact: Optional[Action] eact: Optional[Action] # noqa: UP007
etime_opt: TimeOption etime_opt: TimeOption
emin: int emin: int
# Only on bulbs # Only on bulbs
s_light: Optional[Dict] s_light: Optional[Dict] # noqa: UP006,UP007
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -65,7 +67,7 @@ class RuleModule(IotModule):
return merge(q, self.query_for_command("get_next_action")) return merge(q, self.query_for_command("get_next_action"))
@property @property
def rules(self) -> List[Rule]: def rules(self) -> list[Rule]:
"""Return the list of rules for the service.""" """Return the list of rules for the service."""
try: try:
return [ return [

View File

@ -1,7 +1,8 @@
"""Implementation of the usage interface.""" """Implementation of the usage interface."""
from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Dict
from ..iotmodule import IotModule, merge from ..iotmodule import IotModule, merge
@ -58,7 +59,7 @@ class Usage(IotModule):
return entry["time"] return entry["time"]
return None return None
async def get_raw_daystat(self, *, year=None, month=None) -> Dict: async def get_raw_daystat(self, *, year=None, month=None) -> dict:
"""Return raw daily stats for the given year & month.""" """Return raw daily stats for the given year & month."""
if year is None: if year is None:
year = datetime.now().year year = datetime.now().year
@ -67,14 +68,14 @@ class Usage(IotModule):
return await self.call("get_daystat", {"year": year, "month": month}) return await self.call("get_daystat", {"year": year, "month": month})
async def get_raw_monthstat(self, *, year=None) -> Dict: async def get_raw_monthstat(self, *, year=None) -> dict:
"""Return raw monthly stats for the given year.""" """Return raw monthly stats for the given year."""
if year is None: if year is None:
year = datetime.now().year year = datetime.now().year
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. """Return daily stats for the given year & month.
The return value is a dictionary of {day: time, ...}. The return value is a dictionary of {day: time, ...}.
@ -83,7 +84,7 @@ class Usage(IotModule):
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. """Return monthly stats for the given year.
The return value is a dictionary of {month: time, ...}. The return value is a dictionary of {month: time, ...}.
@ -96,7 +97,7 @@ class Usage(IotModule):
"""Erase all stats.""" """Erase all stats."""
return await self.call("erase_runtime_stat") return await self.call("erase_runtime_stat")
def _convert_stat_data(self, data, entry_key) -> Dict: def _convert_stat_data(self, data, entry_key) -> dict:
"""Return usage information keyed with the day/month. """Return usage information keyed with the day/month.
The incoming data is a list of dictionaries:: The incoming data is a list of dictionaries::

View File

@ -1,8 +1,9 @@
"""Module for the IOT legacy IOT KASA protocol.""" """Module for the IOT legacy IOT KASA protocol."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Dict, Optional, Union
from .deviceconfig import DeviceConfig from .deviceconfig import DeviceConfig
from .exceptions import ( from .exceptions import (
@ -34,7 +35,7 @@ class IotProtocol(BaseProtocol):
self._query_lock = asyncio.Lock() self._query_lock = asyncio.Lock()
async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async def query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Query the device retrying for retry_count on failure.""" """Query the device retrying for retry_count on failure."""
if isinstance(request, dict): if isinstance(request, dict):
request = json_dumps(request) request = json_dumps(request)
@ -43,7 +44,7 @@ class IotProtocol(BaseProtocol):
async with self._query_lock: async with self._query_lock:
return await self._query(request, retry_count) return await self._query(request, retry_count)
async def _query(self, request: str, retry_count: int = 3) -> Dict: async def _query(self, request: str, retry_count: int = 3) -> dict:
for retry in range(retry_count + 1): for retry in range(retry_count + 1):
try: try:
return await self._execute_query(request, retry) return await self._execute_query(request, retry)
@ -83,7 +84,7 @@ class IotProtocol(BaseProtocol):
# make mypy happy, this should never be reached.. # make mypy happy, this should never be reached..
raise KasaException("Query reached somehow to unreachable") raise KasaException("Query reached somehow to unreachable")
async def _execute_query(self, request: str, retry_count: int) -> Dict: async def _execute_query(self, request: str, retry_count: int) -> dict:
return await self._transport.send(request) return await self._transport.send(request)
async def close(self) -> None: async def close(self) -> None:
@ -94,11 +95,11 @@ class IotProtocol(BaseProtocol):
class _deprecated_TPLinkSmartHomeProtocol(IotProtocol): class _deprecated_TPLinkSmartHomeProtocol(IotProtocol):
def __init__( def __init__(
self, self,
host: Optional[str] = None, host: str | None = None,
*, *,
port: Optional[int] = None, port: int | None = None,
timeout: Optional[int] = None, timeout: int | None = None,
transport: Optional[BaseTransport] = None, transport: BaseTransport | None = None,
) -> None: ) -> None:
"""Create a protocol object.""" """Create a protocol object."""
if not host and not transport: if not host and not transport:

View File

@ -40,6 +40,8 @@ https://github.com/python-kasa/python-kasa/pull/117
""" """
from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import datetime import datetime
@ -49,7 +51,7 @@ import secrets
import struct import struct
import time import time
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any, Dict, Optional, Tuple, cast from typing import Any, cast
from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@ -99,7 +101,7 @@ class KlapTransport(BaseTransport):
super().__init__(config=config) super().__init__(config=config)
self._http_client = HttpClient(config) self._http_client = HttpClient(config)
self._local_seed: Optional[bytes] = None self._local_seed: bytes | None = None
if ( if (
not self._credentials or self._credentials.username is None not self._credentials or self._credentials.username is None
) and not self._credentials_hash: ) and not self._credentials_hash:
@ -109,16 +111,16 @@ class KlapTransport(BaseTransport):
self._local_auth_owner = self.generate_owner_hash(self._credentials).hex() self._local_auth_owner = self.generate_owner_hash(self._credentials).hex()
else: else:
self._local_auth_hash = base64.b64decode(self._credentials_hash.encode()) # type: ignore[union-attr] self._local_auth_hash = base64.b64decode(self._credentials_hash.encode()) # type: ignore[union-attr]
self._default_credentials_auth_hash: Dict[str, bytes] = {} self._default_credentials_auth_hash: dict[str, bytes] = {}
self._blank_auth_hash = None self._blank_auth_hash = None
self._handshake_lock = asyncio.Lock() self._handshake_lock = asyncio.Lock()
self._query_lock = asyncio.Lock() self._query_lock = asyncio.Lock()
self._handshake_done = False self._handshake_done = False
self._encryption_session: Optional[KlapEncryptionSession] = None self._encryption_session: KlapEncryptionSession | None = None
self._session_expire_at: Optional[float] = None self._session_expire_at: float | None = None
self._session_cookie: Optional[Dict[str, Any]] = None self._session_cookie: dict[str, Any] | None = None
_LOGGER.debug("Created KLAP transport for %s", self._host) _LOGGER.debug("Created KLAP transport for %s", self._host)
self._app_url = URL(f"http://{self._host}:{self._port}/app") self._app_url = URL(f"http://{self._host}:{self._port}/app")
@ -134,7 +136,7 @@ class KlapTransport(BaseTransport):
"""The hashed credentials used by the transport.""" """The hashed credentials used by the transport."""
return base64.b64encode(self._local_auth_hash).decode() return base64.b64encode(self._local_auth_hash).decode()
async def perform_handshake1(self) -> Tuple[bytes, bytes, bytes]: async def perform_handshake1(self) -> tuple[bytes, bytes, bytes]:
"""Perform handshake1.""" """Perform handshake1."""
local_seed: bytes = secrets.token_bytes(16) local_seed: bytes = secrets.token_bytes(16)
@ -240,7 +242,7 @@ class KlapTransport(BaseTransport):
async def perform_handshake2( async def perform_handshake2(
self, local_seed, remote_seed, auth_hash self, local_seed, remote_seed, auth_hash
) -> "KlapEncryptionSession": ) -> KlapEncryptionSession:
"""Perform handshake2.""" """Perform handshake2."""
# Handshake 2 has the following payload: # Handshake 2 has the following payload:
# sha256(serverBytes | authenticator) # sha256(serverBytes | authenticator)

View File

@ -1,8 +1,9 @@
"""Base class for all module implementations.""" """Base class for all module implementations."""
from __future__ import annotations
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict
from .device import Device from .device import Device
from .exceptions import KasaException from .exceptions import KasaException
@ -18,10 +19,10 @@ class Module(ABC):
executed during the regular update cycle. executed during the regular update cycle.
""" """
def __init__(self, device: "Device", module: str): def __init__(self, device: Device, module: str):
self._device = device self._device = device
self._module = module self._module = module
self._module_features: Dict[str, Feature] = {} self._module_features: dict[str, Feature] = {}
@abstractmethod @abstractmethod
def query(self): def query(self):

View File

@ -10,13 +10,14 @@ which are licensed under the Apache License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
""" """
from __future__ import annotations
import base64 import base64
import errno import errno
import hashlib import hashlib
import logging import logging
import struct import struct
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Tuple, Union
# When support for cpython older than 3.11 is dropped # When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout # async_timeout can be replaced with asyncio.timeout
@ -62,7 +63,7 @@ class BaseTransport(ABC):
"""The hashed credentials used by the transport.""" """The hashed credentials used by the transport."""
@abstractmethod @abstractmethod
async def send(self, request: str) -> Dict: async def send(self, request: str) -> dict:
"""Send a message to the device and return a response.""" """Send a message to the device and return a response."""
@abstractmethod @abstractmethod
@ -95,7 +96,7 @@ class BaseProtocol(ABC):
return self._transport._config return self._transport._config
@abstractmethod @abstractmethod
async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async def query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Query the device for the protocol. Abstract method to be overriden.""" """Query the device for the protocol. Abstract method to be overriden."""
@abstractmethod @abstractmethod
@ -103,7 +104,7 @@ class BaseProtocol(ABC):
"""Close the protocol. Abstract method to be overriden.""" """Close the protocol. Abstract method to be overriden."""
def get_default_credentials(tuple: Tuple[str, str]) -> Credentials: def get_default_credentials(tuple: tuple[str, str]) -> Credentials:
"""Return decoded default credentials.""" """Return decoded default credentials."""
un = base64.b64decode(tuple[0].encode()).decode() un = base64.b64decode(tuple[0].encode()).decode()
pw = base64.b64decode(tuple[1].encode()).decode() pw = base64.b64decode(tuple[1].encode()).decode()

View File

@ -1,6 +1,8 @@
"""Implementation of alarm module.""" """Implementation of alarm module."""
from typing import TYPE_CHECKING, Dict, List, Optional from __future__ import annotations
from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -14,14 +16,14 @@ class AlarmModule(SmartModule):
REQUIRED_COMPONENT = "alarm" REQUIRED_COMPONENT = "alarm"
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
return { return {
"get_alarm_configure": None, "get_alarm_configure": None,
"get_support_alarm_type_list": None, # This should be needed only once "get_support_alarm_type_list": None, # This should be needed only once
} }
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -59,7 +61,7 @@ class AlarmModule(SmartModule):
return self.data["get_alarm_configure"]["type"] return self.data["get_alarm_configure"]["type"]
@property @property
def alarm_sounds(self) -> List[str]: def alarm_sounds(self) -> list[str]:
"""Return list of available alarm sounds.""" """Return list of available alarm sounds."""
return self.data["get_support_alarm_type_list"]["alarm_type_list"] return self.data["get_support_alarm_type_list"]["alarm_type_list"]
@ -74,7 +76,7 @@ class AlarmModule(SmartModule):
return self._device.sys_info["in_alarm"] return self._device.sys_info["in_alarm"]
@property @property
def source(self) -> Optional[str]: def source(self) -> str | None:
"""Return the alarm cause.""" """Return the alarm cause."""
src = self._device.sys_info["in_alarm_source"] src = self._device.sys_info["in_alarm_source"]
return src if src else None return src if src else None

View File

@ -1,7 +1,9 @@
"""Implementation of auto off module.""" """Implementation of auto off module."""
from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Dict, Optional from typing import TYPE_CHECKING
from ...feature import Feature from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -16,7 +18,7 @@ class AutoOffModule(SmartModule):
REQUIRED_COMPONENT = "auto_off" REQUIRED_COMPONENT = "auto_off"
QUERY_GETTER_NAME = "get_auto_off_config" QUERY_GETTER_NAME = "get_auto_off_config"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -42,7 +44,7 @@ class AutoOffModule(SmartModule):
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
return {self.QUERY_GETTER_NAME: {"start_index": 0}} return {self.QUERY_GETTER_NAME: {"start_index": 0}}
@ -75,7 +77,7 @@ class AutoOffModule(SmartModule):
return self._device.sys_info["auto_off_status"] == "on" return self._device.sys_info["auto_off_status"] == "on"
@property @property
def auto_off_at(self) -> Optional[datetime]: def auto_off_at(self) -> datetime | None:
"""Return when the device will be turned off automatically.""" """Return when the device will be turned off automatically."""
if not self.is_timer_active: if not self.is_timer_active:
return None return None

View File

@ -1,5 +1,7 @@
"""Implementation of battery module.""" """Implementation of battery module."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
@ -15,7 +17,7 @@ class BatterySensor(SmartModule):
REQUIRED_COMPONENT = "battery_detect" REQUIRED_COMPONENT = "battery_detect"
QUERY_GETTER_NAME = "get_battery_detect_info" QUERY_GETTER_NAME = "get_battery_detect_info"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(

View File

@ -1,6 +1,8 @@
"""Implementation of brightness module.""" """Implementation of brightness module."""
from typing import TYPE_CHECKING, Dict from __future__ import annotations
from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -14,7 +16,7 @@ class Brightness(SmartModule):
REQUIRED_COMPONENT = "brightness" REQUIRED_COMPONENT = "brightness"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -29,7 +31,7 @@ class Brightness(SmartModule):
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
# Brightness is contained in the main device info response. # Brightness is contained in the main device info response.
return {} return {}

View File

@ -1,5 +1,7 @@
"""Implementation of cloud module.""" """Implementation of cloud module."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
@ -15,7 +17,7 @@ class CloudModule(SmartModule):
QUERY_GETTER_NAME = "get_connect_cloud_state" QUERY_GETTER_NAME = "get_connect_cloud_state"
REQUIRED_COMPONENT = "cloud_connect" REQUIRED_COMPONENT = "cloud_connect"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(

View File

@ -1,6 +1,8 @@
"""Implementation of color temp module.""" """Implementation of color temp module."""
from typing import TYPE_CHECKING, Dict from __future__ import annotations
from typing import TYPE_CHECKING
from ...bulb import ColorTempRange from ...bulb import ColorTempRange
from ...feature import Feature from ...feature import Feature
@ -15,7 +17,7 @@ class ColorTemperatureModule(SmartModule):
REQUIRED_COMPONENT = "color_temperature" REQUIRED_COMPONENT = "color_temperature"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -28,7 +30,7 @@ class ColorTemperatureModule(SmartModule):
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
# Color temp is contained in the main device info response. # Color temp is contained in the main device info response.
return {} return {}

View File

@ -1,6 +1,6 @@
"""Implementation of device module.""" """Implementation of device module."""
from typing import Dict from __future__ import annotations
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -10,7 +10,7 @@ class DeviceModule(SmartModule):
REQUIRED_COMPONENT = "device" REQUIRED_COMPONENT = "device"
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
query = { query = {
"get_device_info": None, "get_device_info": None,

View File

@ -1,6 +1,8 @@
"""Implementation of energy monitoring module.""" """Implementation of energy monitoring module."""
from typing import TYPE_CHECKING, Dict, Optional from __future__ import annotations
from typing import TYPE_CHECKING
from ...emeterstatus import EmeterStatus from ...emeterstatus import EmeterStatus
from ...feature import Feature from ...feature import Feature
@ -15,7 +17,7 @@ class EnergyModule(SmartModule):
REQUIRED_COMPONENT = "energy_monitoring" REQUIRED_COMPONENT = "energy_monitoring"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -42,7 +44,7 @@ class EnergyModule(SmartModule):
) )
) # Wh or kWH? ) # Wh or kWH?
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
req = { req = {
"get_energy_usage": None, "get_energy_usage": None,
@ -77,15 +79,15 @@ class EnergyModule(SmartModule):
) )
@property @property
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Get the emeter value for this month.""" """Get the emeter value for this month."""
return self._convert_energy_data(self.energy.get("month_energy"), 1 / 1000) return self._convert_energy_data(self.energy.get("month_energy"), 1 / 1000)
@property @property
def emeter_today(self) -> Optional[float]: def emeter_today(self) -> float | None:
"""Get the emeter value for today.""" """Get the emeter value for today."""
return self._convert_energy_data(self.energy.get("today_energy"), 1 / 1000) return self._convert_energy_data(self.energy.get("today_energy"), 1 / 1000)
def _convert_energy_data(self, data, scale) -> Optional[float]: def _convert_energy_data(self, data, scale) -> float | None:
"""Return adjusted emeter information.""" """Return adjusted emeter information."""
return data if not data else data * scale return data if not data else data * scale

View File

@ -1,5 +1,8 @@
"""Implementation of fan_control module.""" """Implementation of fan_control module."""
from typing import TYPE_CHECKING, Dict
from __future__ import annotations
from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -13,7 +16,7 @@ class FanModule(SmartModule):
REQUIRED_COMPONENT = "fan_control" REQUIRED_COMPONENT = "fan_control"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
@ -37,11 +40,11 @@ class FanModule(SmartModule):
attribute_getter="sleep_mode", attribute_getter="sleep_mode",
attribute_setter="set_sleep_mode", attribute_setter="set_sleep_mode",
icon="mdi:sleep", icon="mdi:sleep",
type=FeatureType.Switch type=FeatureType.Switch,
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
return {} return {}

View File

@ -1,6 +1,8 @@
"""Implementation of firmware module.""" """Implementation of firmware module."""
from typing import TYPE_CHECKING, Dict, Optional from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from ...exceptions import SmartErrorCode from ...exceptions import SmartErrorCode
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
@ -20,11 +22,11 @@ class UpdateInfo(BaseModel):
"""Update info status object.""" """Update info status object."""
status: int = Field(alias="type") status: int = Field(alias="type")
fw_ver: Optional[str] = None fw_ver: Optional[str] = None # noqa: UP007
release_date: Optional[date] = None release_date: Optional[date] = None # noqa: UP007
release_notes: Optional[str] = Field(alias="release_note", default=None) release_notes: Optional[str] = Field(alias="release_note", default=None) # noqa: UP007
fw_size: Optional[int] = None fw_size: Optional[int] = None # noqa: UP007
oem_id: Optional[str] = None oem_id: Optional[str] = None # noqa: UP007
needs_upgrade: bool = Field(alias="need_to_upgrade") needs_upgrade: bool = Field(alias="need_to_upgrade")
@validator("release_date", pre=True) @validator("release_date", pre=True)
@ -47,7 +49,7 @@ class Firmware(SmartModule):
REQUIRED_COMPONENT = "firmware" REQUIRED_COMPONENT = "firmware"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
if self.supported_version > 1: if self.supported_version > 1:
self._add_feature( self._add_feature(
@ -70,7 +72,7 @@ class Firmware(SmartModule):
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
req = { req = {
"get_latest_fw": None, "get_latest_fw": None,

View File

@ -1,5 +1,7 @@
"""Implementation of humidity module.""" """Implementation of humidity module."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
@ -15,7 +17,7 @@ class HumiditySensor(SmartModule):
REQUIRED_COMPONENT = "humidity" REQUIRED_COMPONENT = "humidity"
QUERY_GETTER_NAME = "get_comfort_humidity_config" QUERY_GETTER_NAME = "get_comfort_humidity_config"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(

View File

@ -1,6 +1,8 @@
"""Module for led controls.""" """Module for led controls."""
from typing import TYPE_CHECKING, Dict from __future__ import annotations
from typing import TYPE_CHECKING
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -15,7 +17,7 @@ class LedModule(SmartModule):
REQUIRED_COMPONENT = "led" REQUIRED_COMPONENT = "led"
QUERY_GETTER_NAME = "get_led_info" QUERY_GETTER_NAME = "get_led_info"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(
@ -29,7 +31,7 @@ class LedModule(SmartModule):
) )
) )
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle.""" """Query to execute during the update cycle."""
return {self.QUERY_GETTER_NAME: {"led_rule": None}} return {self.QUERY_GETTER_NAME: {"led_rule": None}}

View File

@ -1,5 +1,7 @@
"""Module for smooth light transitions.""" """Module for smooth light transitions."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...exceptions import KasaException from ...exceptions import KasaException
@ -17,7 +19,7 @@ class LightTransitionModule(SmartModule):
QUERY_GETTER_NAME = "get_on_off_gradually_info" QUERY_GETTER_NAME = "get_on_off_gradually_info"
MAXIMUM_DURATION = 60 MAXIMUM_DURATION = 60
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._create_features() self._create_features()

View File

@ -1,5 +1,7 @@
"""Implementation of report module.""" """Implementation of report module."""
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ...feature import Feature from ...feature import Feature
@ -15,7 +17,7 @@ class ReportModule(SmartModule):
REQUIRED_COMPONENT = "report_mode" REQUIRED_COMPONENT = "report_mode"
QUERY_GETTER_NAME = "get_report_mode" QUERY_GETTER_NAME = "get_report_mode"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(

View File

@ -1,5 +1,7 @@
"""Implementation of temperature module.""" """Implementation of temperature module."""
from __future__ import annotations
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
from ...feature import Feature, FeatureType from ...feature import Feature, FeatureType
@ -15,7 +17,7 @@ class TemperatureSensor(SmartModule):
REQUIRED_COMPONENT = "temperature" REQUIRED_COMPONENT = "temperature"
QUERY_GETTER_NAME = "get_comfort_temp_config" QUERY_GETTER_NAME = "get_comfort_temp_config"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(
Feature( Feature(

View File

@ -1,5 +1,7 @@
"""Implementation of time module.""" """Implementation of time module."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from time import mktime from time import mktime
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
@ -17,7 +19,7 @@ class TimeModule(SmartModule):
REQUIRED_COMPONENT = "time" REQUIRED_COMPONENT = "time"
QUERY_GETTER_NAME = "get_device_time" QUERY_GETTER_NAME = "get_device_time"
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
super().__init__(device, module) super().__init__(device, module)
self._add_feature( self._add_feature(

View File

@ -1,6 +1,6 @@
"""Module for tapo-branded smart bulbs (L5**).""" """Module for tapo-branded smart bulbs (L5**)."""
from typing import Dict, List, Optional from __future__ import annotations
from ..bulb import Bulb from ..bulb import Bulb
from ..exceptions import KasaException from ..exceptions import KasaException
@ -55,7 +55,7 @@ class SmartBulb(SmartDevice, Bulb):
return "dynamic_light_effect_enable" in self._info return "dynamic_light_effect_enable" in self._info
@property @property
def effect(self) -> Dict: def effect(self) -> dict:
"""Return effect state. """Return effect state.
This follows the format used by SmartLightStrip. This follows the format used by SmartLightStrip.
@ -79,7 +79,7 @@ class SmartBulb(SmartDevice, Bulb):
return data return data
@property @property
def effect_list(self) -> Optional[List[str]]: def effect_list(self) -> list[str] | None:
"""Return built-in effects list. """Return built-in effects list.
Example: Example:
@ -124,10 +124,10 @@ class SmartBulb(SmartDevice, Bulb):
self, self,
hue: int, hue: int,
saturation: int, saturation: int,
value: Optional[int] = None, value: int | None = None,
*, *,
transition: Optional[int] = None, transition: int | None = None,
) -> Dict: ) -> dict:
"""Set new HSV. """Set new HSV.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -163,8 +163,8 @@ class SmartBulb(SmartDevice, Bulb):
return await self.protocol.query({"set_device_info": {**request_payload}}) return await self.protocol.query({"set_device_info": {**request_payload}})
async def set_color_temp( async def set_color_temp(
self, temp: int, *, brightness=None, transition: Optional[int] = None self, temp: int, *, brightness=None, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the color temperature of the device in kelvin. """Set the color temperature of the device in kelvin.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -193,8 +193,8 @@ class SmartBulb(SmartDevice, Bulb):
raise ValueError(f"Invalid brightness value: {value} (valid range: 1-100%)") raise ValueError(f"Invalid brightness value: {value} (valid range: 1-100%)")
async def set_brightness( async def set_brightness(
self, brightness: int, *, transition: Optional[int] = None self, brightness: int, *, transition: int | None = None
) -> Dict: ) -> dict:
"""Set the brightness in percentage. """Set the brightness in percentage.
Note, transition is not supported and will be ignored. Note, transition is not supported and will be ignored.
@ -215,13 +215,13 @@ class SmartBulb(SmartDevice, Bulb):
self, self,
effect: str, effect: str,
*, *,
brightness: Optional[int] = None, brightness: int | None = None,
transition: Optional[int] = None, transition: int | None = None,
) -> None: ) -> None:
"""Set an effect on the device.""" """Set an effect on the device."""
raise NotImplementedError() raise NotImplementedError()
@property @property
def presets(self) -> List[BulbPreset]: def presets(self) -> list[BulbPreset]:
"""Return a list of available bulb setting presets.""" """Return a list of available bulb setting presets."""
return [] return []

View File

@ -1,7 +1,8 @@
"""Child device implementation.""" """Child device implementation."""
from __future__ import annotations
import logging import logging
from typing import Optional
from ..device_type import DeviceType from ..device_type import DeviceType
from ..deviceconfig import DeviceConfig from ..deviceconfig import DeviceConfig
@ -22,8 +23,8 @@ class SmartChildDevice(SmartDevice):
parent: SmartDevice, parent: SmartDevice,
info, info,
component_info, component_info,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[SmartProtocol] = None, protocol: SmartProtocol | None = None,
) -> None: ) -> None:
super().__init__(parent.host, config=parent.config, protocol=parent.protocol) super().__init__(parent.host, config=parent.config, protocol=parent.protocol)
self._parent = parent self._parent = parent
@ -38,7 +39,7 @@ class SmartChildDevice(SmartDevice):
@classmethod @classmethod
async def create(cls, parent: SmartDevice, child_info, child_components): async def create(cls, parent: SmartDevice, child_info, child_components):
"""Create a child device based on device info and component listing.""" """Create a child device based on device info and component listing."""
child: "SmartChildDevice" = cls(parent, child_info, child_components) child: SmartChildDevice = cls(parent, child_info, child_components)
await child._initialize_modules() await child._initialize_modules()
await child._initialize_features() await child._initialize_features()
return child return child

View File

@ -1,9 +1,11 @@
"""Module for a SMART device.""" """Module for a SMART device."""
from __future__ import annotations
import base64 import base64
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Sequence, cast from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
from ..aestransport import AesTransport from ..aestransport import AesTransport
from ..device import Device, WifiNetwork from ..device import Device, WifiNetwork
@ -28,20 +30,20 @@ class SmartDevice(Device):
self, self,
host: str, host: str,
*, *,
config: Optional[DeviceConfig] = None, config: DeviceConfig | None = None,
protocol: Optional[SmartProtocol] = None, protocol: SmartProtocol | None = None,
) -> None: ) -> None:
_protocol = protocol or SmartProtocol( _protocol = protocol or SmartProtocol(
transport=AesTransport(config=config or DeviceConfig(host=host)), transport=AesTransport(config=config or DeviceConfig(host=host)),
) )
super().__init__(host=host, config=config, protocol=_protocol) super().__init__(host=host, config=config, protocol=_protocol)
self.protocol: SmartProtocol self.protocol: SmartProtocol
self._components_raw: Optional[Dict[str, Any]] = None self._components_raw: dict[str, Any] | None = None
self._components: Dict[str, int] = {} self._components: dict[str, int] = {}
self._state_information: Dict[str, Any] = {} self._state_information: dict[str, Any] = {}
self.modules: Dict[str, "SmartModule"] = {} self.modules: dict[str, SmartModule] = {}
self._parent: Optional["SmartDevice"] = None self._parent: SmartDevice | None = None
self._children: Mapping[str, "SmartDevice"] = {} self._children: Mapping[str, SmartDevice] = {}
self._last_update = {} self._last_update = {}
async def _initialize_children(self): async def _initialize_children(self):
@ -74,7 +76,7 @@ class SmartDevice(Device):
} }
@property @property
def children(self) -> Sequence["SmartDevice"]: def children(self) -> Sequence[SmartDevice]:
"""Return list of children.""" """Return list of children."""
return list(self._children.values()) return list(self._children.values())
@ -130,7 +132,7 @@ class SmartDevice(Device):
await self._negotiate() await self._negotiate()
await self._initialize_modules() await self._initialize_modules()
req: Dict[str, Any] = {} req: dict[str, Any] = {}
# TODO: this could be optimized by constructing the query only once # TODO: this could be optimized by constructing the query only once
for module in self.modules.values(): for module in self.modules.values():
@ -236,7 +238,7 @@ class SmartDevice(Device):
self._add_feature(feat) self._add_feature(feat)
@property @property
def sys_info(self) -> Dict[str, Any]: def sys_info(self) -> dict[str, Any]:
"""Returns the device info.""" """Returns the device info."""
return self._info # type: ignore return self._info # type: ignore
@ -246,7 +248,7 @@ class SmartDevice(Device):
return str(self._info.get("model")) return str(self._info.get("model"))
@property @property
def alias(self) -> Optional[str]: def alias(self) -> str | None:
"""Returns the device alias or nickname.""" """Returns the device alias or nickname."""
if self._info and (nickname := self._info.get("nickname")): if self._info and (nickname := self._info.get("nickname")):
return base64.b64decode(nickname).decode() return base64.b64decode(nickname).decode()
@ -265,13 +267,13 @@ class SmartDevice(Device):
return _timemod.time return _timemod.time
@property @property
def timezone(self) -> Dict: def timezone(self) -> dict:
"""Return the timezone and time_difference.""" """Return the timezone and time_difference."""
ti = self.time ti = self.time
return {"timezone": ti.tzname()} return {"timezone": ti.tzname()}
@property @property
def hw_info(self) -> Dict: def hw_info(self) -> dict:
"""Return hardware info for the device.""" """Return hardware info for the device."""
return { return {
"sw_ver": self._info.get("fw_ver"), "sw_ver": self._info.get("fw_ver"),
@ -284,7 +286,7 @@ class SmartDevice(Device):
} }
@property @property
def location(self) -> Dict: def location(self) -> dict:
"""Return the device location.""" """Return the device location."""
loc = { loc = {
"latitude": cast(float, self._info.get("latitude", 0)) / 10_000, "latitude": cast(float, self._info.get("latitude", 0)) / 10_000,
@ -293,7 +295,7 @@ class SmartDevice(Device):
return loc return loc
@property @property
def rssi(self) -> Optional[int]: def rssi(self) -> int | None:
"""Return the rssi.""" """Return the rssi."""
rssi = self._info.get("rssi") rssi = self._info.get("rssi")
return int(rssi) if rssi else None return int(rssi) if rssi else None
@ -321,7 +323,7 @@ class SmartDevice(Device):
self._info = info self._info = info
async def _query_helper( async def _query_helper(
self, method: str, params: Optional[Dict] = None, child_ids=None self, method: str, params: dict | None = None, child_ids=None
) -> Any: ) -> Any:
res = await self.protocol.query({method: params}) res = await self.protocol.query({method: params})
@ -378,19 +380,19 @@ class SmartDevice(Device):
return energy.emeter_realtime return energy.emeter_realtime
@property @property
def emeter_this_month(self) -> Optional[float]: def emeter_this_month(self) -> float | None:
"""Get the emeter value for this month.""" """Get the emeter value for this month."""
energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405 energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405
return energy.emeter_this_month return energy.emeter_this_month
@property @property
def emeter_today(self) -> Optional[float]: def emeter_today(self) -> float | None:
"""Get the emeter value for today.""" """Get the emeter value for today."""
energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405 energy = cast(EnergyModule, self.modules["EnergyModule"]) # noqa: F405
return energy.emeter_today return energy.emeter_today
@property @property
def on_since(self) -> Optional[datetime]: def on_since(self) -> datetime | None:
"""Return the time that the device was turned on or None if turned off.""" """Return the time that the device was turned on or None if turned off."""
if ( if (
not self._info.get("device_on") not self._info.get("device_on")
@ -404,7 +406,7 @@ class SmartDevice(Device):
else: # We have no device time, use current local time. else: # We have no device time, use current local time.
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time) return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
async def wifi_scan(self) -> List[WifiNetwork]: async def wifi_scan(self) -> list[WifiNetwork]:
"""Scan for available wifi networks.""" """Scan for available wifi networks."""
def _net_for_scan_info(res): def _net_for_scan_info(res):
@ -527,7 +529,7 @@ class SmartDevice(Device):
@staticmethod @staticmethod
def _get_device_type_from_components( def _get_device_type_from_components(
components: List[str], device_type: str components: list[str], device_type: str
) -> DeviceType: ) -> DeviceType:
"""Find type to be displayed as a supported device category.""" """Find type to be displayed as a supported device category."""
if "HUB" in device_type: if "HUB" in device_type:

View File

@ -1,7 +1,9 @@
"""Base implementation for SMART modules.""" """Base implementation for SMART modules."""
from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Dict, Type from typing import TYPE_CHECKING
from ..exceptions import KasaException from ..exceptions import KasaException
from ..module import Module from ..module import Module
@ -18,9 +20,9 @@ class SmartModule(Module):
NAME: str NAME: str
REQUIRED_COMPONENT: str REQUIRED_COMPONENT: str
QUERY_GETTER_NAME: str QUERY_GETTER_NAME: str
REGISTERED_MODULES: Dict[str, Type["SmartModule"]] = {} REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
def __init__(self, device: "SmartDevice", module: str): def __init__(self, device: SmartDevice, module: str):
self._device: SmartDevice self._device: SmartDevice
super().__init__(device, module) super().__init__(device, module)
@ -36,7 +38,7 @@ class SmartModule(Module):
"""Name of the module.""" """Name of the module."""
return getattr(self, "NAME", self.__class__.__name__) return getattr(self, "NAME", self.__class__.__name__)
def query(self) -> Dict: def query(self) -> dict:
"""Query to execute during the update cycle. """Query to execute during the update cycle.
Default implementation uses the raw query getter w/o parameters. Default implementation uses the raw query getter w/o parameters.

View File

@ -4,13 +4,15 @@ Based on the work of https://github.com/petretiandrea/plugp100
under compatible GNU GPL3 license. under compatible GNU GPL3 license.
""" """
from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import logging import logging
import time import time
import uuid import uuid
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any, Dict, Union from typing import Any
from .exceptions import ( from .exceptions import (
SMART_AUTHENTICATION_ERRORS, SMART_AUTHENTICATION_ERRORS,
@ -57,12 +59,12 @@ class SmartProtocol(BaseProtocol):
} }
return json_dumps(request) return json_dumps(request)
async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async def query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Query the device retrying for retry_count on failure.""" """Query the device retrying for retry_count on failure."""
async with self._query_lock: async with self._query_lock:
return await self._query(request, retry_count) return await self._query(request, retry_count)
async def _query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async def _query(self, request: str | dict, retry_count: int = 3) -> dict:
for retry in range(retry_count + 1): for retry in range(retry_count + 1):
try: try:
return await self._execute_query(request, retry) return await self._execute_query(request, retry)
@ -103,9 +105,9 @@ class SmartProtocol(BaseProtocol):
# make mypy happy, this should never be reached.. # make mypy happy, this should never be reached..
raise KasaException("Query reached somehow to unreachable") raise KasaException("Query reached somehow to unreachable")
async def _execute_multiple_query(self, request: Dict, retry_count: int) -> Dict: async def _execute_multiple_query(self, request: dict, retry_count: int) -> dict:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
multi_result: Dict[str, Any] = {} multi_result: dict[str, Any] = {}
smart_method = "multipleRequest" smart_method = "multipleRequest"
requests = [ requests = [
{"method": method, "params": params} for method, params in request.items() {"method": method, "params": params} for method, params in request.items()
@ -146,7 +148,7 @@ class SmartProtocol(BaseProtocol):
multi_result[method] = result multi_result[method] = result
return multi_result return multi_result
async def _execute_query(self, request: Union[str, Dict], retry_count: int) -> Dict: async def _execute_query(self, request: str | dict, retry_count: int) -> dict:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
if isinstance(request, dict): if isinstance(request, dict):
@ -322,7 +324,7 @@ class _ChildProtocolWrapper(SmartProtocol):
return smart_method, smart_params return smart_method, smart_params
async def query(self, request: Union[str, Dict], retry_count: int = 3) -> Dict: async def query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Wrap request inside control_child envelope.""" """Wrap request inside control_child envelope."""
method, params = self._get_method_and_params_for_request(request) method, params = self._get_method_and_params_for_request(request)
request_data = { request_data = {

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import warnings import warnings
from typing import Dict
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
@ -37,7 +38,7 @@ def dummy_protocol():
def credentials_hash(self) -> str: def credentials_hash(self) -> str:
return "dummy hash" return "dummy hash"
async def send(self, request: str) -> Dict: async def send(self, request: str) -> dict:
return {} return {}
async def close(self) -> None: async def close(self) -> None:

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from itertools import chain from itertools import chain
from typing import Dict, List, Set
import pytest import pytest
@ -128,10 +129,10 @@ ALL_DEVICES_SMART = (
) )
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART) ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
IP_MODEL_CACHE: Dict[str, str] = {} IP_MODEL_CACHE: dict[str, str] = {}
def parametrize_combine(parametrized: List[pytest.MarkDecorator]): def parametrize_combine(parametrized: list[pytest.MarkDecorator]):
"""Combine multiple pytest parametrize dev marks into one set of fixtures.""" """Combine multiple pytest parametrize dev marks into one set of fixtures."""
fixtures = set() fixtures = set()
for param in parametrized: for param in parametrized:
@ -291,7 +292,7 @@ def check_categories():
+ hubs_smart.args[1] + hubs_smart.args[1]
+ sensors_smart.args[1] + sensors_smart.args[1]
) )
diffs: Set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures) diffs: set[FixtureInfo] = set(FIXTURE_DATA) - set(categorized_fixtures)
if diffs: if diffs:
print(diffs) print(diffs)
for diff in diffs: for diff in diffs:

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from json import dumps as json_dumps from json import dumps as json_dumps
from typing import Optional
import pytest import pytest
@ -76,8 +77,8 @@ def discovery_mock(request, mocker):
query_data: dict query_data: dict
device_type: str device_type: str
encrypt_type: str encrypt_type: str
login_version: Optional[int] = None login_version: int | None = None
port_override: Optional[int] = None port_override: int | None = None
if "discovery_result" in fixture_data: if "discovery_result" in fixture_data:
discovery_data = {"result": fixture_data["discovery_result"]} discovery_data = {"result": fixture_data["discovery_result"]}

View File

@ -1,8 +1,10 @@
from __future__ import annotations
import glob import glob
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Set from typing import NamedTuple
from kasa.device_factory import _get_device_type_from_sys_info from kasa.device_factory import _get_device_type_from_sys_info
from kasa.device_type import DeviceType from kasa.device_type import DeviceType
@ -12,7 +14,7 @@ from kasa.smart.smartdevice import SmartDevice
class FixtureInfo(NamedTuple): class FixtureInfo(NamedTuple):
name: str name: str
protocol: str protocol: str
data: Dict data: dict
FixtureInfo.__hash__ = lambda self: hash((self.name, self.protocol)) # type: ignore[attr-defined, method-assign] FixtureInfo.__hash__ = lambda self: hash((self.name, self.protocol)) # type: ignore[attr-defined, method-assign]
@ -55,7 +57,7 @@ def idgenerator(paramtuple: FixtureInfo):
return None return None
def get_fixture_info() -> List[FixtureInfo]: def get_fixture_info() -> list[FixtureInfo]:
"""Return raw discovery file contents as JSON. Used for discovery tests.""" """Return raw discovery file contents as JSON. Used for discovery tests."""
fixture_data = [] fixture_data = []
for file, protocol in SUPPORTED_DEVICES: for file, protocol in SUPPORTED_DEVICES:
@ -77,17 +79,17 @@ def get_fixture_info() -> List[FixtureInfo]:
return fixture_data return fixture_data
FIXTURE_DATA: List[FixtureInfo] = get_fixture_info() FIXTURE_DATA: list[FixtureInfo] = get_fixture_info()
def filter_fixtures( def filter_fixtures(
desc, desc,
*, *,
data_root_filter: Optional[str] = None, data_root_filter: str | None = None,
protocol_filter: Optional[Set[str]] = None, protocol_filter: set[str] | None = None,
model_filter: Optional[Set[str]] = None, model_filter: set[str] | None = None,
component_filter: Optional[str] = None, component_filter: str | None = None,
device_type_filter: Optional[List[DeviceType]] = None, device_type_filter: list[DeviceType] | None = None,
): ):
"""Filter the fixtures based on supplied parameters. """Filter the fixtures based on supplied parameters.

View File

@ -14,7 +14,11 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
"""Test fan speed feature.""" """Test fan speed feature."""
fan: FanModule = dev.modules["FanModule"] fan: FanModule = dev.modules["FanModule"]
level_feature = fan._module_features["fan_speed_level"] level_feature = fan._module_features["fan_speed_level"]
assert level_feature.minimum_value <= level_feature.value <= level_feature.maximum_value assert (
level_feature.minimum_value
<= level_feature.value
<= level_feature.maximum_value
)
call = mocker.spy(fan, "call") call = mocker.spy(fan, "call")
await fan.set_fan_speed_level(3) await fan.set_fan_speed_level(3)

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import base64 import base64
import json import json
import logging import logging
@ -7,7 +9,7 @@ import time
from contextlib import nullcontext as does_not_raise from contextlib import nullcontext as does_not_raise
from json import dumps as json_dumps from json import dumps as json_dumps
from json import loads as json_loads from json import loads as json_loads
from typing import Any, Dict from typing import Any
import aiohttp import aiohttp
import pytest import pytest
@ -335,7 +337,7 @@ class MockAesDevice:
json = json_loads(item.decode()) json = json_loads(item.decode())
return await self._post(url, json) return await self._post(url, json)
async def _post(self, url: URL, json: Dict[str, Any]): async def _post(self, url: URL, json: dict[str, Any]):
if json["method"] == "handshake": if json["method"] == "handshake":
return await self._return_handshake_response(url, json) return await self._return_handshake_response(url, json)
elif json["method"] == "securePassthrough": elif json["method"] == "securePassthrough":
@ -346,7 +348,7 @@ class MockAesDevice:
assert str(url) == f"http://{self.host}:80/app?token={self.token}" assert str(url) == f"http://{self.host}:80/app?token={self.token}"
return await self._return_send_response(url, json) return await self._return_send_response(url, json)
async def _return_handshake_response(self, url: URL, json: Dict[str, Any]): async def _return_handshake_response(self, url: URL, json: dict[str, Any]):
start = len("-----BEGIN PUBLIC KEY-----\n") start = len("-----BEGIN PUBLIC KEY-----\n")
end = len("\n-----END PUBLIC KEY-----\n") end = len("\n-----END PUBLIC KEY-----\n")
client_pub_key = json["params"]["key"][start:-end] client_pub_key = json["params"]["key"][start:-end]
@ -359,7 +361,7 @@ class MockAesDevice:
self.status_code, {"result": {"key": key_64}, "error_code": self.error_code} self.status_code, {"result": {"key": key_64}, "error_code": self.error_code}
) )
async def _return_secure_passthrough_response(self, url: URL, json: Dict[str, Any]): async def _return_secure_passthrough_response(self, url: URL, json: dict[str, Any]):
encrypted_request = json["params"]["request"] encrypted_request = json["params"]["request"]
decrypted_request = self.encryption_session.decrypt(encrypted_request.encode()) decrypted_request = self.encryption_session.decrypt(encrypted_request.encode())
decrypted_request_dict = json_loads(decrypted_request) decrypted_request_dict = json_loads(decrypted_request)
@ -378,7 +380,7 @@ class MockAesDevice:
} }
return self._mock_response(self.status_code, result) return self._mock_response(self.status_code, result)
async def _return_login_response(self, url: URL, json: Dict[str, Any]): async def _return_login_response(self, url: URL, json: dict[str, Any]):
if "token=" in str(url): if "token=" in str(url):
raise Exception("token should not be in url for a login request") raise Exception("token should not be in url for a login request")
self.token = "".join(random.choices(string.ascii_uppercase, k=32)) # noqa: S311 self.token = "".join(random.choices(string.ascii_uppercase, k=32)) # noqa: S311
@ -386,7 +388,7 @@ class MockAesDevice:
self.inner_call_count += 1 self.inner_call_count += 1
return self._mock_response(self.status_code, result) return self._mock_response(self.status_code, result)
async def _return_send_response(self, url: URL, json: Dict[str, Any]): async def _return_send_response(self, url: URL, json: dict[str, Any]):
result = {"result": {"method": None}, "error_code": self.inner_error_code} result = {"result": {"method": None}, "error_code": self.inner_error_code}
response = self.send_response if self.send_response else result response = self.send_response if self.send_response else result
self.inner_call_count += 1 self.inner_call_count += 1

View File

@ -1,7 +1,9 @@
"""Tests for SMART devices.""" """Tests for SMART devices."""
from __future__ import annotations
import logging import logging
from typing import Any, Dict from typing import Any
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -99,7 +101,7 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture):
assert dev.modules assert dev.modules
await dev.update() await dev.update()
full_query: Dict[str, Any] = {} full_query: dict[str, Any] = {}
for mod in dev.modules.values(): for mod in dev.modules.values():
full_query = {**full_query, **mod.query()} full_query = {**full_query, **mod.query()}

View File

@ -10,6 +10,8 @@ which are licensed under the Apache License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
""" """
from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
import errno import errno
@ -17,7 +19,7 @@ import logging
import socket import socket
import struct import struct
from pprint import pformat as pf from pprint import pformat as pf
from typing import Dict, Generator, Optional from typing import Generator
# When support for cpython older than 3.11 is dropped # When support for cpython older than 3.11 is dropped
# async_timeout can be replaced with asyncio.timeout # async_timeout can be replaced with asyncio.timeout
@ -41,10 +43,10 @@ class XorTransport(BaseTransport):
def __init__(self, *, config: DeviceConfig) -> None: def __init__(self, *, config: DeviceConfig) -> None:
super().__init__(config=config) super().__init__(config=config)
self.reader: Optional[asyncio.StreamReader] = None self.reader: asyncio.StreamReader | None = None
self.writer: Optional[asyncio.StreamWriter] = None self.writer: asyncio.StreamWriter | None = None
self.query_lock = asyncio.Lock() self.query_lock = asyncio.Lock()
self.loop: Optional[asyncio.AbstractEventLoop] = None self.loop: asyncio.AbstractEventLoop | None = None
@property @property
def default_port(self): def default_port(self):
@ -72,7 +74,7 @@ class XorTransport(BaseTransport):
# the buffer on the device # the buffer on the device
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
async def _execute_send(self, request: str) -> Dict: async def _execute_send(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 # noqa: S101 assert self.writer is not None # noqa: S101
assert self.reader is not None # noqa: S101 assert self.reader is not None # noqa: S101
@ -115,7 +117,7 @@ class XorTransport(BaseTransport):
""" """
await self.close() await self.close()
async def send(self, request: str) -> Dict: async def send(self, request: str) -> dict:
"""Send a message to the device and return a response.""" """Send a message to the device and return a response."""
# #
# 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

View File

@ -110,6 +110,7 @@ select = [
"UP", # pyupgrade "UP", # pyupgrade
"B", # flake8-bugbear "B", # flake8-bugbear
"SIM", # flake8-simplify "SIM", # flake8-simplify
"FA", # flake8-future-annotations
"I", # isort "I", # isort
"S", # bandit "S", # bandit
] ]