mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Enable and convert to future annotations (#838)
This commit is contained in:
parent
82d92aeea5
commit
203bd79253
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
32
kasa/bulb.py
32
kasa/bulb.py
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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}
|
||||||
|
@ -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"]
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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"]]
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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::
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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 [
|
||||||
|
@ -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::
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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 {}
|
||||||
|
@ -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(
|
||||||
|
@ -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 {}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 {}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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}}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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 []
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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 = {
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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"]}
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user