mirror of
https://github.com/python-kasa/python-kasa.git
synced 2026-11-06 03:47:22 +00:00
Some checks failed
CI / Perform Lint Checks (3.15) (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.14 on macos-latest (push) Has been cancelled
CI / Python 3.15 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.14 on ubuntu-latest (push) Has been cancelled
CI / Python 3.15 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CI / Python 3.14 on windows-latest (push) Has been cancelled
CI / Python 3.15 on windows-latest (push) Has been cancelled
Stale / stale (push) Has been cancelled
Hubs keep reporting for children they have lost contact with, so callers see stale values as if they were live. Trying to write to such devices fail after timeouts with a generic error. This PR exposes the offline/online information as reported by the hub, and refuses writes to unreachable child devices without waiting for device errors. In details: - Add new API: `Device.is_available`; hub children report unavailable when the hub marks them offline or no longer lists them. - New feature `available` for child devices to make it visible to the CLI. - Refuse writes to unavailable children with the new `DeviceUnavailableError` instead of waiting for the hub to time out.
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""Child device implementation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from ..device import DeviceInfo
|
|
from ..device_type import DeviceType
|
|
from ..deviceconfig import DeviceConfig
|
|
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
|
|
from ..protocols.smartprotocol import SmartProtocol
|
|
from ..smart.smartchilddevice import SmartChildDevice
|
|
from ..smart.smartdevice import ComponentsRaw, SmartDevice
|
|
from .smartcamdevice import SmartCamDevice
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# SmartCamChild devices have a different info format from getChildDeviceInfo
|
|
# than when querying getDeviceInfo directly on the child.
|
|
# As _get_device_info is also called by dump_devtools and generate_supported
|
|
# this key will be expected by _get_device_info
|
|
CHILD_INFO_FROM_PARENT = "child_info_from_parent"
|
|
|
|
|
|
class SmartCamChild(SmartChildDevice, SmartCamDevice):
|
|
"""Presentation of a child device.
|
|
|
|
This wraps the protocol communications and sets internal data for the child.
|
|
"""
|
|
|
|
CHILD_DEVICE_TYPE_MAP = {
|
|
"camera": DeviceType.Camera,
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
parent: SmartDevice,
|
|
info: dict,
|
|
component_info_raw: ComponentsRaw,
|
|
*,
|
|
config: DeviceConfig | None = None,
|
|
protocol: SmartProtocol | None = None,
|
|
) -> None:
|
|
_protocol = protocol or _ChildCameraProtocolWrapper(
|
|
info["device_id"], parent.protocol
|
|
)
|
|
super().__init__(parent, info, component_info_raw, protocol=_protocol)
|
|
self._child_info_from_parent: dict = {}
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo:
|
|
"""Return device info.
|
|
|
|
Child device does not have it info and components in _last_update so
|
|
this overrides the base implementation to call _get_device_info with
|
|
info and components combined as they would be in _last_update.
|
|
"""
|
|
return self._get_device_info(
|
|
{
|
|
CHILD_INFO_FROM_PARENT: self._child_info_from_parent,
|
|
},
|
|
None,
|
|
)
|
|
|
|
@staticmethod
|
|
def _map_child_info_from_parent(device_info: dict) -> dict:
|
|
mappings = {
|
|
"device_model": "model",
|
|
"sw_ver": "fw_ver",
|
|
"hw_id": "hwId",
|
|
}
|
|
return {mappings.get(k, k): v for k, v in device_info.items()}
|
|
|
|
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
|
"""Update the internal info state.
|
|
|
|
This is used by the parent to push updates to its children.
|
|
"""
|
|
# smartcam children have info with different keys to their own
|
|
# getDeviceInfo queries
|
|
self._child_info_from_parent = info
|
|
|
|
# self._info will have the values normalized across smart and smartcam
|
|
# devices
|
|
self._info = self._map_child_info_from_parent(info)
|
|
|
|
@property
|
|
def is_available(self) -> bool:
|
|
"""Return False if the hub reports the camera offline or no longer lists it."""
|
|
# Camera children report "online" instead of an "offline" status.
|
|
return super().is_available and self._info.get("online") is not False
|
|
|
|
@property
|
|
def device_type(self) -> DeviceType:
|
|
"""Return the device type."""
|
|
if self._device_type == DeviceType.Unknown and self._info:
|
|
self._device_type = self._get_device_type_from_sysinfo(self._info)
|
|
return self._device_type
|
|
|
|
@staticmethod
|
|
def _get_device_info(
|
|
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
|
) -> DeviceInfo:
|
|
"""Get model information for a device."""
|
|
if not (cifp := info.get(CHILD_INFO_FROM_PARENT)):
|
|
return SmartCamDevice._get_device_info(info, discovery_info)
|
|
|
|
model = cifp["device_model"]
|
|
device_type = SmartCamDevice._get_device_type_from_sysinfo(cifp)
|
|
fw_version_full = cifp["sw_ver"]
|
|
if " " in fw_version_full:
|
|
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
|
else:
|
|
firmware_version, firmware_build = fw_version_full, None
|
|
return DeviceInfo(
|
|
short_name=model,
|
|
long_name=model,
|
|
brand="tapo",
|
|
device_family=cifp["device_type"],
|
|
device_type=device_type,
|
|
hardware_version=cifp["hw_ver"],
|
|
firmware_version=firmware_version,
|
|
firmware_build=firmware_build,
|
|
requires_auth=True,
|
|
region=cifp.get("region"),
|
|
)
|