mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Add bare-bones matter modules to smart and smartcam devices (#1371)
This commit is contained in:
parent
223f3318ea
commit
2ca6d3ebe9
@ -151,6 +151,8 @@ class Module(ABC):
|
|||||||
)
|
)
|
||||||
TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs")
|
TriggerLogs: Final[ModuleName[smart.TriggerLogs]] = ModuleName("TriggerLogs")
|
||||||
|
|
||||||
|
Matter: Final[ModuleName[smart.Matter]] = ModuleName("Matter")
|
||||||
|
|
||||||
# SMARTCAM only modules
|
# SMARTCAM only modules
|
||||||
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from .lighteffect import LightEffect
|
|||||||
from .lightpreset import LightPreset
|
from .lightpreset import LightPreset
|
||||||
from .lightstripeffect import LightStripEffect
|
from .lightstripeffect import LightStripEffect
|
||||||
from .lighttransition import LightTransition
|
from .lighttransition import LightTransition
|
||||||
|
from .matter import Matter
|
||||||
from .motionsensor import MotionSensor
|
from .motionsensor import MotionSensor
|
||||||
from .overheatprotection import OverheatProtection
|
from .overheatprotection import OverheatProtection
|
||||||
from .reportmode import ReportMode
|
from .reportmode import ReportMode
|
||||||
@ -66,4 +67,5 @@ __all__ = [
|
|||||||
"Thermostat",
|
"Thermostat",
|
||||||
"SmartLightEffect",
|
"SmartLightEffect",
|
||||||
"OverheatProtection",
|
"OverheatProtection",
|
||||||
|
"Matter",
|
||||||
]
|
]
|
||||||
|
43
kasa/smart/modules/matter.py
Normal file
43
kasa/smart/modules/matter.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
|
class Matter(SmartModule):
|
||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME: str = "get_matter_setup_info"
|
||||||
|
REQUIRED_COMPONENT = "matter"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_code",
|
||||||
|
name="Matter setup code",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_code"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_payload",
|
||||||
|
name="Matter setup payload",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_payload"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Matter setup info."""
|
||||||
|
return self.data
|
@ -57,7 +57,7 @@ class SmartModule(Module):
|
|||||||
#: Module is initialized, if any of the given keys exists in the sysinfo
|
#: Module is initialized, if any of the given keys exists in the sysinfo
|
||||||
SYSINFO_LOOKUP_KEYS: list[str] = []
|
SYSINFO_LOOKUP_KEYS: list[str] = []
|
||||||
#: Query to execute during the main update cycle
|
#: Query to execute during the main update cycle
|
||||||
QUERY_GETTER_NAME: str
|
QUERY_GETTER_NAME: str = ""
|
||||||
|
|
||||||
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
||||||
|
|
||||||
@ -138,7 +138,9 @@ class SmartModule(Module):
|
|||||||
|
|
||||||
Default implementation uses the raw query getter w/o parameters.
|
Default implementation uses the raw query getter w/o parameters.
|
||||||
"""
|
"""
|
||||||
|
if self.QUERY_GETTER_NAME:
|
||||||
return {self.QUERY_GETTER_NAME: None}
|
return {self.QUERY_GETTER_NAME: None}
|
||||||
|
return {}
|
||||||
|
|
||||||
async def call(self, method: str, params: dict | None = None) -> dict:
|
async def call(self, method: str, params: dict | None = None) -> dict:
|
||||||
"""Call a method.
|
"""Call a method.
|
||||||
|
@ -5,6 +5,7 @@ from .camera import Camera
|
|||||||
from .childdevice import ChildDevice
|
from .childdevice import ChildDevice
|
||||||
from .device import DeviceModule
|
from .device import DeviceModule
|
||||||
from .led import Led
|
from .led import Led
|
||||||
|
from .matter import Matter
|
||||||
from .pantilt import PanTilt
|
from .pantilt import PanTilt
|
||||||
from .time import Time
|
from .time import Time
|
||||||
|
|
||||||
@ -16,4 +17,5 @@ __all__ = [
|
|||||||
"Led",
|
"Led",
|
||||||
"PanTilt",
|
"PanTilt",
|
||||||
"Time",
|
"Time",
|
||||||
|
"Matter",
|
||||||
]
|
]
|
||||||
|
44
kasa/smartcam/modules/matter.py
Normal file
44
kasa/smartcam/modules/matter.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartcammodule import SmartCamModule
|
||||||
|
|
||||||
|
|
||||||
|
class Matter(SmartCamModule):
|
||||||
|
"""Implementation of matter module."""
|
||||||
|
|
||||||
|
QUERY_GETTER_NAME = "getMatterSetupInfo"
|
||||||
|
QUERY_MODULE_NAME = "matter"
|
||||||
|
REQUIRED_COMPONENT = "matter"
|
||||||
|
|
||||||
|
def _initialize_features(self) -> None:
|
||||||
|
"""Initialize features after the initial update."""
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_code",
|
||||||
|
name="Matter setup code",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_code"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
id="matter_setup_payload",
|
||||||
|
name="Matter setup payload",
|
||||||
|
container=self,
|
||||||
|
attribute_getter=lambda x: x.info["setup_payload"],
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
category=Feature.Category.Debug,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def info(self) -> dict[str, str]:
|
||||||
|
"""Matter setup info."""
|
||||||
|
return self.data
|
@ -21,8 +21,6 @@ class SmartCamModule(SmartModule):
|
|||||||
|
|
||||||
SmartCamAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCamAlarm")
|
SmartCamAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCamAlarm")
|
||||||
|
|
||||||
#: Query to execute during the main update cycle
|
|
||||||
QUERY_GETTER_NAME: str
|
|
||||||
#: Module name to be queried
|
#: Module name to be queried
|
||||||
QUERY_MODULE_NAME: str
|
QUERY_MODULE_NAME: str
|
||||||
#: Section name or names to be queried
|
#: Section name or names to be queried
|
||||||
@ -37,6 +35,8 @@ class SmartCamModule(SmartModule):
|
|||||||
|
|
||||||
Default implementation uses the raw query getter w/o parameters.
|
Default implementation uses the raw query getter w/o parameters.
|
||||||
"""
|
"""
|
||||||
|
if not self.QUERY_GETTER_NAME:
|
||||||
|
return {}
|
||||||
section_names = (
|
section_names = (
|
||||||
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
|
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
|
||||||
)
|
)
|
||||||
@ -86,7 +86,8 @@ class SmartCamModule(SmartModule):
|
|||||||
f" for '{self._module}'"
|
f" for '{self._module}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return query_resp.get(self.QUERY_MODULE_NAME)
|
# Some calls return the data under the module, others not
|
||||||
|
return query_resp.get(self.QUERY_MODULE_NAME, query_resp)
|
||||||
else:
|
else:
|
||||||
found = {key: val for key, val in dev._last_update.items() if key in q}
|
found = {key: val for key, val in dev._last_update.items() if key in q}
|
||||||
for key in q:
|
for key in q:
|
||||||
|
@ -151,6 +151,13 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
"energy_monitoring",
|
"energy_monitoring",
|
||||||
{"igain": 10861, "vgain": 118657},
|
{"igain": 10861, "vgain": 118657},
|
||||||
),
|
),
|
||||||
|
"get_matter_setup_info": (
|
||||||
|
"matter",
|
||||||
|
{
|
||||||
|
"setup_code": "00000000000",
|
||||||
|
"setup_payload": "00:0000000-0000.00.000",
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def send(self, request: str):
|
async def send(self, request: str):
|
||||||
|
@ -44,6 +44,7 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fixture_name = fixture_name
|
self.fixture_name = fixture_name
|
||||||
# When True verbatim will bypass any extra processing of missing
|
# When True verbatim will bypass any extra processing of missing
|
||||||
# methods and is used to test the fixture creation itself.
|
# methods and is used to test the fixture creation itself.
|
||||||
@ -58,6 +59,13 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
# self.child_protocols = self._get_child_protocols()
|
# self.child_protocols = self._get_child_protocols()
|
||||||
self.list_return_size = list_return_size
|
self.list_return_size = list_return_size
|
||||||
|
|
||||||
|
self.components = {
|
||||||
|
comp["name"]: comp["version"]
|
||||||
|
for comp in self.info["getAppComponentList"]["app_component"][
|
||||||
|
"app_component_list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_port(self):
|
def default_port(self):
|
||||||
"""Default port for the transport."""
|
"""Default port for the transport."""
|
||||||
@ -112,6 +120,15 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
info = info[key]
|
info = info[key]
|
||||||
info[set_keys[-1]] = value
|
info[set_keys[-1]] = value
|
||||||
|
|
||||||
|
FIXTURE_MISSING_MAP = {
|
||||||
|
"getMatterSetupInfo": (
|
||||||
|
"matter",
|
||||||
|
{
|
||||||
|
"setup_code": "00000000000",
|
||||||
|
"setup_payload": "00:0000000-0000.00.000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
# Setters for when there's not a simple mapping of setters to getters
|
# Setters for when there's not a simple mapping of setters to getters
|
||||||
SETTERS = {
|
SETTERS = {
|
||||||
("system", "sys", "dev_alias"): [
|
("system", "sys", "dev_alias"): [
|
||||||
@ -217,7 +234,16 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
start_index : start_index + self.list_return_size
|
start_index : start_index + self.list_return_size
|
||||||
]
|
]
|
||||||
return {"result": result, "error_code": 0}
|
return {"result": result, "error_code": 0}
|
||||||
else:
|
if (
|
||||||
|
# FIXTURE_MISSING is for service calls not in place when
|
||||||
|
# SMART fixtures started to be generated
|
||||||
|
missing_result := self.FIXTURE_MISSING_MAP.get(method)
|
||||||
|
) and missing_result[0] in self.components:
|
||||||
|
# Copy to info so it will work with update methods
|
||||||
|
info[method] = copy.deepcopy(missing_result[1])
|
||||||
|
result = copy.deepcopy(info[method])
|
||||||
|
return {"result": result, "error_code": 0}
|
||||||
|
|
||||||
return {"error_code": -1}
|
return {"error_code": -1}
|
||||||
return {"error_code": -1}
|
return {"error_code": -1}
|
||||||
|
|
||||||
|
@ -145,12 +145,21 @@ def filter_fixtures(
|
|||||||
def _component_match(
|
def _component_match(
|
||||||
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
|
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
|
||||||
):
|
):
|
||||||
if (component_nego := fixture_data.data.get("component_nego")) is None:
|
components = {}
|
||||||
return False
|
if component_nego := fixture_data.data.get("component_nego"):
|
||||||
components = {
|
components = {
|
||||||
component["id"]: component["ver_code"]
|
component["id"]: component["ver_code"]
|
||||||
for component in component_nego["component_list"]
|
for component in component_nego["component_list"]
|
||||||
}
|
}
|
||||||
|
if get_app_component_list := fixture_data.data.get("getAppComponentList"):
|
||||||
|
components = {
|
||||||
|
component["name"]: component["version"]
|
||||||
|
for component in get_app_component_list["app_component"][
|
||||||
|
"app_component_list"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if not components:
|
||||||
|
return False
|
||||||
if isinstance(component_filter, str):
|
if isinstance(component_filter, str):
|
||||||
return component_filter in components
|
return component_filter in components
|
||||||
else:
|
else:
|
||||||
|
20
tests/smart/modules/test_matter.py
Normal file
20
tests/smart/modules/test_matter.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from kasa import Module
|
||||||
|
from kasa.smart import SmartDevice
|
||||||
|
|
||||||
|
from ...device_fixtures import parametrize
|
||||||
|
|
||||||
|
matter = parametrize(
|
||||||
|
"has matter", component_filter="matter", protocol_filter={"SMART", "SMARTCAM"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@matter
|
||||||
|
async def test_info(dev: SmartDevice):
|
||||||
|
"""Test matter info."""
|
||||||
|
matter = dev.modules.get(Module.Matter)
|
||||||
|
assert matter
|
||||||
|
assert matter.info
|
||||||
|
setup_code = dev.features.get("matter_setup_code")
|
||||||
|
assert setup_code
|
||||||
|
setup_payload = dev.features.get("matter_setup_payload")
|
||||||
|
assert setup_payload
|
@ -533,3 +533,16 @@ async def test_initialize_modules_required_component(
|
|||||||
|
|
||||||
assert "AvailableComponent" in dev.modules
|
assert "AvailableComponent" in dev.modules
|
||||||
assert "NonExistingComponent" not in dev.modules
|
assert "NonExistingComponent" not in dev.modules
|
||||||
|
|
||||||
|
|
||||||
|
async def test_smartmodule_query():
|
||||||
|
"""Test that a module that doesn't set QUERY_GETTER_NAME has empty query."""
|
||||||
|
|
||||||
|
class DummyModule(SmartModule):
|
||||||
|
pass
|
||||||
|
|
||||||
|
dummy_device = await get_device_for_fixture_protocol(
|
||||||
|
"KS240(US)_1.0_1.0.5.json", "SMART"
|
||||||
|
)
|
||||||
|
mod = DummyModule(dummy_device, "dummy")
|
||||||
|
assert mod.query() == {}
|
||||||
|
Loading…
Reference in New Issue
Block a user