mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
a2444da9df
Several KASA devices seem to have pretty strict buffer size limitations on incoming/outgoing data transfers. Testing on KL125-US and HL103 has shown that sending a request size larger than about ~768 bytes will immediately crash the device. Additionally, a query that generates a response larger than ~4096 bytes will crash the KL125-US. I was unable to generate such a large response to test the HL103. The KL125-US will only return such large queries when its monthly usage stats have been populated. This means that a new bulb would work fine, but after a month of data collection the bulb would break the 4K limit and start to crash. To work around this issue, an estimated worst-case response size is calculated before sending a request by summing up all modules estimated response size. If the estimated size is greater than the device's max_response_payload_size then the query will be split into multiple queries. This PR implements splitting queries expected to have large responses and also removes the module 'skip list' which was a previous workaround to the crash (which worked by simply reducing the number of modules queried, which prevented the overflow) since it is no longer necessary. This PR does not attempt to address the "input buffer size limit." Thus far this limit has not been an issue.
84 lines
2.6 KiB
Python
84 lines
2.6 KiB
Python
"""Base class for all module implementations."""
|
|
import collections
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ..exceptions import SmartDeviceException
|
|
|
|
if TYPE_CHECKING:
|
|
from kasa import SmartDevice
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
# TODO: This is used for query construcing
|
|
def merge(d, u):
|
|
"""Update dict recursively."""
|
|
for k, v in u.items():
|
|
if isinstance(v, collections.abc.Mapping):
|
|
d[k] = merge(d.get(k, {}), v)
|
|
else:
|
|
d[k] = v
|
|
return d
|
|
|
|
|
|
class Module(ABC):
|
|
"""Base class implemention for all modules.
|
|
|
|
The base classes should implement `query` to return the query they want to be
|
|
executed during the regular update cycle.
|
|
"""
|
|
|
|
def __init__(self, device: "SmartDevice", module: str):
|
|
self._device: "SmartDevice" = device
|
|
self._module = module
|
|
|
|
@abstractmethod
|
|
def query(self):
|
|
"""Query to execute during the update cycle.
|
|
|
|
The inheriting modules implement this to include their wanted
|
|
queries to the query that gets executed when Device.update() gets called.
|
|
"""
|
|
|
|
@property
|
|
def estimated_query_response_size(self):
|
|
"""Estimated maximum size of query response.
|
|
|
|
The inheriting modules implement this to estimate how large a query response
|
|
will be so that queries can be split should an estimated response be too large
|
|
"""
|
|
return 256 # Estimate for modules that don't specify
|
|
|
|
@property
|
|
def data(self):
|
|
"""Return the module specific raw data from the last update."""
|
|
if self._module not in self._device._last_update:
|
|
raise SmartDeviceException(
|
|
f"You need to call update() prior accessing module data for '{self._module}'"
|
|
)
|
|
|
|
return self._device._last_update[self._module]
|
|
|
|
@property
|
|
def is_supported(self) -> bool:
|
|
"""Return whether the module is supported by the device."""
|
|
if self._module not in self._device._last_update:
|
|
_LOGGER.debug("Initial update, so consider supported: %s", self._module)
|
|
return True
|
|
|
|
return "err_code" not in self.data
|
|
|
|
def call(self, method, params=None):
|
|
"""Call the given method with the given parameters."""
|
|
return self._device._query_helper(self._module, method, params)
|
|
|
|
def query_for_command(self, query, params=None):
|
|
"""Create a request object for the given parameters."""
|
|
return self._device._create_request(self._module, query, params)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Module {self.__class__.__name__} ({self._module}) for {self._device.host}>"
|