diff --git a/.github/workflows/mypy-validation.yml b/.github/workflows/mypy-validation.yml index d57bf6932dabea..701baf0d989c13 100644 --- a/.github/workflows/mypy-validation.yml +++ b/.github/workflows/mypy-validation.yml @@ -18,18 +18,20 @@ on: push: branches: [ master ] paths: - - 'src/controller/python/matter/ChipDeviceCtrl.py' - - 'src/python_testing/matter_testing_infrastructure/**/*.py' - - 'scripts/tests/chipyaml/paths_finder.py' - - 'scripts/tests/chiptest/**/*.py' - - 'scripts/tests/run_test_suite.py' + - "scripts/tests/chiptest/**/*.py" + - "scripts/tests/chipyaml/paths_finder.py" + - "scripts/tests/run_test_suite.py" + - "src/controller/python/matter/ChipDeviceCtrl.py" + - "src/controller/python/matter/MatterTlvJson.py" + - "src/python_testing/matter_testing_infrastructure/**/*.py" pull_request: paths: - - 'src/controller/python/matter/ChipDeviceCtrl.py' - - 'src/python_testing/matter_testing_infrastructure/**/*.py' - - 'scripts/tests/chipyaml/paths_finder.py' - - 'scripts/tests/chiptest/**/*.py' - - 'scripts/tests/run_test_suite.py' + - "scripts/tests/chiptest/**/*.py" + - "scripts/tests/chipyaml/paths_finder.py" + - "scripts/tests/run_test_suite.py" + - "src/controller/python/matter/ChipDeviceCtrl.py" + - "src/controller/python/matter/MatterTlvJson.py" + - "src/python_testing/matter_testing_infrastructure/**/*.py" jobs: mypy-check: @@ -37,7 +39,7 @@ jobs: if: github.actor != 'restyled-io[bot]' container: - image: ghcr.io/project-chip/chip-build:181 + image: ghcr.io/project-chip/chip-build:191 options: --privileged --sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" steps: @@ -54,34 +56,12 @@ jobs: scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv' out/venv/bin/pip install mypy - - name: Run mypy validation (ChipDeviceCtrl.py) - shell: bash - run: | - set +e - - OUTPUT=$(./scripts/run_in_python_env.sh out/venv "mypy --follow-imports=skip src/controller/python/matter/ChipDeviceCtrl.py" 2>&1) - STATUS=$? - - echo "$OUTPUT" - - ERRORS=$(echo "$OUTPUT" | grep -c '^src/controller/python/matter/ChipDeviceCtrl.py:[0-9]\+: error:') - - if [ "$STATUS" -eq 0 ]; then - echo "No mypy errors found in ChipDeviceCtrl.py" - exit 0 - elif [ "$ERRORS" -gt 0 ]; then - echo "Mypy found $ERRORS error(s) in ChipDeviceCtrl.py" - echo "$OUTPUT" | grep '^src/controller/python/matter/ChipDeviceCtrl.py:[0-9]\+: error:' - exit 1 - else - echo "Mypy exited with error but no errors in ChipDeviceCtrl.py" - exit 0 - fi - - - name: Run mypy validation (MatterJsonTlv.py) + - name: Run mypy validation (without following imports) run: | ./scripts/run_in_python_env.sh out/venv "mypy --follow-imports=skip \ - src/controller/python/matter/MatterTlvJson.py" + src/controller/python/matter/ChipDeviceCtrl.py \ + src/controller/python/matter/MatterTlvJson.py \ + " - name: Run mypy validation run: | @@ -92,22 +72,22 @@ jobs: # Eventually we should just check all files in the chip/testing directory ./scripts/run_in_python_env.sh out/venv "mypy \ + scripts/tests/chiptest/ \ + scripts/tests/chipyaml/paths_finder.py \ + scripts/tests/run_test_suite.py \ src/python_testing/matter_testing_infrastructure/matter/testing/apps.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/pics.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/runner.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py \ src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py \ src/python_testing/matter_testing_infrastructure/matter/testing/commissioning.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py \ src/python_testing/matter_testing_infrastructure/matter/testing/conformance.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py \ - src/python_testing/matter_testing_infrastructure/matter/testing/metadata.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py \ src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py \ - scripts/tests/chipyaml/paths_finder.py \ - scripts/tests/chiptest/ \ - scripts/tests/run_test_suite.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/metadata.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/pics.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/runner.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py \ + src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py \ " # Print a reminder about expanding coverage diff --git a/pyproject.toml b/pyproject.toml index 664ee27247d73b..1ded5fe947b2f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,11 @@ exclude = [ [tool.pyright] reportMissingTypeStubs = "warning" +extraPaths = [ + "./src/python_testing/matter_testing_infrastructure", + "./src/controller/python", + "./scripts/py_matter_yamltests", +] [tool.ruff.lint] select = [ @@ -62,8 +67,9 @@ ignore = [ ] [tool.mypy] -mypy_path = ["matter/typings"] +check_untyped_defs = true +disallow_incomplete_defs = true +explicit_package_bases = true +ignore_missing_imports = true namespace_packages = true warn_unused_configs = true -ignore_missing_imports = true -explicit_package_bases = true diff --git a/scripts/py_matter_yamltests/matter/yamltests/parser.py b/scripts/py_matter_yamltests/matter/yamltests/parser.py index fac18ee5a9eaa0..3795597599c45e 100644 --- a/scripts/py_matter_yamltests/matter/yamltests/parser.py +++ b/scripts/py_matter_yamltests/matter/yamltests/parser.py @@ -1453,8 +1453,8 @@ def __next__(self) -> TestStep: @dataclass class TestParserConfig: - pics: str = None - definitions: SpecDefinitions = None + pics: str | None = None + definitions: SpecDefinitions | None = None config_override: dict = field(default_factory=dict) diff --git a/scripts/tests/chiptest/__init__.py b/scripts/tests/chiptest/__init__.py index 9b6853b95f6e5f..27bc6bd169ca01 100644 --- a/scripts/tests/chiptest/__init__.py +++ b/scripts/tests/chiptest/__init__.py @@ -17,9 +17,9 @@ import json import logging import os +from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path -from typing import Iterator, Set import yaml @@ -77,7 +77,7 @@ def _IsValidYamlTest(name: str) -> bool: return name not in INVALID_TESTS -def _LoadManualTestsJson(json_file_path: str) -> Iterator[str]: +def _LoadManualTestsJson(json_file_path: str) -> Iterable[str]: with open(json_file_path) as f: data = json.load(f) for c in data["collection"]: @@ -85,7 +85,7 @@ def _LoadManualTestsJson(json_file_path: str) -> Iterator[str]: yield f"{name}.yaml" -def _GetManualTests() -> Set[str]: +def _GetManualTests() -> set[str]: manualtests: set[str] = set() # Flagged as manual from: src/app/tests/suites/manualTests.json @@ -95,7 +95,7 @@ def _GetManualTests() -> Set[str]: return manualtests -def _GetFlakyTests() -> Set[str]: +def _GetFlakyTests() -> set[str]: """List of flaky tests. While this list is empty, it remains here in case we need to quickly add a new test @@ -104,7 +104,7 @@ def _GetFlakyTests() -> Set[str]: return set() -def _GetSlowTests() -> Set[str]: +def _GetSlowTests() -> set[str]: """Generally tests using sleep() a bit too freely. 10s seems like a good threshold to consider something slow @@ -139,7 +139,7 @@ def _GetSlowTests() -> Set[str]: } -def _GetExtraSlowTests() -> Set[str]: +def _GetExtraSlowTests() -> set[str]: """Generally tests using sleep() so much they should never run in CI. 1 minute seems like a good threshold to consider something extra slow @@ -149,7 +149,7 @@ def _GetExtraSlowTests() -> Set[str]: } -def _GetInDevelopmentTests() -> Set[str]: +def _GetInDevelopmentTests() -> set[str]: """Tests that fail in YAML for some reason.""" return { "Test_TC_PSCFG_1_1.yaml", # Power source configuration cluster is deprecated and removed from all-clusters @@ -169,14 +169,14 @@ def _GetInDevelopmentTests() -> Set[str]: } -def _GetChipToolUnsupportedTests() -> Set[str]: +def _GetChipToolUnsupportedTests() -> set[str]: """Tests that fail in chip-tool for some reason""" return { "TestDiagnosticLogsDownloadCommand", # chip-tool does not implement a bdx download command. } -def _GetDarwinFrameworkToolUnsupportedTests() -> Set[str]: +def _GetDarwinFrameworkToolUnsupportedTests() -> set[str]: """Tests that fail in darwin-framework-tool for some reason""" return { "DL_LockUnlock", # darwin-framework-tool does not currently support reading or subscribing to Events @@ -233,7 +233,7 @@ def _GetDarwinFrameworkToolUnsupportedTests() -> Set[str]: } -def _GetReplUnsupportedTests() -> Set[str]: +def _GetReplUnsupportedTests() -> set[str]: """Tests that fail in matter-repl for some reason""" return { "Test_AddNewFabricFromExistingFabric.yaml", # matter-repl does not support GetCommissionerRootCertificate and IssueNocChain command @@ -255,7 +255,7 @@ def _GetReplUnsupportedTests() -> Set[str]: } -def _GetPurposefulFailureTests() -> Set[str]: +def _GetPurposefulFailureTests() -> set[str]: """Tests that fail in YAML on purpose.""" return { "TestPurposefulFailureEqualities.yaml", @@ -264,7 +264,7 @@ def _GetPurposefulFailureTests() -> Set[str]: } -def _AllYamlTests(): +def _AllYamlTests() -> Iterable[Path]: yaml_test_suite_path = Path(_YAML_TEST_SUITE_PATH) if not yaml_test_suite_path.exists(): @@ -298,7 +298,8 @@ def _TargetsForYaml(yaml_path: Path) -> list[TestTarget]: return targets -def _AllFoundYamlTests(treat_repl_unsupported_as_in_development: bool, treat_dft_unsupported_as_in_development: bool, treat_chip_tool_unsupported_as_in_development: bool, use_short_run_name: bool): +def _AllFoundYamlTests(treat_repl_unsupported_as_in_development: bool, treat_dft_unsupported_as_in_development: bool, + treat_chip_tool_unsupported_as_in_development: bool, use_short_run_name: bool) -> Iterable[TestDefinition]: """ use_short_run_name should be true if we want the run_name to be "Test_ABC" instead of "some/path/Test_ABC.yaml" """ @@ -357,16 +358,16 @@ def _AllFoundYamlTests(treat_repl_unsupported_as_in_development: bool, treat_dft ) -def AllReplYamlTests(): +def AllReplYamlTests() -> Iterable[TestDefinition]: for test in _AllFoundYamlTests(treat_repl_unsupported_as_in_development=True, treat_dft_unsupported_as_in_development=False, treat_chip_tool_unsupported_as_in_development=False, use_short_run_name=False): yield test -def AllChipToolYamlTests(use_short_run_name: bool = True): +def AllChipToolYamlTests(use_short_run_name: bool = True) -> Iterable[TestDefinition]: for test in _AllFoundYamlTests(treat_repl_unsupported_as_in_development=False, treat_dft_unsupported_as_in_development=False, treat_chip_tool_unsupported_as_in_development=True, use_short_run_name=use_short_run_name): yield test -def AllDarwinFrameworkToolYamlTests(): +def AllDarwinFrameworkToolYamlTests() -> Iterable[TestDefinition]: for test in _AllFoundYamlTests(treat_repl_unsupported_as_in_development=False, treat_dft_unsupported_as_in_development=True, treat_chip_tool_unsupported_as_in_development=False, use_short_run_name=True): yield test diff --git a/scripts/tests/chiptest/darwin.py b/scripts/tests/chiptest/darwin.py index 6fbe68777ab9a7..d85d140151a519 100644 --- a/scripts/tests/chiptest/darwin.py +++ b/scripts/tests/chiptest/darwin.py @@ -12,13 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. +import subprocess from typing import IO, Any from .runner import Executor, LogPipe, SubprocessInfo class DarwinExecutor(Executor): - def run(self, subproc: SubprocessInfo, stdin: IO[Any] | None = None, stdout: IO[Any] | LogPipe | None = None, stderr: IO[Any] | LogPipe | None = None): + def run(self, subproc: SubprocessInfo, stdin: IO[Any] | None = None, stdout: IO[Any] | LogPipe | None = None, + stderr: IO[Any] | LogPipe | None = None) -> subprocess.Popen[bytes]: # Try harder to avoid any stdout buffering in our tests wrapped = subproc.wrap_with('stdbuf', '-o0', '-i0') return super().run(wrapped, stdin, stdout, stderr) diff --git a/scripts/tests/chiptest/linux.py b/scripts/tests/chiptest/linux.py index 936eca8d83f4c2..bedf87c541ca86 100644 --- a/scripts/tests/chiptest/linux.py +++ b/scripts/tests/chiptest/linux.py @@ -20,6 +20,7 @@ import logging import os +import subprocess from typing import IO, Any from chiptest.runner import Executor, LogPipe, SubprocessInfo @@ -53,7 +54,7 @@ def __init__(self, ns: IsolatedNetworkNamespace): self.ns = ns def run(self, subproc: SubprocessInfo, stdin: IO[Any] | None = None, stdout: IO[Any] | LogPipe | None = None, - stderr: IO[Any] | LogPipe | None = None): + stderr: IO[Any] | LogPipe | None = None) -> subprocess.Popen[bytes]: try: subprocess_ns = self.ns.netns_for_subprocess_kind(subproc.kind) wrapped = subproc.wrap_with(*subprocess_ns.netns_cmd_wrapper) diff --git a/scripts/tests/chiptest/runner.py b/scripts/tests/chiptest/runner.py index bdd81be7ce5cab..31c4cb3909e2db 100644 --- a/scripts/tests/chiptest/runner.py +++ b/scripts/tests/chiptest/runner.py @@ -23,7 +23,7 @@ import subprocess import threading from contextlib import suppress -from typing import IO, TYPE_CHECKING, Any, Protocol +from typing import IO, TYPE_CHECKING, Any, Match, Protocol import python_path @@ -68,18 +68,18 @@ def CapturedLogContains(self, txt: str, index: int = 0) -> tuple[bool, int]: return True, index + i return False, len(self.captured_logs) - def FindLastMatchingLine(self, matcher: str): + def FindLastMatchingLine(self, matcher: str) -> Match[str] | None: for line in reversed(self.captured_logs): match = re.match(matcher, line) if match: return match return None - def fileno(self): + def fileno(self) -> int: """Return the write file descriptor of the pipe.""" return self.fd_write - def run(self): + def run(self) -> None: """Run the thread, logging everything.""" while True: try: @@ -96,7 +96,7 @@ def run(self): self.capture_delegate.Log(self.name, line) self.reader.close() - def close(self): + def close(self) -> None: """Close the write end of the pipe.""" os.close(self.fd_write) @@ -115,7 +115,7 @@ def __init__(self, timeout_seconds: int | None): self.timeout_seconds = timeout_seconds self.timed_out = False - def __wait(self, process: Process, userdata: AppsRegister | None): + def __wait(self, process: Process, userdata: AppsRegister | None) -> None: if userdata is None: # We're the main process for this wait queue. timeout = self.timeout_seconds @@ -130,12 +130,12 @@ def __wait(self, process: Process, userdata: AppsRegister | None): process.wait() self.queue.put((process, userdata)) - def add_process(self, process: Process, userdata: AppsRegister | None = None): + def add_process(self, process: Process, userdata: AppsRegister | None = None) -> None: t = threading.Thread(target=self.__wait, args=(process, userdata)) t.daemon = True t.start() - def get(self): + def get(self) -> tuple[Process, AppsRegister | None]: return self.queue.get() @@ -145,7 +145,8 @@ class Executor: def __init__(self) -> None: self._processes: queue.Queue[subprocess.Popen[bytes]] = queue.Queue() - def run(self, subproc: SubprocessInfo, stdin: IO[Any] | None = None, stdout: IO[Any] | LogPipe | None = None, stderr: IO[Any] | LogPipe | None = None): + def run(self, subproc: SubprocessInfo, stdin: IO[Any] | None = None, stdout: IO[Any] | LogPipe | None = None, + stderr: IO[Any] | LogPipe | None = None) -> subprocess.Popen[bytes]: # Seems like LogPipe has all what Popen needs to perceive it as stdout/stderr, # but mypy doesn't think the same. self._processes.put(process := subprocess.Popen(subproc.to_cmd(), stdin=stdin, diff --git a/scripts/tests/chiptest/test_definition.py b/scripts/tests/chiptest/test_definition.py index 842d894fec5212..49eb0731cfda7f 100644 --- a/scripts/tests/chiptest/test_definition.py +++ b/scripts/tests/chiptest/test_definition.py @@ -59,7 +59,7 @@ def __repr__(self) -> str: return repr(self.subproc) @property - def returncode(self): + def returncode(self) -> int | None: """Exposes return code of the underlying process, so that common code can be used between subprocess.Popen and Apps. """ @@ -107,7 +107,7 @@ def factoryReset(self) -> bool: return True - def waitForApplicationUp(self): + def waitForApplicationUp(self) -> None: # Watch for both mDNS advertisement start as well as event loop start. # These two messages can appear in any order depending on the implementation. # Waiting for both makes the startup detection more robust. @@ -122,7 +122,7 @@ def waitForApplicationUp(self): self.__waitFor(what) - def waitForMessage(self, message: str, timeoutInSeconds: float = 10): + def waitForMessage(self, message: str, timeoutInSeconds: float = 10) -> bool: self.__waitFor([message], timeoutInSeconds=timeoutInSeconds) return True @@ -164,7 +164,7 @@ def __startServer(self) -> tuple[subprocess.Popen[bytes], LogPipe, LogPipe]: self.kvsPathSet.add(value) return self.runner.RunSubprocess(subproc, name='APP ', wait=False) - def __waitFor(self, patterns: Iterable[str], timeoutInSeconds: float = 10): + def __waitFor(self, patterns: Iterable[str], timeoutInSeconds: float = 10) -> None: """ Wait for all provided pattern strings to appear in the process output pipe (capture log). """ @@ -199,14 +199,14 @@ def allPatternsFound() -> int | None: self.lastLogIndex = lastLogIndex + 1 log.debug('Success waiting for: %r', patterns) - def __updateSetUpCode(self): + def __updateSetUpCode(self) -> None: assert self.outpipe is not None, "__updateSetUpCode needs to happen after __startServer" qrLine = self.outpipe.FindLastMatchingLine('.*SetupQRCode: *\\[(.*)]') if not qrLine: raise RuntimeError("Unable to find QR code") self.setupCode = qrLine.group(1) - def __terminateProcess(self): + def __terminateProcess(self) -> bool: """ Returns False if the process existed and had a nonzero exit code. """ @@ -291,18 +291,17 @@ def get(self, target_name: str) -> Path | None: pass -class SubprocessInfoRepo(dict): +class SubprocessInfoRepo(dict[str, SubprocessInfo]): # We don't want to explicitly reference PathsFinder type because we # don't want to create a dependency on the diskcache module which PathsFinder imports. # Instead we just want a dict-like object - def __init__(self, paths: PathsFinderProto, - subproc_knowhow: MappingProxyType[str, KnownSubprocessEntry] = BUILTIN_SUBPROC_DATA, - *args, **kwargs): + def __init__(self, paths: PathsFinderProto, subproc_knowhow: MappingProxyType[str, KnownSubprocessEntry] = BUILTIN_SUBPROC_DATA, + *args: typing.Any, **kwargs: typing.Any): super().__init__(*args, **kwargs) self.paths = paths self.subproc_knowhow = subproc_knowhow - def addSpec(self, spec: str, kind: SubprocessKind | None = None): + def addSpec(self, spec: str, kind: SubprocessKind | None = None) -> None: """Add a path to the repo as specified on the command line""" el = spec.split(':') if len(el) == 2: @@ -324,14 +323,14 @@ def addSpec(self, spec: str, kind: SubprocessKind | None = None): s = s.wrap_with('python3') self[key] = s - def missing_keys(self): + def missing_keys(self) -> list[str]: """ Return a list of keys for tools or apps missing (not specified) based on the know-how dictionary. """ return [k for k in self.subproc_knowhow if k not in self] - def discover(self): + def discover(self) -> None: """ Try to discover paths to all apps and tools in the know-how which we are still missing. Reuse the `require` method but ignore failures, we expect the test-cases to fail if they @@ -349,7 +348,7 @@ def discover(self): log.warning("Exception while trying to discover '%s': %r", key, e) log.info("Discovery of %d paths took %.2f seconds", discovered_count, time.time() - start_ts) - def require(self, key: str, kind: SubprocessKind | None = None, target_name: str | None = None): + def require(self, key: str, kind: SubprocessKind | None = None, target_name: str | None = None) -> SubprocessInfo: """ Indicate that a subprocess path is required. Throw exception if it's not already in the repo and can't be discovered using the paths finder. @@ -392,7 +391,7 @@ def __init__(self) -> None: self.lock = threading.Lock() self.captures: list[CaptureLine] = [] - def Log(self, source: str, line: str): + def Log(self, source: str, line: str) -> None: with self.lock: self.captures.append(CaptureLine( when=datetime.now(), @@ -400,7 +399,7 @@ def Log(self, source: str, line: str): line=line.strip('\n') )) - def LogContents(self): + def LogContents(self) -> None: log.error("================ CAPTURED LOG START ==================") with self.lock: for entry in self.captures: @@ -424,7 +423,7 @@ class TestTag(StrEnum): EXTRA_SLOW = auto() # test uses Sleep and is generally _very_ slow (>= 60s is a typical threshold) PURPOSEFUL_FAILURE = auto() # test fails on purpose - def to_s(self): + def to_s(self) -> str: for (k, v) in TestTag.__members__.items(): if self == v: return k @@ -469,7 +468,7 @@ def Run(self, runner: Runner, apps_register: AppsRegister, subproc_info_repo: Su thread_ba_host: str | None = None, thread_ba_port: int | None = None, wifipaf_wifi: bool = False - ): + ) -> None: """ Executes the given test case using the provided runner for execution. Will iterate and execute every target. @@ -488,10 +487,10 @@ def _RunImpl(self, target: TestTarget, runner: Runner, apps_register: AppsRegist op_network: str = 'WiFi', thread_ba_host: str | None = None, thread_ba_port: int | None = None, - wifipaf_wifi: bool = False): + wifipaf_wifi: bool = False) -> None: runner.capture_delegate = ExecutionCapture() - tool_storage_dir = None + tool_storage_dir: str | None = None loggedCapturedLogs = False try: diff --git a/scripts/tests/chipyaml/paths_finder.py b/scripts/tests/chipyaml/paths_finder.py index aba542f81ad89c..41b20fe2817bb2 100755 --- a/scripts/tests/chipyaml/paths_finder.py +++ b/scripts/tests/chipyaml/paths_finder.py @@ -35,7 +35,9 @@ def __init__(self, roots: typing.List[str] = [DEFAULT_CHIP_ROOT]): def get(self, target_name: str) -> Path | None: path = _PATHS_CACHE.get(target_name) - if path and path.is_file(): + if isinstance(path, str): + path = Path(path) + if path and isinstance(path, Path) and path.is_file(): return path if path: @@ -59,12 +61,12 @@ def _find_from_root(self, root: str, target_name: str) -> Path | None: @click.group() -def finder(): +def finder() -> None: pass @finder.command() -def view(): +def view() -> None: """View the cache entries.""" for name in _PATHS_CACHE: print(click.style(f'{name}', bold=True) + f':\t{_PATHS_CACHE[name]}') @@ -73,29 +75,28 @@ def view(): @finder.command() @click.argument('key', type=str) @click.argument('value', type=str) -def add(key: str, value: str): +def add(key: str, value: str | Path) -> None: """Add a cache entry.""" - _PATHS_CACHE[key] = value + _PATHS_CACHE[key] = Path(value) @finder.command() @click.argument('name', type=str) -def delete(name: str): +def delete(name: str) -> None: """Delete a cache entry.""" if name in _PATHS_CACHE: del _PATHS_CACHE[name] @finder.command() -def reset(): +def reset() -> None: """Delete all cache entries.""" - for name in _PATHS_CACHE: - del _PATHS_CACHE[name] + _PATHS_CACHE.clear() @finder.command() @click.argument('name', type=str) -def search(name: str): +def search(name: str) -> None: """Search for a target and add it to the cache.""" paths_finder = PathsFinder() path = paths_finder.get(name) diff --git a/src/controller/python/matter/ChipDeviceCtrl.py b/src/controller/python/matter/ChipDeviceCtrl.py index 87911d863cd166..5d5b756d0d0de8 100644 --- a/src/controller/python/matter/ChipDeviceCtrl.py +++ b/src/controller/python/matter/ChipDeviceCtrl.py @@ -31,6 +31,7 @@ import asyncio import builtins import concurrent.futures +import contextlib import copy import ctypes import enum @@ -50,12 +51,13 @@ from . import discovery from .bdx import Bdx from .clusters import Attribute as ClusterAttribute -from .clusters import ClusterObjects as ClusterObjects +from .clusters import ClusterObjects from .clusters import Command as ClusterCommand from .clusters.CHIPClusters import ChipClusters from .crypto import p256keypair from .interaction_model import SessionParameters, SessionParametersStruct from .native import PyChipError +from .tlv import uint __all__ = ["ChipDeviceController", "CommissioningParameters", "AttributeReadRequest", "AttributeReadRequestList", "SubscriptionTargetList"] @@ -63,59 +65,47 @@ # Type aliases for ReadAttribute method to improve type safety AttributeReadRequest = typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[int], # Endpoint - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster]], - # Wildcard endpoint, Cluster + Attribute present - typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - typing.Tuple[int, typing.Type[ClusterObjects.Cluster] - ], # Wildcard attribute id - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - ClusterAttribute.TypedAttributePath # Directly specified attribute path + tuple[int], # Empty tuple, all wildcard + tuple[type[ClusterObjects.Cluster]], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterAttributeDescriptor]], # Wildcard endpoint, Cluster + Attribute present + tuple[int, type[ClusterObjects.Cluster]], # Wildcard attribute id + tuple[int, type[ClusterObjects.ClusterAttributeDescriptor]], # Concrete path + ClusterAttribute.TypedAttributePath # Directly specified typed attribute path ] -AttributeReadRequestList = typing.Optional[typing.List[AttributeReadRequest]] +AttributeReadRequestList = list[AttributeReadRequest] | None + +AttributeWriteRequest = typing.Union[ + tuple[int, ClusterObjects.ClusterAttributeDescriptor], + tuple[int, ClusterObjects.ClusterAttributeDescriptor, int] +] +AttributeWriteRequestList = list[AttributeWriteRequest] + # Type alias for subscription target specifications -SubscriptionTargetList = typing.List[typing.Tuple[int, - typing.Union[ClusterObjects.Cluster, ClusterObjects.ClusterAttributeDescriptor]]] +SubscriptionTargetList = list[tuple[int, ClusterObjects.Cluster | ClusterObjects.ClusterAttributeDescriptor]] # Defined in $CHIP_ROOT/src/lib/core/CHIPError.h CHIP_ERROR_TIMEOUT: int = 50 -_RCACCallbackType = CFUNCTYPE(None, POINTER(c_uint8), c_size_t) - LOGGER = logging.getLogger(__name__) _DevicePairingDelegate_OnPairingCompleteFunct = CFUNCTYPE(None, PyChipError) _DeviceUnpairingCompleteFunct = CFUNCTYPE(None, c_uint64, PyChipError) -_DevicePairingDelegate_OnCommissioningCompleteFunct = CFUNCTYPE( - None, c_uint64, PyChipError) -_DevicePairingDelegate_OnOpenWindowCompleteFunct = CFUNCTYPE( - None, c_uint64, c_uint32, c_char_p, c_char_p, PyChipError) - -DevicePairingDelegate_OnCommissioningStatusUpdateFunct: typing.TypeAlias = typing.Callable[ - [int, int, PyChipError], - None, -] -_DevicePairingDelegate_OnCommissioningStatusUpdateFunct = CFUNCTYPE( - None, c_uint64, c_uint8, PyChipError) +_DevicePairingDelegate_OnCommissioningCompleteFunct = CFUNCTYPE(None, c_uint64, PyChipError) +_DevicePairingDelegate_OnOpenWindowCompleteFunct = CFUNCTYPE(None, c_uint64, c_uint32, c_char_p, c_char_p, PyChipError) -DevicePairingDelegate_OnCommissioningStageStartFunct: typing.TypeAlias = typing.Callable[ - [int, bytes], - None, -] -_DevicePairingDelegate_OnCommissioningStageStartFunct = CFUNCTYPE( - None, c_uint64, c_char_p) +DevicePairingDelegate_OnCommissioningStatusUpdateFunct = typing.Callable[[int, int, PyChipError], None] +_DevicePairingDelegate_OnCommissioningStatusUpdateFunct = CFUNCTYPE(None, c_uint64, c_uint8, PyChipError) -_DevicePairingDelegate_OnFabricCheckFunct = CFUNCTYPE( - None, c_uint64) +DevicePairingDelegate_OnCommissioningStageStartFunct = typing.Callable[[int, bytes], None] +_DevicePairingDelegate_OnCommissioningStageStartFunct = CFUNCTYPE(None, c_uint64, c_char_p) + +_DevicePairingDelegate_OnFabricCheckFunct = CFUNCTYPE(None, c_uint64) # void (*)(Device *, CHIP_ERROR). # -# CHIP_ERROR is actually signed, so using c_uint32 is weird, but everything -# else seems to do it. +# CHIP_ERROR is actually signed, so using c_uint32 is weird, but everything else seems to do it. _DeviceAvailableCallbackFunct = CFUNCTYPE(None, py_object, c_void_p, PyChipError) _IssueNOCChainCallbackPythonCallbackFunct = CFUNCTYPE( @@ -143,37 +133,43 @@ class CommissioningParameters: @dataclass class NOCChain: - nocBytes: typing.Optional[bytes] - icacBytes: typing.Optional[bytes] - rcacBytes: typing.Optional[bytes] - ipkBytes: typing.Optional[bytes] + nocBytes: bytes | None + icacBytes: bytes | None + rcacBytes: bytes | None + ipkBytes: bytes | None adminSubject: int @dataclass class ICDRegistrationParameters: - symmetricKey: typing.Optional[bytes] - checkInNodeId: typing.Optional[int] - monitoredSubject: typing.Optional[int] - stayActiveMs: typing.Optional[int] - clientType: typing.Optional[Clusters.IcdManagement.Enums.ClientTypeEnum] + symmetricKey: bytes + checkInNodeId: int + monitoredSubject: int + stayActiveMs: int + clientType: Clusters.IcdManagement.Enums.ClientTypeEnum class CStruct(Structure): - _fields_ = [('symmetricKey', c_char_p), ('symmetricKeyLength', c_size_t), ('checkInNodeId', - c_uint64), ('monitoredSubject', c_uint64), ('stayActiveMsec', c_uint32), ('clientType', c_uint8)] + _fields_ = [('symmetricKey', c_char_p), ('symmetricKeyLength', c_size_t), ('checkInNodeId', c_uint64), + ('monitoredSubject', c_uint64), ('stayActiveMsec', c_uint32), ('clientType', c_uint8)] def to_c(self): - return ICDRegistrationParameters.CStruct(self.symmetricKey, len(self.symmetricKey), self.checkInNodeId, self.monitoredSubject, self.stayActiveMs, self.clientType.value) + return ICDRegistrationParameters.CStruct(self.symmetricKey, len(self.symmetricKey), self.checkInNodeId, + self.monitoredSubject, self.stayActiveMs, self.clientType.value) + + +class DeviceAvailableClosureProtocol(typing.Protocol): + def deviceAvailable(self, device: int, err: PyChipError) -> None: ... @_DeviceAvailableCallbackFunct -def _DeviceAvailableCallback(closure, device, err): +def _DeviceAvailableCallback(closure: DeviceAvailableClosureProtocol, device: int, err: PyChipError) -> None: closure.deviceAvailable(device, err) @_IssueNOCChainCallbackPythonCallbackFunct -def _IssueNOCChainCallbackPythonCallback(devCtrl, status: PyChipError, noc: c_void_p, nocLen: int, icac: c_void_p, - icacLen: int, rcac: c_void_p, rcacLen: int, ipk: c_void_p, ipkLen: int, adminSubject: int): +def _IssueNOCChainCallbackPythonCallback(devCtrl: ChipDeviceController, status: PyChipError, noc: c_void_p, nocLen: int, + icac: c_void_p, icacLen: int, rcac: c_void_p, rcacLen: int, ipk: c_void_p, ipkLen: int, + adminSubject: int) -> None: nocChain = NOCChain(None, None, None, None, 0) if status.is_success: @@ -212,11 +208,12 @@ def __eq__(self, other): _OnCheckInCompleteFunct = CFUNCTYPE(None, ScopedNodeId) _OnCheckInCompleteWaitListLock = threading.Lock() -_OnCheckInCompleteWaitList: typing.Dict[ScopedNodeId, set] = {} +_OnCheckInCompleteWaitListCallback = typing.Callable[[ScopedNodeId], None] +_OnCheckInCompleteWaitList: dict[ScopedNodeId, set[_OnCheckInCompleteWaitListCallback]] = {} @_OnCheckInCompleteFunct -def _OnCheckInComplete(scopedNodeId: ScopedNodeId): +def _OnCheckInComplete(scopedNodeId: ScopedNodeId) -> None: callbacks = [] with _OnCheckInCompleteWaitListLock: callbacks = list(_OnCheckInCompleteWaitList.get(scopedNodeId, set())) @@ -225,7 +222,7 @@ def _OnCheckInComplete(scopedNodeId: ScopedNodeId): callback(scopedNodeId) -def RegisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Callable[[ScopedNodeId], None]): +def RegisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: _OnCheckInCompleteWaitListCallback) -> None: ''' Registers a callback when the device with given (fabric index, node id) becomes active. Does nothing if the callback is already registered. @@ -236,7 +233,7 @@ def RegisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Callab _OnCheckInCompleteWaitList[scopedNodeId] = waitList -def UnregisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Callable[[ScopedNodeId], None]): +def UnregisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: _OnCheckInCompleteWaitListCallback) -> None: ''' Unregisters a callback when the device with given (fabric index, node id) becomes active. Does nothing if the callback has not been registered. @@ -245,7 +242,7 @@ def UnregisterOnActiveCallback(scopedNodeId: ScopedNodeId, callback: typing.Call _OnCheckInCompleteWaitList.get(scopedNodeId, set()).remove(callback) -async def WaitForCheckIn(scopedNodeId: ScopedNodeId, timeoutSeconds: float): +async def WaitForCheckIn(scopedNodeId: ScopedNodeId, timeoutSeconds: float) -> None: ''' Waits for a device becomes active. Returns: @@ -254,8 +251,8 @@ async def WaitForCheckIn(scopedNodeId: ScopedNodeId, timeoutSeconds: float): eventLoop = asyncio.get_running_loop() future = eventLoop.create_future() - def OnCheckInCallback(scopedNodeId: ScopedNodeId): - def callback(future: asyncio.Future): + def OnCheckInCallback(scopedNodeId: ScopedNodeId) -> None: + def callback(future: asyncio.Future) -> None: if not future.done(): future.set_result(None) eventLoop.call_soon_threadsafe(callback, future) @@ -291,19 +288,19 @@ class CallbackContext: def __init__(self, lock: asyncio.Lock) -> None: self._lock = lock - self._future = None + self._future: concurrent.futures.Future | None = None - async def __aenter__(self): + async def __aenter__(self) -> concurrent.futures.Future: await self._lock.acquire() self._future = concurrent.futures.Future() - return self + return self._future @property - def future(self) -> typing.Optional[concurrent.futures.Future]: + def future(self) -> concurrent.futures.Future | None: return self._future async def __aexit__(self, exc_type, exc_value, traceback): - if not self._future.done(): + if self._future is not None and not self._future.done(): # In case the initial call (which sets up for the callback) fails, # the future will never be used actually. So just cancel it here # for completeness, in case somebody is expecting it to be completed. @@ -322,17 +319,17 @@ def __init__(self, devCtrl: ChipDeviceControllerBase, lock: asyncio.Lock) -> Non super().__init__(lock) self._devCtrl = devCtrl - async def __aenter__(self): - await super().__aenter__() + async def __aenter__(self) -> concurrent.futures.Future: + future = await super().__aenter__() self._devCtrl._fabricCheckNodeId = -1 - return self + return future async def __aexit__(self, exc_type, exc_value, traceback): await super().__aexit__(exc_type, exc_value, traceback) class CommissionableNode(discovery.CommissionableNode): - def SetDeviceController(self, devCtrl: 'ChipDeviceController'): + def SetDeviceController(self, devCtrl: ChipDeviceController) -> None: self._devCtrl = devCtrl def Commission(self, nodeId: int, setupPinCode: int) -> int: @@ -358,7 +355,7 @@ def __rich_repr__(self): yield k, self.__dict__[k] -class DeviceProxyWrapper(): +class DeviceProxyWrapper: ''' Encapsulates a pointer to OperationalDeviceProxy on the c++ side that needs to be freed when DeviceProxyWrapper goes out of scope. There is a potential issue where if this is copied around that a double free will occur, but how this is used today @@ -369,18 +366,21 @@ class DeviceProxyType(enum.Enum): OPERATIONAL = enum.auto() COMMISSIONEE = enum.auto() - def __init__(self, deviceProxy: ctypes.c_void_p, proxyType, dmLib=None): + def __init__(self, deviceProxy: ctypes.c_void_p, proxyType: DeviceProxyType, dmLib: CDLL): self._deviceProxy = deviceProxy self._dmLib = dmLib self._proxyType = proxyType def __del__(self): # Commissionee device proxies are owned by the DeviceCommissioner. See #33031 - if (self._proxyType == self.DeviceProxyType.OPERATIONAL and self._dmLib is not None and hasattr(builtins, 'chipStack') and builtins.chipStack is not None): + if (self._proxyType == self.DeviceProxyType.OPERATIONAL and (dmlib := self._dmLib) is not None + # type: ignore[attr-defined] # 'chipStack' is dynamically added; referred to in DeviceProxyWrapper class __del__ method + and hasattr(builtins, 'chipStack') and builtins.chipStack is not None): # This destructor is called from any threading context, including on the Matter threading context. # So, we cannot call chipStack.Call or chipStack.CallAsyncWithCompleteCallback which waits for the posted work to # actually be executed. Instead, we just post/schedule the work and move on. - builtins.chipStack.PostTaskOnChipThread(lambda: self._dmLib.pychip_FreeOperationalDeviceProxy(self._deviceProxy)) + builtins.chipStack.PostTaskOnChipThread( # type: ignore[attr-defined] # 'chipStack' is dynamically added; referred to in DeviceProxyWrapper class __del__ method + lambda: dmlib.pychip_FreeOperationalDeviceProxy(self._deviceProxy)) @property def deviceProxy(self) -> ctypes.c_void_p: @@ -469,13 +469,14 @@ def closeTCPConnectionWithPeer(self): self._dmLib.pychip_CloseTCPConnectionWithPeer.argtypes = [ctypes.c_void_p] self._dmLib.pychip_CloseTCPConnectionWithPeer.restype = PyChipError - builtins.chipStack.Call( + builtins.chipStack.Call( # type: ignore[attr-defined] # 'chipStack' is dynamically added; referred to in DeviceProxyWrapper class __del__ method lambda: self._dmLib.pychip_CloseTCPConnectionWithPeer(self._deviceProxy) ).raise_on_error() DiscoveryFilterType: typing.TypeAlias = discovery.FilterType DiscoveryType: typing.TypeAlias = discovery.DiscoveryType +ResponseT = typing.TypeVar("ResponseT", bound=ClusterObjects.ClusterCommand) class ChipDeviceControllerBase(): @@ -485,18 +486,17 @@ def __init__(self, name: str = ''): self.devCtrl = None # 'chipStack' is dynamically added; referred to in DeviceProxyWrapper class __del__ method self._ChipStack = builtins.chipStack # type: ignore[attr-defined] - self._dmLib: typing.Any = None self._isActive = False - self._InitLib() + self._dmLib = self._InitLib() pairingDelegate = c_void_p(None) devCtrl = c_void_p(None) - self.pairingDelegate = pairingDelegate + self.pairingDelegate: c_void_p | None = pairingDelegate self.devCtrl = devCtrl self.name = name - self._fabricCheckNodeId = -1 + self._fabricCheckNodeId: int = -1 # 'chipStack' is dynamically added; referred to in DeviceProxyWrapper class __del__ method self._Cluster = ChipClusters(builtins.chipStack) # type: ignore[attr-defined] @@ -506,8 +506,8 @@ def __init__(self, name: str = ''): self._unpair_device_context: CallbackContext = CallbackContext(asyncio.Lock()) self._pase_establishment_context: CallbackContext = CallbackContext(self._commissioning_lock) - def _set_dev_ctrl(self, devCtrl, pairingDelegate): - def HandleCommissioningComplete(nodeId: int, err: PyChipError): + def _set_dev_ctrl(self, devCtrl: c_void_p, pairingDelegate: c_void_p) -> None: + def HandleCommissioningComplete(nodeId: int, err: PyChipError) -> None: if err.is_success: LOGGER.info("Commissioning complete") else: @@ -528,11 +528,12 @@ def HandleCommissioningComplete(nodeId: int, err: PyChipError): else: self._commissioning_context.future.set_exception(err.to_exception()) - def HandleFabricCheck(nodeId: int): + def HandleFabricCheck(nodeId: int) -> None: self._fabricCheckNodeId = nodeId def HandleOpenWindowComplete(nodeId: int, setupPinCode: int, setupManualCode: bytes, setupQRCode: bytes, err: PyChipError) -> None: + commissioningParameters: CommissioningParameters | None = None if err.is_success: LOGGER.info("Open Commissioning Window complete setting node ID 0x%016X pincode to %d", nodeId, setupPinCode) commissioningParameters = CommissioningParameters( @@ -549,7 +550,7 @@ def HandleOpenWindowComplete(nodeId: int, setupPinCode: int, setupManualCode: by else: self._open_window_context.future.set_exception(err.to_exception()) - def HandleUnpairDeviceComplete(nodeId: int, err: PyChipError): + def HandleUnpairDeviceComplete(nodeId: int, err: PyChipError) -> None: if err.is_success: LOGGER.info("Successfully unpaired device with node ID 0x%016X", nodeId) else: @@ -564,7 +565,7 @@ def HandleUnpairDeviceComplete(nodeId: int, err: PyChipError): else: self._unpair_device_context.future.set_exception(err.to_exception()) - def HandlePASEEstablishmentComplete(err: PyChipError): + def HandlePASEEstablishmentComplete(err: PyChipError) -> None: if not err.is_success: LOGGER.warning("Failed to establish secure session to device: {}".format(err)) else: @@ -622,7 +623,7 @@ def _finish_init(self): ChipDeviceController.activeList.add(self) - def _enablePairingCompleteCallback(self, value: bool): + def _enablePairingCompleteCallback(self, value: bool) -> None: self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete(self.pairingDelegate, value) @property @@ -642,7 +643,7 @@ def name(self) -> str: return self._name @name.setter - def name(self, new_name: str): + def name(self, new_name: str) -> None: self._name = new_name @property @@ -665,7 +666,7 @@ def setCommissioningStageStartCallback( self.pairingDelegate, callback ) - def Shutdown(self): + def Shutdown(self) -> None: ''' Shuts down this controller and reclaims any used resources, including the bound C++ constructor instance in the SDK. @@ -690,7 +691,7 @@ def Shutdown(self): ChipDeviceController.activeList.remove(self) self._isActive = False - def ShutdownAll(self): + def ShutdownAll(self) -> None: ''' Shut down all active controllers and reclaim any used resources. ''' @@ -710,7 +711,7 @@ def ShutdownAll(self): ChipDeviceController.activeList.clear() - def CheckIsActive(self): + def CheckIsActive(self) -> None: ''' Checks if the device controller is active. @@ -724,7 +725,7 @@ def CheckIsActive(self): def __del__(self): self.Shutdown() - def IsConnected(self): + def IsConnected(self) -> bool: ''' Checks if the device controller is connected. @@ -756,14 +757,14 @@ async def ConnectBLE(self, discriminator: int, setupPinCode: int, nodeId: int, i ''' self.CheckIsActive() - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_ConnectBLE( self.devCtrl, discriminator, isShortDiscriminator, setupPinCode, nodeId) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) async def ConnectNFC(self, discriminator: int, setupPinCode: int, nodeId: int) -> int: ''' @@ -779,14 +780,14 @@ async def ConnectNFC(self, discriminator: int, setupPinCode: int, nodeId: int) - ''' self.CheckIsActive() - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_ConnectNFC( self.devCtrl, discriminator, setupPinCode, nodeId) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) async def ContinueCommissioningAfterConnectNetworkRequest(self, nodeId: int) -> int: ''' @@ -802,14 +803,14 @@ async def ContinueCommissioningAfterConnectNetworkRequest(self, nodeId: int) -> ''' self.CheckIsActive() - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(False) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_ContinueCommissioningAfterConnectNetworkRequest( self.devCtrl, nodeId) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) async def UnpairDevice(self, nodeId: int) -> None: ''' @@ -823,15 +824,15 @@ async def UnpairDevice(self, nodeId: int) -> None: ''' self.CheckIsActive() - async with self._unpair_device_context as ctx: + async with self._unpair_device_context as ctx_future: await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_UnpairDevice( self.devCtrl, nodeId, self.cbHandleDeviceUnpairCompleteFunct) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) - def CloseBLEConnection(self): + def CloseBLEConnection(self) -> None: ''' Closes the BLE connection for the device controller. @@ -845,7 +846,7 @@ def CloseBLEConnection(self): self.devCtrl) ).raise_on_error() - def ExpireSessions(self, nodeId: int): + def ExpireSessions(self, nodeId: int) -> None: ''' Close all sessions with `nodeId` (if any existed) so that sessions get re-established. @@ -864,7 +865,7 @@ def ExpireSessions(self, nodeId: int): self._ChipStack.Call(lambda: self._dmLib.pychip_ExpireSessions(self.devCtrl, nodeId)).raise_on_error() - def MarkSessionDefunct(self, nodeId: int): + def MarkSessionDefunct(self, nodeId: int) -> None: ''' Marks a previously active session with the specified node as defunct to temporarily prevent it from being used with new exchanges to send or receive messages. This should be called when there @@ -890,7 +891,7 @@ def MarkSessionDefunct(self, nodeId: int): self.devCtrl, nodeId) ).raise_on_error() - def MarkSessionForEviction(self, nodeId: int): + def MarkSessionForEviction(self, nodeId: int) -> None: ''' Marks the session with the specified node for eviction. It will first detach all SessionHolders attached to this session by calling 'OnSessionReleased' on each of them. This will force them @@ -913,7 +914,7 @@ def MarkSessionForEviction(self, nodeId: int): self.devCtrl, nodeId) ).raise_on_error() - def DeleteAllSessionResumptionStorage(self): + def DeleteAllSessionResumptionStorage(self) -> None: ''' Remove all session resumption information associated with the fabric index of the controller. @@ -927,7 +928,7 @@ def DeleteAllSessionResumptionStorage(self): lambda: self._dmLib.pychip_DeviceController_DeleteAllSessionResumption( self.devCtrl)).raise_on_error() - def SetLocalMRPConfig(self, idle_ms: int, active_ms: int, active_threshold_ms: int): + def SetLocalMRPConfig(self, idle_ms: int, active_ms: int, active_threshold_ms: int) -> None: ''' Set the local MRP config. This will be advertised to peers and used by them for message retransmissions. @@ -940,11 +941,14 @@ def SetLocalMRPConfig(self, idle_ms: int, active_ms: int, active_threshold_ms: i ChipStackError: On failure. ''' self.CheckIsActive() - self._ChipStack.Call( - lambda: self._dmLib.pychip_DeviceController_SetLocalMRPConfig(idle_ms, active_ms, active_threshold_ms) - ).raise_on_error() + try: + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_SetLocalMRPConfig(idle_ms, active_ms, active_threshold_ms) + ).raise_on_error() + except AttributeError: + raise NotImplementedError("Dynamic MRP config support is not available in this Matter SDK build") - def ResetLocalMRPConfig(self): + def ResetLocalMRPConfig(self) -> None: ''' Resets the local MRP config to the default values. @@ -952,17 +956,20 @@ def ResetLocalMRPConfig(self): ChipStackError: On failure. ''' self.CheckIsActive() - self._ChipStack.Call( - lambda: self._dmLib.pychip_DeviceController_ResetLocalMRPConfig() - ).raise_on_error() + try: + self._ChipStack.Call( + lambda: self._dmLib.pychip_DeviceController_ResetLocalMRPConfig() + ).raise_on_error() + except AttributeError: + raise NotImplementedError("Dynamic MRP config support is not available in this Matter SDK build") - async def _establishPASESession(self, callFunct): + async def _establishPASESession(self, callFunct: typing.Callable[[], None]) -> None: self.CheckIsActive() - async with self._pase_establishment_context as ctx: + async with self._pase_establishment_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync(callFunct) - await asyncio.futures.wrap_future(ctx.future) + await asyncio.futures.wrap_future(ctx_future) async def EstablishPASESessionBLE(self, setupPinCode: int, discriminator: int, nodeId: int) -> None: ''' @@ -1024,7 +1031,7 @@ async def EstablishPASESession(self, setUpCode: str, nodeId: int) -> None: self.devCtrl, setUpCode.encode("utf-8"), nodeId) ) - def GetTestCommissionerUsed(self): + def GetTestCommissionerUsed(self) -> bool: ''' Get the status of test commissioner in use. @@ -1035,10 +1042,10 @@ def GetTestCommissionerUsed(self): lambda: self._dmLib.pychip_TestCommissionerUsed() ) - def ResetTestCommissioner(self): + def ResetTestCommissioner(self) -> None: self._dmLib.pychip_ResetCommissioningTests() - def SetTestCommissionerSimulateFailureOnStage(self, stage: int): + def SetTestCommissionerSimulateFailureOnStage(self, stage: int) -> bool: ''' Simulates a failure on a specific stage of the test commissioner. @@ -1050,10 +1057,9 @@ def SetTestCommissionerSimulateFailureOnStage(self, stage: int): Returns: bool: True if the failure simulate success, False if not. ''' - return self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage( - stage) + return self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage(stage) - def SetTestCommissionerSimulateFailureOnReport(self, stage: int): + def SetTestCommissionerSimulateFailureOnReport(self, stage: int) -> bool: ''' Simulates a failure on report of the test commissioner. @@ -1065,10 +1071,9 @@ def SetTestCommissionerSimulateFailureOnReport(self, stage: int): Returns: bool: True if the failure simulate success, False if not. ''' - return self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport( - stage) + return self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport(stage) - def SetTestCommissionerPrematureCompleteAfter(self, stage: int): + def SetTestCommissionerPrematureCompleteAfter(self, stage: int) -> bool: ''' Premature complete of the test commissioner. @@ -1080,10 +1085,9 @@ def SetTestCommissionerPrematureCompleteAfter(self, stage: int): Returns: bool: True if the premature complete success, False if not. ''' - return self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter( - stage) + return self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter(stage) - def CheckTestCommissionerCallbacks(self): + def CheckTestCommissionerCallbacks(self) -> bool: ''' Check the test commissioner callbacks. @@ -1094,7 +1098,7 @@ def CheckTestCommissionerCallbacks(self): lambda: self._dmLib.pychip_TestCommissioningCallbacks() ) - def CheckStageSuccessful(self, stage: int): + def CheckStageSuccessful(self, stage: int) -> bool: ''' Check the test commissioner stage success. @@ -1108,7 +1112,7 @@ def CheckStageSuccessful(self, stage: int): lambda: self._dmLib.pychip_TestCommissioningStageSuccessful(stage) ) - def CheckTestCommissionerPaseConnection(self, nodeId: int): + def CheckTestCommissionerPaseConnection(self, nodeId: int) -> bool: ''' Check the test commissioner Pase connection success. @@ -1120,7 +1124,7 @@ def CheckTestCommissionerPaseConnection(self, nodeId: int): ''' return self._dmLib.pychip_TestPaseConnection(nodeId) - def ResolveNode(self, nodeId: int): + def ResolveNode(self, nodeId: int) -> None: ''' Resolve node ID. @@ -1131,7 +1135,7 @@ def ResolveNode(self, nodeId: int): self.GetConnectedDeviceSync(nodeId, allowPASE=False) - def GetAddressAndPort(self, nodeId: int): + def GetAddressAndPort(self, nodeId: int) -> tuple[str, int] | None: ''' Get the address and port. @@ -1154,8 +1158,9 @@ def GetAddressAndPort(self, nodeId: int): # Intentionally return None instead of raising exceptions on error return (address.value.decode(), port.value) if error == 0 else None - async def DiscoverCommissionableNodes(self, filterType: discovery.FilterType = discovery.FilterType.NONE, filter: typing.Any = None, - stopOnFirst: bool = False, timeoutSecond: int = 5) -> typing.Union[None, CommissionableNode, typing.List[CommissionableNode]]: + async def DiscoverCommissionableNodes(self, filterType: discovery.FilterType = discovery.FilterType.NONE, + filter: typing.Any = None, stopOnFirst: bool = False, + timeoutSecond: int = 5) -> CommissionableNode | list[CommissionableNode] | None: ''' Discover commissionable nodes via DNS-SD with specified filters. Supported filters are: @@ -1207,29 +1212,30 @@ async def _wait_discovery(): return await self.GetDiscoveredDevices() - async def GetDiscoveredDevices(self): + async def GetDiscoveredDevices(self) -> list[CommissionableNode]: ''' Get the discovered devices. Returns: list: A list of discovered devices. ''' - def GetDevices(devCtrl): + def GetDevices(devCtrl: ChipDeviceController) -> list[CommissionableNode]: devices = [] @_ChipDeviceController_IterateDiscoveredCommissionableNodesFunct def HandleDevice(deviceJson, deviceJsonLen): jsonStr = ctypes.string_at(deviceJson, deviceJsonLen).decode("utf-8") - device = dacite.from_dict(data_class=CommissionableNode, data=json.loads(jsonStr)) + device: CommissionableNode = dacite.from_dict(data_class=CommissionableNode, data=json.loads(jsonStr)) device.SetDeviceController(devCtrl) devices.append(device) self._dmLib.pychip_DeviceController_IterateDiscoveredCommissionableNodes(devCtrl.devCtrl, HandleDevice) return devices - return await self._ChipStack.CallAsyncWithResult(lambda: GetDevices(self)) + # TODO: Fix the dependency on devCtrl.CommissionOnNetwork in SetDeviceController + return await self._ChipStack.CallAsyncWithResult(lambda: GetDevices(self)) # type: ignore[arg-type] - def GetIPForDiscoveredDevice(self, idx, addrStr, length): + def GetIPForDiscoveredDevice(self, idx: int, addrStr: str, length: int) -> bool: ''' Get the IP address for a discovered device. @@ -1274,13 +1280,13 @@ async def OpenCommissioningWindow(self, nodeId: int, timeout: int, iteration: in ''' self.CheckIsActive() - async with self._open_window_context as ctx: + async with self._open_window_context as ctx_future: await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_OpenCommissioningWindow( self.devCtrl, self.pairingDelegate, nodeId, timeout, iteration, discriminator, option) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) async def OpenJointCommissioningWindow(self, nodeId: int, endpointId: int, timeout: int, iteration: int, discriminator: int) -> CommissioningParameters: @@ -1299,15 +1305,18 @@ async def OpenJointCommissioningWindow(self, nodeId: int, endpointId: int, timeo ''' self.CheckIsActive() - async with self._open_window_context as ctx: - await self._ChipStack.CallAsync( - lambda: self._dmLib.pychip_DeviceController_OpenJointCommissioningWindow( - self.devCtrl, self.pairingDelegate, nodeId, endpointId, timeout, iteration, discriminator) - ) + async with self._open_window_context as ctx_future: + try: + await self._ChipStack.CallAsync( + lambda: self._dmLib.pychip_DeviceController_OpenJointCommissioningWindow( + self.devCtrl, self.pairingDelegate, nodeId, endpointId, timeout, iteration, discriminator) + ) + except AttributeError: + raise NotImplementedError("Joint Fabric support is not available in this Matter SDK build.") - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) - def GetCompressedFabricId(self): + def GetCompressedFabricId(self) -> int: ''' Get compressed fabric Id. @@ -1412,7 +1421,7 @@ def GetRootPublicKeyBytesInternal(self) -> bytes: return bytes(buf[:csize.value]) - def GetClusterHandler(self): + def GetClusterHandler(self) -> ChipClusters: ''' Get cluster handler @@ -1423,7 +1432,8 @@ def GetClusterHandler(self): return self._Cluster - async def FindOrEstablishPASESession(self, setupCode: str, nodeId: int, timeoutMs: typing.Optional[int] = None) -> typing.Optional[DeviceProxyWrapper]: + async def FindOrEstablishPASESession(self, setupCode: str, nodeId: int, + timeoutMs: int | None = None) -> DeviceProxyWrapper | None: ''' Find or establish a PASE session. @@ -1451,7 +1461,8 @@ async def FindOrEstablishPASESession(self, setupCode: str, nodeId: int, timeoutM return None - def GetConnectedDeviceSync(self, nodeId: int, allowPASE=True, timeoutMs: typing.Optional[int] = None, payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + def GetConnectedDeviceSync(self, nodeId: int, allowPASE: bool = True, timeoutMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> DeviceProxyWrapper: ''' Gets an OperationalDeviceProxy or CommissioneeDeviceProxy for the specified Node. @@ -1466,7 +1477,7 @@ def GetConnectedDeviceSync(self, nodeId: int, allowPASE=True, timeoutMs: typing. self.CheckIsActive() returnDevice = c_void_p(None) - returnErr: typing.Any = None + returnErr: PyChipError | None = None deviceAvailableCV = threading.Condition() if allowPASE: @@ -1477,7 +1488,7 @@ def GetConnectedDeviceSync(self, nodeId: int, allowPASE=True, timeoutMs: typing. return DeviceProxyWrapper(returnDevice, DeviceProxyWrapper.DeviceProxyType.COMMISSIONEE, self._dmLib) class DeviceAvailableClosure(): - def deviceAvailable(self, device, err): + def deviceAvailable(self, device: int, err: PyChipError) -> None: nonlocal returnDevice nonlocal returnErr nonlocal deviceAvailableCV @@ -1505,12 +1516,13 @@ def deviceAvailable(self, device, err): if ret is False: raise TimeoutError("Timed out waiting for DNS-SD resolution") - if returnDevice.value is None: + if returnDevice.value is None and returnErr is not None: returnErr.raise_on_error() return DeviceProxyWrapper(returnDevice, DeviceProxyWrapper.DeviceProxyType.OPERATIONAL, self._dmLib) - async def WaitForActive(self, nodeId: int, *, timeoutSeconds=30.0, stayActiveDurationMs=30000): + async def WaitForActive(self, nodeId: int, *, timeoutSeconds: float = 30.0, + stayActiveDurationMs: int = 30000) -> Clusters.IcdManagement.Commands.StayActiveResponse | None: ''' Waits a LIT ICD device to become active. Will send a StayActive command to the device on active to allow human operations. @@ -1522,10 +1534,12 @@ async def WaitForActive(self, nodeId: int, *, timeoutSeconds=30.0, stayActiveDur StayActiveResponse on success ''' await WaitForCheckIn(ScopedNodeId(nodeId, self._fabricIndex), timeoutSeconds=timeoutSeconds) - return await self.SendCommand(nodeId, 0, Clusters.IcdManagement.Commands.StayActiveRequest(stayActiveDuration=stayActiveDurationMs)) + return await self.SendCommand( + nodeId, 0, Clusters.IcdManagement.Commands.StayActiveRequest(stayActiveDuration=uint(stayActiveDurationMs)), + Clusters.IcdManagement.Commands.StayActiveResponse) - async def GetConnectedDevice(self, nodeId: int, allowPASE: bool = True, timeoutMs: typing.Optional[int] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def GetConnectedDevice(self, nodeId: int, allowPASE: bool = True, timeoutMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> DeviceProxyWrapper: ''' Gets an OperationalDeviceProxy or CommissioneeDeviceProxy for the specified Node. @@ -1551,21 +1565,21 @@ async def GetConnectedDevice(self, nodeId: int, allowPASE: bool = True, timeoutM future = eventLoop.create_future() class DeviceAvailableClosure(): - def __init__(self, loop, future: asyncio.Future): + def __init__(self, loop: asyncio.AbstractEventLoop, future: asyncio.Future): self._returnDevice = c_void_p(None) - self._returnErr = None + self._returnErr: PyChipError | None = None self._event_loop = loop self._future = future - def _deviceAvailable(self): + def _deviceAvailable(self) -> None: if self._future.cancelled(): return if self._returnDevice.value is not None: self._future.set_result(self._returnDevice) - else: - self._future.set_exception(self._returnErr.to_exception()) + elif self._returnErr is not None and (exc := self._returnErr.to_exception()) is not None: + self._future.set_exception(exc) - def deviceAvailable(self, device, err): + def deviceAvailable(self, device: int, err: PyChipError) -> None: self._returnDevice = c_void_p(device) self._returnErr = err self._event_loop.call_soon_threadsafe(self._deviceAvailable) @@ -1593,7 +1607,7 @@ def deviceAvailable(self, device, err): return DeviceProxyWrapper(future.result(), DeviceProxyWrapper.DeviceProxyType.OPERATIONAL, self._dmLib) - def ComputeRoundTripTimeout(self, nodeId: int, upperLayerProcessingTimeoutMs: int = 0): + def ComputeRoundTripTimeout(self, nodeId: int, upperLayerProcessingTimeoutMs: int = 0) -> int: ''' Returns a computed timeout value based on the round-trip time it takes for the peer at the other end of the session to receive a message, process it and send it back. This is computed based on the session type, the type of transport, @@ -1609,7 +1623,7 @@ def ComputeRoundTripTimeout(self, nodeId: int, upperLayerProcessingTimeoutMs: in return self._ChipStack.Call(lambda: self._dmLib.pychip_DeviceProxy_ComputeRoundTripTimeout( device.deviceProxy, upperLayerProcessingTimeoutMs)) - def GetRemoteSessionParameters(self, nodeId: int) -> typing.Optional[SessionParameters]: + def GetRemoteSessionParameters(self, nodeId: int) -> SessionParameters | None: ''' Returns the SessionParameters of reported by the remote node associated with `nodeId`. If there is some error in getting SessionParameters None is returned. @@ -1637,11 +1651,11 @@ def GetRemoteSessionParameters(self, nodeId: int) -> typing.Optional[SessionPara specificationVersion=sessionParametersStruct.SpecificationVersion if sessionParametersStruct.SpecificationVersion != 0 else None, maxPathsPerInvoke=sessionParametersStruct.MaxPathsPerInvoke) - async def TestOnlySendBatchCommands(self, nodeId: int, commands: typing.List[ClusterCommand.InvokeRequestInfo], - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - suppressResponse: typing.Optional[bool] = None, remoteMaxPathsPerInvoke: typing.Optional[int] = None, - suppressTimedRequestMessage: bool = False, commandRefsOverride: typing.Optional[typing.List[int]] = None): + async def TestOnlySendBatchCommands( + self, nodeId: int, commands: list[ClusterCommand.InvokeRequestInfo], timedRequestTimeoutMs: int | None = None, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, suppressResponse: bool | None = None, + remoteMaxPathsPerInvoke: int | None = None, suppressTimedRequestMessage: bool = False, + commandRefsOverride: list[int] | None = None) -> ClusterCommand.TestOnlyBatchCommandResponse: ''' Please see SendBatchCommands for description. TestOnly overridable arguments: @@ -1668,7 +1682,8 @@ async def TestOnlySendBatchCommands(self, nodeId: int, commands: typing.List[Clu return await future async def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(self, nodeId: int, endpoint: int, - payload: ClusterObjects.ClusterCommand, responseType=None): + payload: ClusterObjects.ClusterCommand, + responseType: type[ResponseT] | None = None) -> ResponseT | None: ''' Please see SendCommand for description. @@ -1693,12 +1708,7 @@ async def TestOnlySendCommandTimedRequestFlagWithNoTimedInvoke(self, nodeId: int return await future def _prepare_write_attribute_requests( - self, - attributes: typing.List[typing.Union[ - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor], - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int] - ]] - ) -> typing.List[ClusterAttribute.AttributeWriteRequest]: + self, attributes: AttributeWriteRequestList) -> list[ClusterAttribute.AttributeWriteRequest]: """Helper method to prepare attribute write requests.""" attrs = [] for v in attributes: @@ -1715,20 +1725,13 @@ def _prepare_write_attribute_requests( Attribute=v[1], DataVersion=v[2], HasDataVersion=1, - Data=v[1].value)) + Data=v[1].value)) # type: ignore[attr-defined] return attrs - async def TestOnlyWriteAttributeWithMismatchedTimedRequestField(self, nodeid: int, - attributes: typing.List[typing.Union[ - typing.Tuple[int, - ClusterObjects.ClusterAttributeDescriptor], - typing.Tuple[int, - ClusterObjects.ClusterAttributeDescriptor, int] - ]], - timedRequestTimeoutMs: int, - timedRequestFieldValue: bool, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def TestOnlyWriteAttributeWithMismatchedTimedRequestField( + self, nodeid: int, attributes: AttributeWriteRequestList, timedRequestTimeoutMs: int, timedRequestFieldValue: bool, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> list[ClusterAttribute.AttributeStatus]: ''' ONLY TO BE USED FOR TEST: Write attributes with decoupled Timed Request action and TimedRequest field. This allows testing TIMED_REQUEST_MISMATCH scenarios: @@ -1763,11 +1766,11 @@ async def TestOnlyWriteAttributeWithMismatchedTimedRequestField(self, nodeid: in interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs).raise_on_error() return await future - async def SendCommand(self, nodeId: int, endpoint: int, payload: ClusterObjects.ClusterCommand, responseType=None, - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - suppressResponse: typing.Optional[bool] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def SendCommand(self, nodeId: int, endpoint: int, payload: ClusterObjects.ClusterCommand, + responseType: type[ResponseT] | None = None, timedRequestTimeoutMs: int | None = None, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, + suppressResponse: bool | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> ResponseT | None: ''' Send a cluster-object encapsulated command to a node and get returned a future that can be awaited upon to receive the response. If a valid responseType is passed in, that will be used to de-serialize the object. If not, @@ -1801,11 +1804,10 @@ async def SendCommand(self, nodeId: int, endpoint: int, payload: ClusterObjects. res.raise_on_error() return await future - async def SendBatchCommands(self, nodeId: int, commands: typing.List[ClusterCommand.InvokeRequestInfo], - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - suppressResponse: typing.Optional[bool] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def SendBatchCommands(self, nodeId: int, commands: list[ClusterCommand.InvokeRequestInfo], + timedRequestTimeoutMs: int | None = None, interactionTimeoutMs: int | None = None, + busyWaitMs: int | None = None, suppressResponse: bool | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> list: ''' Send a batch of cluster-object encapsulated commands to a node and get returned a future that can be awaited upon to receive the responses. If a valid responseType is passed in, that will be used to de-serialize the object. If not, @@ -1842,7 +1844,7 @@ async def SendBatchCommands(self, nodeId: int, commands: typing.List[ClusterComm res.raise_on_error() return await future - def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: typing.Optional[int] = None): + def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, busyWaitMs: int | None = None) -> None: ''' Send a group cluster-object encapsulated command to a group_id and get returned a future that can be awaited upon to get confirmation command was sent. @@ -1862,14 +1864,10 @@ def SendGroupCommand(self, groupid: int, payload: ClusterObjects.ClusterCommand, # None is the expected return for sending group commands. return - async def WriteAttribute(self, nodeId: int, - attributes: typing.List[typing.Union[ - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor], - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int] - ]], - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def WriteAttribute( + self, nodeId: int, attributes: AttributeWriteRequestList, timedRequestTimeoutMs: int | None = None, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> list[ClusterAttribute.AttributeStatus]: ''' Write a list of attributes on a target node. @@ -1897,14 +1895,10 @@ async def WriteAttribute(self, nodeId: int, payloadCapability=payloadCapability, forceLegacyListEncoding=False) - async def _WriteAttribute(self, nodeId: int, - attributes: typing.List[typing.Union[ - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor], - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int] - ]], - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD, forceLegacyListEncoding: bool = False): + async def _WriteAttribute(self, nodeId: int, attributes: AttributeWriteRequestList, timedRequestTimeoutMs: int | None = None, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD, + forceLegacyListEncoding: bool = False) -> list[ClusterAttribute.AttributeStatus]: self.CheckIsActive() @@ -1920,14 +1914,10 @@ async def _WriteAttribute(self, nodeId: int, interactionTimeoutMs=interactionTimeoutMs, busyWaitMs=busyWaitMs, forceLegacyListEncoding=forceLegacyListEncoding).raise_on_error() return await future - async def TestOnlyWriteAttributeWithLegacyList(self, nodeId: int, - attributes: typing.List[typing.Union[ - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor], - typing.Tuple[int, ClusterObjects.ClusterAttributeDescriptor, int] - ]], - timedRequestTimeoutMs: typing.Optional[int] = None, - interactionTimeoutMs: typing.Optional[int] = None, busyWaitMs: typing.Optional[int] = None, - payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD): + async def TestOnlyWriteAttributeWithLegacyList( + self, nodeId: int, attributes: AttributeWriteRequestList, timedRequestTimeoutMs: int | None = None, + interactionTimeoutMs: int | None = None, busyWaitMs: int | None = None, + payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD) -> list[ClusterAttribute.AttributeStatus]: ''' Please see WriteAttribute for description. This is a test-only wrapper for _WriteAttribute that sets forceLegacyListEncoding to True. @@ -1950,8 +1940,8 @@ async def TestOnlyWriteAttributeWithLegacyList(self, nodeId: int, payloadCapability=payloadCapability, forceLegacyListEncoding=True) - def WriteGroupAttribute( - self, groupid: int, attributes: typing.List[typing.Tuple[ClusterObjects.ClusterAttributeDescriptor, int]], busyWaitMs: typing.Optional[int] = None): + def WriteGroupAttribute(self, groupid: int, attributes: list[tuple[ClusterObjects.ClusterAttributeDescriptor, int]], + busyWaitMs: int | None = None) -> list: ''' Write a list of attributes on a target group. @@ -2037,18 +2027,13 @@ def TestOnlyPrepareToSendBdxData(self, data: bytes) -> asyncio.Future: # TODO: Explore proper typing for dynamic attributes in ChipDeviceCtrl.py #618 def _parseAttributePathTuple(self, pathTuple: typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[int], # Endpoint - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster]], - # Wildcard endpoint, Cluster + Attribute present - typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Wildcard attribute id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster]], - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Directly specified attribute path - ClusterAttribute.AttributePath - ]): + tuple[int], # Endpoint + tuple[type[ClusterObjects.Cluster]], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterAttributeDescriptor]], # Wildcard endpoint, Cluster + Attribute present + tuple[int, type[ClusterObjects.Cluster]], # Wildcard attribute id + tuple[int, type[ClusterObjects.ClusterAttributeDescriptor]], # Concrete path + ClusterAttribute.AttributePath # Directly specified attribute path + ]) -> ClusterAttribute.AttributePath: if isinstance(pathTuple, ClusterAttribute.AttributePath): return pathTuple if pathTuple == ('*') or pathTuple == (): @@ -2075,38 +2060,30 @@ def _parseAttributePathTuple(self, pathTuple: typing.Union[ ) raise ValueError("Unsupported Attribute Path") - def _parseDataVersionFilterTuple(self, pathTuple: typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]]): - endpoint = None - cluster = None - + def _parseDataVersionFilterTuple( + self, pathTuple: tuple[int, type[ClusterObjects.Cluster], int]) -> ClusterAttribute.DataVersionFilter: # endpoint + (cluster) attribute / endpoint + cluster endpoint = pathTuple[0] # mypy errors ignored due to valid use of dynamic types (e.g., int, str, or class types). # Fixing these typing errors is a high risk to affect existing functionality. # These mismatches are intentional and safe within the current logic. # TODO: Explore proper typing for dynamic attributes in ChipDeviceCtrl.py #618 - if issubclass(pathTuple[1], ClusterObjects.Cluster): # type: ignore[arg-type] + if issubclass(pathTuple[1], ClusterObjects.Cluster): cluster = pathTuple[1] else: raise ValueError("Unsupported Cluster Path") dataVersion = pathTuple[2] - return ClusterAttribute.DataVersionFilter.from_cluster( - EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion) # type: ignore[arg-type] + return ClusterAttribute.DataVersionFilter.from_cluster(EndpointId=endpoint, Cluster=cluster, DataVersion=dataVersion) def _parseEventPathTuple(self, pathTuple: typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[str, int], # all wildcard with urgency set - typing.Tuple[int, int], # Endpoint, - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster], int], - # Wildcard endpoint, Cluster + Event present - typing.Tuple[typing.Type[ClusterObjects.ClusterEvent], int], - # Wildcard event id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int], - # Concrete path - typing.Tuple[int, - typing.Type[ClusterObjects.ClusterEvent], int] - ]): + tuple[str, int], # all wildcard with urgency set + tuple[int, int], # Endpoint, + tuple[type[ClusterObjects.Cluster], int], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterEvent], int], # Wildcard endpoint, Cluster + Event present + tuple[int, type[ClusterObjects.Cluster], int], # Wildcard event id + tuple[int, type[ClusterObjects.ClusterEvent], int] # Concrete path + ]) -> ClusterAttribute.EventPath: if pathTuple in [('*'), ()]: # Wildcard return ClusterAttribute.EventPath() @@ -2144,39 +2121,31 @@ def _parseEventPathTuple(self, pathTuple: typing.Union[ async def Read( self, nodeId: int, - attributes: typing.Optional[typing.List[typing.Union[ + attributes: list[typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[int], # Endpoint - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster]], - # Wildcard endpoint, Cluster + Attribute present - typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Wildcard attribute id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster]], - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Directly specified attribute path - ClusterAttribute.AttributePath - ]]] = None, - dataVersionFilters: typing.Optional[typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]]] = None, events: typing.Optional[typing.List[ + tuple[int], # Endpoint + tuple[type[ClusterObjects.Cluster]], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterAttributeDescriptor]], # Wildcard endpoint, Cluster + Attribute present + tuple[int, type[ClusterObjects.Cluster]], # Wildcard attribute id + tuple[int, type[ClusterObjects.ClusterAttributeDescriptor]], # Concrete path + ClusterAttribute.AttributePath # Directly specified attribute path + ]] | None = None, + dataVersionFilters: list[tuple[int, type[ClusterObjects.Cluster], int]] | None = None, + events: list[ typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[str, int], # all wildcard with urgency set - typing.Tuple[int, int], # Endpoint, - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster], int], - # Wildcard endpoint, Cluster + Event present - typing.Tuple[typing.Type[ClusterObjects.ClusterEvent], int], - # Wildcard event id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int], - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterEvent], int] - ]]] = None, - eventNumberFilter: typing.Optional[int] = None, - returnClusterObject: bool = False, reportInterval: typing.Optional[typing.Tuple[int, int]] = None, + tuple[str, int], # all wildcard with urgency set + tuple[int, int], # Endpoint with urgency set + tuple[type[ClusterObjects.Cluster], int], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterEvent], int], # Wildcard endpoint, Cluster + Event present + tuple[int, type[ClusterObjects.Cluster], int], # Wildcard event id + tuple[int, type[ClusterObjects.ClusterEvent], int] # Concrete path + ]] | None = None, + eventNumberFilter: int | None = None, + returnClusterObject: bool = False, reportInterval: tuple[int, int] | None = None, fabricFiltered: bool = True, keepSubscriptions: bool = False, autoResubscribe: bool = True, payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD - ): + ) -> ClusterAttribute.AsyncReadTransaction.ReadResponse | ClusterAttribute.SubscriptionTransaction: ''' Read a list of attributes and/or events from a target node @@ -2246,8 +2215,8 @@ async def Read( device = await self.GetConnectedDevice(nodeId, payloadCapability=payloadCapability) attributePaths = [self._parseAttributePathTuple( v) for v in attributes] if attributes else None - clusterDataVersionFilters = [self._parseDataVersionFilterTuple( - v) for v in dataVersionFilters] if dataVersionFilters else None # type: ignore[arg-type] + clusterDataVersionFilters = [self._parseDataVersionFilterTuple(v) + for v in dataVersionFilters] if dataVersionFilters else None eventPaths = [self._parseEventPathTuple( v) for v in events] if events else None @@ -2270,25 +2239,21 @@ async def Read( async def ReadAttribute( self, nodeId: int, - attributes: typing.Optional[typing.List[typing.Union[ + attributes: list[typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[int], # Endpoint - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster]], - # Wildcard endpoint, Cluster + Attribute present - typing.Tuple[typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Wildcard attribute id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster]], - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterAttributeDescriptor]], - # Directly specified attribute path - ClusterAttribute.AttributePath - ]]], dataVersionFilters: typing.Optional[typing.List[typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int]]] = None, + tuple[int], # Endpoint + tuple[type[ClusterObjects.Cluster]], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterAttributeDescriptor]], # Wildcard endpoint, Cluster + Attribute present + tuple[int, type[ClusterObjects.Cluster]], # Wildcard attribute id + tuple[int, type[ClusterObjects.ClusterAttributeDescriptor]], # Concrete path + ClusterAttribute.AttributePath # Directly specified attribute path + ]] | None, + dataVersionFilters: list[tuple[int, type[ClusterObjects.Cluster], int]] | None = None, returnClusterObject: bool = False, - reportInterval: typing.Optional[typing.Tuple[int, int]] = None, + reportInterval: tuple[int, int] | None = None, fabricFiltered: bool = True, keepSubscriptions: bool = False, autoResubscribe: bool = True, payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD - ): + ) -> ClusterAttribute.SubscriptionTransaction | dict[int, list[ClusterObjects.Cluster]]: ''' Read a list of attributes from a target node, this is a wrapper of DeviceController.Read() @@ -2359,25 +2324,22 @@ async def ReadAttribute( async def ReadEvent( self, nodeId: int, - events: typing.List[typing.Union[ + events: list[typing.Union[ None, # Empty tuple, all wildcard - typing.Tuple[str, int], # all wildcard with urgency set - typing.Tuple[int, int], # Endpoint, - # Wildcard endpoint, Cluster id present - typing.Tuple[typing.Type[ClusterObjects.Cluster], int], - # Wildcard endpoint, Cluster + Event present - typing.Tuple[typing.Type[ClusterObjects.ClusterEvent], int], - # Wildcard event id - typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int], - # Concrete path - typing.Tuple[int, typing.Type[ClusterObjects.ClusterEvent], int] - ]], eventNumberFilter: typing.Optional[int] = None, + tuple[str, int], # all wildcard with urgency set + tuple[int, int], # Endpoint with urgency set + tuple[type[ClusterObjects.Cluster], int], # Wildcard endpoint, Cluster id present + tuple[type[ClusterObjects.ClusterEvent], int], # Wildcard endpoint, Cluster + Event present + tuple[int, type[ClusterObjects.Cluster], int], # Wildcard event id + tuple[int, type[ClusterObjects.ClusterEvent], int] # Concrete path + ]], + eventNumberFilter: int | None = None, fabricFiltered: bool = True, - reportInterval: typing.Optional[typing.Tuple[int, int]] = None, + reportInterval: tuple[int, int] | None = None, keepSubscriptions: bool = False, autoResubscribe: bool = True, payloadCapability: int = TransportPayloadCapability.MRP_PAYLOAD - ): + ) -> ClusterAttribute.SubscriptionTransaction | list[ClusterObjects.ClusterEvent]: ''' Read a list of events from a target node, this is a wrapper of DeviceController.Read() @@ -2429,7 +2391,7 @@ async def ReadEvent( return res return res.events - def SetIpk(self, ipk: bytes): + def SetIpk(self, ipk: bytes) -> None: ''' Sets the Identity Protection Key (IPK) for the device controller. @@ -2440,7 +2402,7 @@ def SetIpk(self, ipk: bytes): lambda: self._dmLib.pychip_DeviceController_SetIpk(self.devCtrl, ipk, len(ipk)) ).raise_on_error() - def InitGroupTestingData(self): + def InitGroupTestingData(self) -> None: ''' Populates the Device Controller's GroupDataProvider with known test group info and keys. @@ -2456,8 +2418,8 @@ def InitGroupTestingData(self): def SetGroupKeySet(self, keyset_id: int, policy: int, num_keys: int, epoch_key0: bytes, epoch_start_time0: int, - epoch_key1: typing.Optional[bytes] = None, epoch_start_time1: int = 0, - epoch_key2: typing.Optional[bytes] = None, epoch_start_time2: int = 0): + epoch_key1: bytes | None = None, epoch_start_time1: int = 0, + epoch_key2: bytes | None = None, epoch_start_time2: int = 0) -> None: ''' Writes a GroupKeySet into the controller's GroupDataProvider for this fabric. @@ -2485,7 +2447,7 @@ def SetGroupKeySet(self, keyset_id: int, policy: int, num_keys: int, epoch_key2, epoch_start_time2) ).raise_on_error() - def SetGroupInfo(self, group_id: int, group_name: str, flags: int = 0x02): + def SetGroupInfo(self, group_id: int, group_name: str, flags: int = 0x02) -> None: ''' Adds or updates a group entry in the controller's GroupDataProvider for this fabric. @@ -2505,7 +2467,7 @@ def SetGroupInfo(self, group_id: int, group_name: str, flags: int = 0x02): self.devCtrl, group_id, group_name.encode('utf-8'), flags) ).raise_on_error() - def AddGroupEndpoint(self, group_id: int, endpoint_id: int): + def AddGroupEndpoint(self, group_id: int, endpoint_id: int) -> None: ''' Associates an endpoint with a group in the controller's GroupDataProvider for this fabric. @@ -2523,7 +2485,7 @@ def AddGroupEndpoint(self, group_id: int, endpoint_id: int): self.devCtrl, group_id, endpoint_id) ).raise_on_error() - def SetGroupKey(self, group_id: int, keyset_id: int): + def SetGroupKey(self, group_id: int, keyset_id: int) -> None: ''' Maps a group to a key set in the controller's GroupDataProvider for this fabric. @@ -2567,319 +2529,276 @@ def CreateManualCode(self, discriminator: int, passcode: int) -> str: return buf.value.decode() # ----- Private Members ----- - def _InitLib(self): - if self._dmLib is None: - self._dmLib = CDLL(self._ChipStack.LocateChipDLL()) + def _InitLib(self) -> CDLL: + dm_lib = CDLL(self._ChipStack.LocateChipDLL()) - self._dmLib.pychip_DeviceController_DeleteDeviceController.argtypes = [ - c_void_p, c_void_p] - self._dmLib.pychip_DeviceController_DeleteDeviceController.restype = PyChipError + dm_lib.pychip_DeviceController_DeleteDeviceController.argtypes = [c_void_p, c_void_p] + dm_lib.pychip_DeviceController_DeleteDeviceController.restype = PyChipError - self._dmLib.pychip_DeviceController_ConnectBLE.argtypes = [ - c_void_p, c_uint16, c_bool, c_uint32, c_uint64] - self._dmLib.pychip_DeviceController_ConnectBLE.restype = PyChipError + dm_lib.pychip_DeviceController_ConnectBLE.argtypes = [c_void_p, c_uint16, c_bool, c_uint32, c_uint64] + dm_lib.pychip_DeviceController_ConnectBLE.restype = PyChipError - self._dmLib.pychip_DeviceController_ConnectNFC.argtypes = [ - c_void_p, c_uint16, c_uint32, c_uint64] - self._dmLib.pychip_DeviceController_ConnectNFC.restype = PyChipError + dm_lib.pychip_DeviceController_ConnectNFC.argtypes = [c_void_p, c_uint16, c_uint32, c_uint64] + dm_lib.pychip_DeviceController_ConnectNFC.restype = PyChipError - self._dmLib.pychip_DeviceController_ContinueCommissioningAfterConnectNetworkRequest.argtypes = [ - c_void_p, c_uint64] - self._dmLib.pychip_DeviceController_ContinueCommissioningAfterConnectNetworkRequest.restype = PyChipError + dm_lib.pychip_DeviceController_ContinueCommissioningAfterConnectNetworkRequest.argtypes = [c_void_p, c_uint64] + dm_lib.pychip_DeviceController_ContinueCommissioningAfterConnectNetworkRequest.restype = PyChipError - self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.argtypes = [ - c_char_p, c_uint32] - self._dmLib.pychip_DeviceController_SetThreadOperationalDataset.restype = PyChipError + dm_lib.pychip_DeviceController_SetThreadOperationalDataset.argtypes = [c_char_p, c_uint32] + dm_lib.pychip_DeviceController_SetThreadOperationalDataset.restype = PyChipError - self._dmLib.pychip_DeviceController_SetWiFiCredentials.argtypes = [ - c_char_p, c_char_p] - self._dmLib.pychip_DeviceController_SetWiFiCredentials.restype = PyChipError + dm_lib.pychip_DeviceController_SetWiFiCredentials.argtypes = [c_char_p, c_char_p] + dm_lib.pychip_DeviceController_SetWiFiCredentials.restype = PyChipError - # Currently only supports 1 list item - self._dmLib.pychip_DeviceController_SetTimeZone.restype = PyChipError - self._dmLib.pychip_DeviceController_SetTimeZone.argtypes = [ - c_int32, c_uint64, c_char_p] + # Currently only supports 1 list item + dm_lib.pychip_DeviceController_SetTimeZone.restype = PyChipError + dm_lib.pychip_DeviceController_SetTimeZone.argtypes = [c_int32, c_uint64, c_char_p] - # Currently only supports 1 list item - self._dmLib.pychip_DeviceController_SetDSTOffset.restype = PyChipError - self._dmLib.pychip_DeviceController_SetDSTOffset.argtypes = [ - c_int32, c_uint64, c_uint64] + # Currently only supports 1 list item + dm_lib.pychip_DeviceController_SetDSTOffset.restype = PyChipError + dm_lib.pychip_DeviceController_SetDSTOffset.argtypes = [c_int32, c_uint64, c_uint64] - self._dmLib.pychip_DeviceController_SetDefaultNtp.restype = PyChipError - self._dmLib.pychip_DeviceController_SetDefaultNtp.argtypes = [c_char_p] + dm_lib.pychip_DeviceController_SetDefaultNtp.restype = PyChipError + dm_lib.pychip_DeviceController_SetDefaultNtp.argtypes = [c_char_p] - self._dmLib.pychip_DeviceController_SetTrustedTimeSource.restype = PyChipError - self._dmLib.pychip_DeviceController_SetTrustedTimeSource.argtypes = [c_uint64, c_uint16] + dm_lib.pychip_DeviceController_SetTrustedTimeSource.restype = PyChipError + dm_lib.pychip_DeviceController_SetTrustedTimeSource.argtypes = [c_uint64, c_uint16] - self._dmLib.pychip_DeviceController_SetCheckMatchingFabric.restype = PyChipError - self._dmLib.pychip_DeviceController_SetCheckMatchingFabric.argtypes = [c_bool] + dm_lib.pychip_DeviceController_SetCheckMatchingFabric.restype = PyChipError + dm_lib.pychip_DeviceController_SetCheckMatchingFabric.argtypes = [c_bool] - self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters.restype = PyChipError - self._dmLib.pychip_DeviceController_SetIcdRegistrationParameters.argtypes = [ - c_bool, c_void_p - ] + dm_lib.pychip_DeviceController_SetIcdRegistrationParameters.restype = PyChipError + dm_lib.pychip_DeviceController_SetIcdRegistrationParameters.argtypes = [c_bool, c_void_p] - self._dmLib.pychip_DeviceController_ResetCommissioningParameters.restype = PyChipError - self._dmLib.pychip_DeviceController_ResetCommissioningParameters.argtypes = [] + dm_lib.pychip_DeviceController_ResetCommissioningParameters.restype = PyChipError + dm_lib.pychip_DeviceController_ResetCommissioningParameters.argtypes = [] - self._dmLib.pychip_DeviceController_Commission.argtypes = [ - c_void_p, c_uint64] - self._dmLib.pychip_DeviceController_Commission.restype = PyChipError + dm_lib.pychip_DeviceController_Commission.argtypes = [c_void_p, c_uint64] + dm_lib.pychip_DeviceController_Commission.restype = PyChipError - self._dmLib.pychip_DeviceController_OnNetworkCommission.argtypes = [ - c_void_p, c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, c_uint32] - self._dmLib.pychip_DeviceController_OnNetworkCommission.restype = PyChipError + dm_lib.pychip_DeviceController_OnNetworkCommission.argtypes = [c_void_p, c_void_p, c_uint64, c_uint32, c_uint8, c_char_p, + c_uint32] + dm_lib.pychip_DeviceController_OnNetworkCommission.restype = PyChipError - if hasattr(self._dmLib, "pychip_DeviceController_ThreadMeshcopCommission"): - self._dmLib.pychip_DeviceController_ThreadMeshcopCommission.argtypes = [ - c_void_p, c_void_p, c_uint64, c_uint32, c_uint16, c_char_p, c_uint16] - self._dmLib.pychip_DeviceController_ThreadMeshcopCommission.restype = PyChipError - else: - logging.getLogger(__name__).warning( - "pychip_DeviceController_ThreadMeshcopCommission is not available in the loaded CHIP library; " - "Thread Meshcop commissioning is disabled.") + if hasattr(dm_lib, "pychip_DeviceController_ThreadMeshcopCommission"): + dm_lib.pychip_DeviceController_ThreadMeshcopCommission.argtypes = [ + c_void_p, c_void_p, c_uint64, c_uint32, c_uint16, c_char_p, c_uint16] + dm_lib.pychip_DeviceController_ThreadMeshcopCommission.restype = PyChipError + else: + logging.getLogger(__name__).warning( + "pychip_DeviceController_ThreadMeshcopCommission is not available in the loaded CHIP library; " + "Thread Meshcop commissioning is disabled.") - self._dmLib.pychip_DeviceController_DiscoverCommissionableNodes.argtypes = [ - c_void_p, c_uint8, c_char_p] - self._dmLib.pychip_DeviceController_DiscoverCommissionableNodes.restype = PyChipError + dm_lib.pychip_DeviceController_DiscoverCommissionableNodes.argtypes = [c_void_p, c_uint8, c_char_p] + dm_lib.pychip_DeviceController_DiscoverCommissionableNodes.restype = PyChipError - self._dmLib.pychip_DeviceController_StopCommissionableDiscovery.argtypes = [ - c_void_p] - self._dmLib.pychip_DeviceController_StopCommissionableDiscovery.restype = PyChipError + dm_lib.pychip_DeviceController_StopCommissionableDiscovery.argtypes = [c_void_p] + dm_lib.pychip_DeviceController_StopCommissionableDiscovery.restype = PyChipError - self._dmLib.pychip_DeviceController_EstablishPASESessionIP.argtypes = [ - c_void_p, c_char_p, c_uint32, c_uint64, c_uint16] - self._dmLib.pychip_DeviceController_EstablishPASESessionIP.restype = PyChipError + dm_lib.pychip_DeviceController_EstablishPASESessionIP.argtypes = [c_void_p, c_char_p, c_uint32, c_uint64, c_uint16] + dm_lib.pychip_DeviceController_EstablishPASESessionIP.restype = PyChipError - self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [ - c_void_p, c_uint32, c_uint16, c_uint64] - self._dmLib.pychip_DeviceController_EstablishPASESessionBLE.restype = PyChipError + dm_lib.pychip_DeviceController_EstablishPASESessionBLE.argtypes = [c_void_p, c_uint32, c_uint16, c_uint64] + dm_lib.pychip_DeviceController_EstablishPASESessionBLE.restype = PyChipError - self._dmLib.pychip_DeviceController_EstablishPASESession.argtypes = [ - c_void_p, c_char_p, c_uint64] - self._dmLib.pychip_DeviceController_EstablishPASESession.restype = PyChipError + dm_lib.pychip_DeviceController_EstablishPASESession.argtypes = [c_void_p, c_char_p, c_uint64] + dm_lib.pychip_DeviceController_EstablishPASESession.restype = PyChipError - self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.argtypes = [c_void_p] - self._dmLib.pychip_DeviceController_HasDiscoveredCommissionableNode.restype = c_bool + dm_lib.pychip_DeviceController_HasDiscoveredCommissionableNode.argtypes = [c_void_p] + dm_lib.pychip_DeviceController_HasDiscoveredCommissionableNode.restype = c_bool - self._dmLib.pychip_DeviceController_GetIPForDiscoveredDevice.argtypes = [ - c_void_p, c_int, c_char_p, c_uint32] - self._dmLib.pychip_DeviceController_GetIPForDiscoveredDevice.restype = c_bool + dm_lib.pychip_DeviceController_GetIPForDiscoveredDevice.argtypes = [c_void_p, c_int, c_char_p, c_uint32] + dm_lib.pychip_DeviceController_GetIPForDiscoveredDevice.restype = c_bool - self._dmLib.pychip_DeviceController_ConnectIP.argtypes = [ - c_void_p, c_char_p, c_uint32, c_uint64] - self._dmLib.pychip_DeviceController_ConnectIP.restype = PyChipError + dm_lib.pychip_DeviceController_ConnectIP.argtypes = [c_void_p, c_char_p, c_uint32, c_uint64] + dm_lib.pychip_DeviceController_ConnectIP.restype = PyChipError - self._dmLib.pychip_DeviceController_ConnectWithCode.argtypes = [ - c_void_p, c_char_p, c_uint64, c_uint8] - self._dmLib.pychip_DeviceController_ConnectWithCode.restype = PyChipError + dm_lib.pychip_DeviceController_ConnectWithCode.argtypes = [c_void_p, c_char_p, c_uint64, c_uint8] + dm_lib.pychip_DeviceController_ConnectWithCode.restype = PyChipError - self._dmLib.pychip_DeviceController_UnpairDevice.argtypes = [ - c_void_p, c_uint64, _DeviceUnpairingCompleteFunct] - self._dmLib.pychip_DeviceController_UnpairDevice.restype = PyChipError + dm_lib.pychip_DeviceController_UnpairDevice.argtypes = [c_void_p, c_uint64, _DeviceUnpairingCompleteFunct] + dm_lib.pychip_DeviceController_UnpairDevice.restype = PyChipError - self._dmLib.pychip_DeviceController_MarkSessionDefunct.argtypes = [ - c_void_p, c_uint64] - self._dmLib.pychip_DeviceController_MarkSessionDefunct.restype = PyChipError + dm_lib.pychip_DeviceController_MarkSessionDefunct.argtypes = [c_void_p, c_uint64] + dm_lib.pychip_DeviceController_MarkSessionDefunct.restype = PyChipError - self._dmLib.pychip_DeviceController_MarkSessionForEviction.argtypes = [ - c_void_p, c_uint64] - self._dmLib.pychip_DeviceController_MarkSessionForEviction.restype = PyChipError + dm_lib.pychip_DeviceController_MarkSessionForEviction.argtypes = [c_void_p, c_uint64] + dm_lib.pychip_DeviceController_MarkSessionForEviction.restype = PyChipError - self._dmLib.pychip_DeviceController_DeleteAllSessionResumption.argtypes = [ - c_void_p] - self._dmLib.pychip_DeviceController_DeleteAllSessionResumption.restype = PyChipError + dm_lib.pychip_DeviceController_DeleteAllSessionResumption.argtypes = [c_void_p] + dm_lib.pychip_DeviceController_DeleteAllSessionResumption.restype = PyChipError - try: - self._dmLib.pychip_DeviceController_SetLocalMRPConfig.restype = PyChipError - self._dmLib.pychip_DeviceController_SetLocalMRPConfig.argtypes = [c_uint32, c_uint32, c_uint16] + with contextlib.suppress(AttributeError): + dm_lib.pychip_DeviceController_SetLocalMRPConfig.restype = PyChipError + dm_lib.pychip_DeviceController_SetLocalMRPConfig.argtypes = [c_uint32, c_uint32, c_uint16] - self._dmLib.pychip_DeviceController_ResetLocalMRPConfig.restype = PyChipError - self._dmLib.pychip_DeviceController_ResetLocalMRPConfig.argtypes = [] - except AttributeError: - # This can happen if the SDK is not built with CHIP_DEVICE_CONFIG_ENABLE_DYNAMIC_MRP_CONFIG - def _unsupported_dynamic_mrp(*args, **kwargs): - raise NotImplementedError("Dynamic MRP config support is not available in this Matter SDK build.") - self._dmLib.pychip_DeviceController_SetLocalMRPConfig = _unsupported_dynamic_mrp - self._dmLib.pychip_DeviceController_ResetLocalMRPConfig = _unsupported_dynamic_mrp + dm_lib.pychip_DeviceController_ResetLocalMRPConfig.restype = PyChipError + dm_lib.pychip_DeviceController_ResetLocalMRPConfig.argtypes = [] - self._dmLib.pychip_DeviceController_GetAddressAndPort.argtypes = [ - c_void_p, c_uint64, c_char_p, c_uint64, POINTER(c_uint16)] - self._dmLib.pychip_DeviceController_GetAddressAndPort.restype = PyChipError + dm_lib.pychip_DeviceController_GetAddressAndPort.argtypes = [c_void_p, c_uint64, c_char_p, c_uint64, POINTER(c_uint16)] + dm_lib.pychip_DeviceController_GetAddressAndPort.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnPairingCompleteFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnPairingCompleteFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetKeyExchangeCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnCommissioningCompleteFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnCommissioningCompleteFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningCompleteCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnOpenWindowCompleteFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnOpenWindowCompleteFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetOpenWindowCompleteCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnCommissioningStatusUpdateFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnCommissioningStatusUpdateFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningStatusUpdateCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStageStartCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnCommissioningStageStartFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetCommissioningStageStartCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningStageStartCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnCommissioningStageStartFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetCommissioningStageStartCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.argtypes = [ - c_void_p, _DevicePairingDelegate_OnFabricCheckFunct] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.argtypes = [ + c_void_p, _DevicePairingDelegate_OnFabricCheckFunct] + dm_lib.pychip_ScriptDevicePairingDelegate_SetFabricCheckCallback.restype = PyChipError - self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.argtypes = [ - c_void_p, c_bool] - self._dmLib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.restype = PyChipError + dm_lib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.argtypes = [c_void_p, c_bool] + dm_lib.pychip_ScriptDevicePairingDelegate_SetExpectingPairingComplete.restype = PyChipError - self._dmLib.pychip_GetConnectedDeviceByNodeId.argtypes = [ - c_void_p, c_uint64, py_object, _DeviceAvailableCallbackFunct, c_int] - self._dmLib.pychip_GetConnectedDeviceByNodeId.restype = PyChipError + dm_lib.pychip_GetConnectedDeviceByNodeId.argtypes = [c_void_p, c_uint64, py_object, _DeviceAvailableCallbackFunct, c_int] + dm_lib.pychip_GetConnectedDeviceByNodeId.restype = PyChipError - self._dmLib.pychip_FreeOperationalDeviceProxy.argtypes = [ - c_void_p] - self._dmLib.pychip_FreeOperationalDeviceProxy.restype = PyChipError + dm_lib.pychip_FreeOperationalDeviceProxy.argtypes = [c_void_p] + dm_lib.pychip_FreeOperationalDeviceProxy.restype = PyChipError - self._dmLib.pychip_GetDeviceBeingCommissioned.argtypes = [ - c_void_p, c_uint64, c_void_p] - self._dmLib.pychip_GetDeviceBeingCommissioned.restype = PyChipError + dm_lib.pychip_GetDeviceBeingCommissioned.argtypes = [c_void_p, c_uint64, c_void_p] + dm_lib.pychip_GetDeviceBeingCommissioned.restype = PyChipError - self._dmLib.pychip_ExpireSessions.argtypes = [c_void_p, c_uint64] - self._dmLib.pychip_ExpireSessions.restype = PyChipError + dm_lib.pychip_ExpireSessions.argtypes = [c_void_p, c_uint64] + dm_lib.pychip_ExpireSessions.restype = PyChipError - self._dmLib.pychip_DeviceCommissioner_CloseBleConnection.argtypes = [ - c_void_p] - self._dmLib.pychip_DeviceCommissioner_CloseBleConnection.restype = PyChipError + dm_lib.pychip_DeviceCommissioner_CloseBleConnection.argtypes = [c_void_p] + dm_lib.pychip_DeviceCommissioner_CloseBleConnection.restype = PyChipError - self._dmLib.pychip_GetCommandSenderHandle.argtypes = [c_void_p] - self._dmLib.pychip_GetCommandSenderHandle.restype = c_uint64 + dm_lib.pychip_GetCommandSenderHandle.argtypes = [c_void_p] + dm_lib.pychip_GetCommandSenderHandle.restype = c_uint64 - self._dmLib.pychip_DeviceController_GetCompressedFabricId.argtypes = [ - c_void_p, POINTER(c_uint64)] - self._dmLib.pychip_DeviceController_GetCompressedFabricId.restype = PyChipError + dm_lib.pychip_DeviceController_GetCompressedFabricId.argtypes = [c_void_p, POINTER(c_uint64)] + dm_lib.pychip_DeviceController_GetCompressedFabricId.restype = PyChipError - self._dmLib.pychip_DeviceController_OpenCommissioningWindow.argtypes = [ - c_void_p, c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] - self._dmLib.pychip_DeviceController_OpenCommissioningWindow.restype = PyChipError + dm_lib.pychip_DeviceController_OpenCommissioningWindow.argtypes = [ + c_void_p, c_void_p, c_uint64, c_uint16, c_uint32, c_uint16, c_uint8] + dm_lib.pychip_DeviceController_OpenCommissioningWindow.restype = PyChipError - try: - # NOTE: Joint Fabric is an optional feature in the Matter SDK core library. - # Build with CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC=1 to enable it. - self._dmLib.pychip_DeviceController_OpenJointCommissioningWindow.argtypes = [ - c_void_p, c_void_p, c_uint64, c_uint16, c_uint16, c_uint32, c_uint16] - self._dmLib.pychip_DeviceController_OpenJointCommissioningWindow.restype = PyChipError - except AttributeError: - def _unsupported_joint_fabric(*args, **kwargs): - raise NotImplementedError("Joint Fabric support is not available in this Matter SDK build.") - self._dmLib.pychip_DeviceController_OpenJointCommissioningWindow = _unsupported_joint_fabric + with contextlib.suppress(AttributeError): + # NOTE: Joint Fabric is an optional feature in the Matter SDK core library. + # Build with CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC=1 to enable it. + dm_lib.pychip_DeviceController_OpenJointCommissioningWindow.argtypes = [ + c_void_p, c_void_p, c_uint64, c_uint16, c_uint16, c_uint32, c_uint16] + dm_lib.pychip_DeviceController_OpenJointCommissioningWindow.restype = PyChipError + + dm_lib.pychip_TestCommissionerUsed.argtypes = [] + dm_lib.pychip_TestCommissionerUsed.restype = c_bool - self._dmLib.pychip_TestCommissionerUsed.argtypes = [] - self._dmLib.pychip_TestCommissionerUsed.restype = c_bool + dm_lib.pychip_TestCommissioningCallbacks.argtypes = [] + dm_lib.pychip_TestCommissioningCallbacks.restype = c_bool - self._dmLib.pychip_TestCommissioningCallbacks.argtypes = [] - self._dmLib.pychip_TestCommissioningCallbacks.restype = c_bool + dm_lib.pychip_TestCommissioningStageSuccessful.argtypes = [c_uint8] + dm_lib.pychip_TestCommissioningStageSuccessful.restype = c_bool - self._dmLib.pychip_TestCommissioningStageSuccessful.argtypes = [c_uint8] - self._dmLib.pychip_TestCommissioningStageSuccessful.restype = c_bool + dm_lib.pychip_ResetCommissioningTests.argtypes = [] + dm_lib.pychip_TestPaseConnection.argtypes = [c_uint64] - self._dmLib.pychip_ResetCommissioningTests.argtypes = [] - self._dmLib.pychip_TestPaseConnection.argtypes = [c_uint64] + dm_lib.pychip_SetTestCommissionerSimulateFailureOnStage.argtypes = [c_uint8] + dm_lib.pychip_SetTestCommissionerSimulateFailureOnStage.restype = c_bool - self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.argtypes = [ - c_uint8] - self._dmLib.pychip_SetTestCommissionerSimulateFailureOnStage.restype = c_bool + dm_lib.pychip_SetTestCommissionerSimulateFailureOnReport.argtypes = [c_uint8] + dm_lib.pychip_SetTestCommissionerSimulateFailureOnReport.restype = c_bool - self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.argtypes = [ - c_uint8] - self._dmLib.pychip_SetTestCommissionerSimulateFailureOnReport.restype = c_bool + dm_lib.pychip_SetTestCommissionerPrematureCompleteAfter.argtypes = [c_uint8] + dm_lib.pychip_SetTestCommissionerPrematureCompleteAfter.restype = c_bool - self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter.argtypes = [ - c_uint8] - self._dmLib.pychip_SetTestCommissionerPrematureCompleteAfter.restype = c_bool + dm_lib.pychip_GetCompletionError.argtypes = [] + dm_lib.pychip_GetCompletionError.restype = PyChipError - self._dmLib.pychip_GetCompletionError.argtypes = [] - self._dmLib.pychip_GetCompletionError.restype = PyChipError + dm_lib.pychip_GetCommissioningRCACData.argtypes = [ctypes.POINTER(ctypes.c_uint8), ctypes.POINTER(ctypes.c_size_t), + ctypes.c_size_t] + dm_lib.pychip_GetCommissioningRCACData.restype = None - self._dmLib.pychip_GetCommissioningRCACData.argtypes = [ctypes.POINTER( - ctypes.c_uint8), ctypes.POINTER(ctypes.c_size_t), ctypes.c_size_t] - self._dmLib.pychip_GetCommissioningRCACData.restype = None + dm_lib.pychip_DeviceController_IssueNOCChain.argtypes = [c_void_p, py_object, c_char_p, c_size_t, c_uint64] + dm_lib.pychip_DeviceController_IssueNOCChain.restype = PyChipError - self._dmLib.pychip_DeviceController_IssueNOCChain.argtypes = [ - c_void_p, py_object, c_char_p, c_size_t, c_uint64] - self._dmLib.pychip_DeviceController_IssueNOCChain.restype = PyChipError + dm_lib.pychip_OpCreds_InitGroupTestingData.argtypes = [c_void_p] + dm_lib.pychip_OpCreds_InitGroupTestingData.restype = PyChipError - self._dmLib.pychip_OpCreds_InitGroupTestingData.argtypes = [ - c_void_p] - self._dmLib.pychip_OpCreds_InitGroupTestingData.restype = PyChipError + dm_lib.pychip_OpCreds_SetKeySet.argtypes = [ + c_void_p, c_uint16, c_uint8, c_uint8, + c_char_p, c_uint64, + c_char_p, c_uint64, + c_char_p, c_uint64] + dm_lib.pychip_OpCreds_SetKeySet.restype = PyChipError - self._dmLib.pychip_OpCreds_SetKeySet.argtypes = [ - c_void_p, c_uint16, c_uint8, c_uint8, - c_char_p, c_uint64, - c_char_p, c_uint64, - c_char_p, c_uint64] - self._dmLib.pychip_OpCreds_SetKeySet.restype = PyChipError + dm_lib.pychip_OpCreds_SetGroupInfo.argtypes = [c_void_p, c_uint16, c_char_p, c_uint8] + dm_lib.pychip_OpCreds_SetGroupInfo.restype = PyChipError - self._dmLib.pychip_OpCreds_SetGroupInfo.argtypes = [c_void_p, c_uint16, c_char_p, c_uint8] - self._dmLib.pychip_OpCreds_SetGroupInfo.restype = PyChipError + dm_lib.pychip_OpCreds_AddGroupEndpoint.argtypes = [c_void_p, c_uint16, c_uint16] + dm_lib.pychip_OpCreds_AddGroupEndpoint.restype = PyChipError - self._dmLib.pychip_OpCreds_AddGroupEndpoint.argtypes = [c_void_p, c_uint16, c_uint16] - self._dmLib.pychip_OpCreds_AddGroupEndpoint.restype = PyChipError + dm_lib.pychip_OpCreds_SetGroupKey.argtypes = [c_void_p, c_uint16, c_uint16] + dm_lib.pychip_OpCreds_SetGroupKey.restype = PyChipError - self._dmLib.pychip_OpCreds_SetGroupKey.argtypes = [c_void_p, c_uint16, c_uint16] - self._dmLib.pychip_OpCreds_SetGroupKey.restype = PyChipError + dm_lib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.argtypes = [_IssueNOCChainCallbackPythonCallbackFunct] + dm_lib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.restype = None - self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.argtypes = [ - _IssueNOCChainCallbackPythonCallbackFunct] - self._dmLib.pychip_DeviceController_SetIssueNOCChainCallbackPythonCallback.restype = None + dm_lib.pychip_DeviceController_GetNodeId.argtypes = [c_void_p, POINTER(c_uint64)] + dm_lib.pychip_DeviceController_GetNodeId.restype = PyChipError - self._dmLib.pychip_DeviceController_GetNodeId.argtypes = [c_void_p, POINTER(c_uint64)] - self._dmLib.pychip_DeviceController_GetNodeId.restype = PyChipError + dm_lib.pychip_DeviceController_GetFabricId.argtypes = [c_void_p, POINTER(c_uint64)] + dm_lib.pychip_DeviceController_GetFabricId.restype = PyChipError - self._dmLib.pychip_DeviceController_GetFabricId.argtypes = [c_void_p, POINTER(c_uint64)] - self._dmLib.pychip_DeviceController_GetFabricId.restype = PyChipError + dm_lib.pychip_DeviceController_GetFabricIndex.argtypes = [c_void_p, POINTER(c_uint8)] + dm_lib.pychip_DeviceController_GetFabricIndex.restype = PyChipError - self._dmLib.pychip_DeviceController_GetFabricIndex.argtypes = [c_void_p, POINTER(c_uint8)] - self._dmLib.pychip_DeviceController_GetFabricIndex.restype = PyChipError + dm_lib.pychip_DeviceController_GetLogFilter.argtypes = [] + dm_lib.pychip_DeviceController_GetLogFilter.restype = c_uint8 - self._dmLib.pychip_DeviceController_GetLogFilter = [None] - self._dmLib.pychip_DeviceController_GetLogFilter = c_uint8 + dm_lib.pychip_DeviceController_GetRootPublicKeyBytes.argtypes = [c_void_p, POINTER(c_uint8), POINTER(c_size_t)] + dm_lib.pychip_DeviceController_GetRootPublicKeyBytes.restype = PyChipError - self._dmLib.pychip_DeviceController_GetRootPublicKeyBytes.argtypes = [c_void_p, POINTER(c_uint8), POINTER(c_size_t)] - self._dmLib.pychip_DeviceController_GetRootPublicKeyBytes.restype = PyChipError + dm_lib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER(c_void_p), POINTER(c_void_p), c_uint64, c_uint64, + c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, + c_void_p] + dm_lib.pychip_OpCreds_AllocateController.restype = PyChipError - self._dmLib.pychip_OpCreds_AllocateController.argtypes = [c_void_p, POINTER( - c_void_p), POINTER(c_void_p), c_uint64, c_uint64, c_uint16, c_char_p, c_bool, c_bool, POINTER(c_uint32), c_uint32, c_void_p] - self._dmLib.pychip_OpCreds_AllocateController.restype = PyChipError + dm_lib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.argtypes = [ + POINTER(c_void_p), POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), + c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] + dm_lib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.restype = PyChipError - self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.argtypes = [ - POINTER(c_void_p), POINTER(c_void_p), c_void_p, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, POINTER(c_char), c_uint32, c_uint16, c_bool] - self._dmLib.pychip_OpCreds_AllocateControllerForPythonCommissioningFLow.restype = PyChipError + dm_lib.pychip_DeviceController_SetIpk.argtypes = [c_void_p, POINTER(c_char), c_size_t] + dm_lib.pychip_DeviceController_SetIpk.restype = PyChipError - self._dmLib.pychip_DeviceController_SetIpk.argtypes = [c_void_p, POINTER(c_char), c_size_t] - self._dmLib.pychip_DeviceController_SetIpk.restype = PyChipError + dm_lib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.restype = None + dm_lib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.argtypes = [_OnCheckInCompleteFunct] - self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.restype = None - self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback.argtypes = [_OnCheckInCompleteFunct] + dm_lib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback(_OnCheckInComplete) - self._dmLib.pychip_CheckInDelegate_SetOnCheckInCompleteCallback(_OnCheckInComplete) + dm_lib.pychip_DeviceProxy_GetRemoteSessionParameters.restype = PyChipError + dm_lib.pychip_DeviceProxy_GetRemoteSessionParameters.argtypes = [c_void_p, c_char_p] - self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters.restype = PyChipError - self._dmLib.pychip_DeviceProxy_GetRemoteSessionParameters.argtypes = [c_void_p, c_char_p] + dm_lib.pychip_CreateManualCode.restype = PyChipError + dm_lib.pychip_CreateManualCode.argtypes = [c_uint16, c_uint32, c_char_p, c_size_t, POINTER(c_size_t)] - self._dmLib.pychip_CreateManualCode.restype = PyChipError - self._dmLib.pychip_CreateManualCode.argtypes = [c_uint16, c_uint32, c_char_p, c_size_t, POINTER(c_size_t)] + dm_lib.pychip_DeviceController_SetSkipCommissioningComplete.restype = PyChipError + dm_lib.pychip_DeviceController_SetSkipCommissioningComplete.argtypes = [c_bool] - self._dmLib.pychip_DeviceController_SetSkipCommissioningComplete.restype = PyChipError - self._dmLib.pychip_DeviceController_SetSkipCommissioningComplete.argtypes = [c_bool] + dm_lib.pychip_DeviceController_SetTermsAcknowledgements.restype = PyChipError + dm_lib.pychip_DeviceController_SetTermsAcknowledgements.argtypes = [c_uint16, c_uint16] - self._dmLib.pychip_DeviceController_SetTermsAcknowledgements.restype = PyChipError - self._dmLib.pychip_DeviceController_SetTermsAcknowledgements.argtypes = [c_uint16, c_uint16] + dm_lib.pychip_DeviceController_SetDACRevocationSetPath.restype = PyChipError + dm_lib.pychip_DeviceController_SetDACRevocationSetPath.argtypes = [c_char_p] - self._dmLib.pychip_DeviceController_SetDACRevocationSetPath.restype = PyChipError - self._dmLib.pychip_DeviceController_SetDACRevocationSetPath.argtypes = [c_char_p] + return dm_lib class ChipDeviceController(ChipDeviceControllerBase): @@ -2893,11 +2812,11 @@ def __init__(self, nodeId: int, adminVendorId: int, fabricAdmin: FabricAdmin.FabricAdmin, - catTags: typing.List[int] = [], + catTags: list[int] = [], paaTrustStorePath: str = "", useTestCommissioner: bool = False, name: str = '', - keypair: typing.Optional[p256keypair.P256Keypair] = None): + keypair: p256keypair.P256Keypair | None = None): super().__init__( name or f"caIndex({fabricAdmin.caIndex:x})/fabricId(0x{fabricId:016X})/nodeId(0x{nodeId:016X})" @@ -2938,7 +2857,7 @@ def caIndex(self) -> int: return self._caIndex @property - def fabricAdmin(self) -> typing.Optional[FabricAdmin.FabricAdmin]: + def fabricAdmin(self) -> FabricAdmin.FabricAdmin | None: return self._fabricAdmin async def Commission(self, nodeId: int) -> int: @@ -2961,16 +2880,17 @@ async def Commission(self, nodeId: int) -> int: ''' self.CheckIsActive() - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(False) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_Commission( self.devCtrl, nodeId) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) - async def CommissionBleThread(self, discriminator, setupPinCode, nodeId: int, threadOperationalDataset: bytes, isShortDiscriminator: bool = False) -> int: + async def CommissionBleThread(self, discriminator: int, setupPinCode: int, nodeId: int, threadOperationalDataset: bytes, + isShortDiscriminator: bool = False) -> int: ''' Commissions a Thread device over BLE. @@ -2987,7 +2907,7 @@ async def CommissionBleThread(self, discriminator, setupPinCode, nodeId: int, th self.SetThreadOperationalDataset(threadOperationalDataset) return await self.ConnectBLE(discriminator, setupPinCode, nodeId, isShortDiscriminator) - async def CommissionNfcThread(self, discriminator, setupPinCode, nodeId: int, threadOperationalDataset: bytes) -> int: + async def CommissionNfcThread(self, discriminator: int, setupPinCode: int, nodeId: int, threadOperationalDataset: bytes) -> int: ''' Commissions a Thread device over NFC. @@ -3003,7 +2923,7 @@ async def CommissionNfcThread(self, discriminator, setupPinCode, nodeId: int, th self.SetThreadOperationalDataset(threadOperationalDataset) return await self.ConnectNFC(discriminator, setupPinCode, nodeId) - async def CommissionNfcWiFi(self, discriminator, setupPinCode, nodeId: int, ssid: str, credentials: str) -> int: + async def CommissionNfcWiFi(self, discriminator: int, setupPinCode: int, nodeId: int, ssid: str, credentials: str) -> int: ''' Commissions a Wi-Fi device over NFC. @@ -3020,7 +2940,8 @@ async def CommissionNfcWiFi(self, discriminator, setupPinCode, nodeId: int, ssid self.SetWiFiCredentials(ssid, credentials) return await self.ConnectNFC(discriminator, setupPinCode, nodeId) - async def CommissionBleWiFi(self, discriminator, setupPinCode, nodeId: int, ssid: str, credentials: str, isShortDiscriminator: bool = False) -> int: + async def CommissionBleWiFi(self, discriminator: int, setupPinCode: int, nodeId: int, ssid: str, credentials: str, + isShortDiscriminator: bool = False) -> int: ''' Commissions a Wi-Fi device over BLE. @@ -3038,7 +2959,7 @@ async def CommissionBleWiFi(self, discriminator, setupPinCode, nodeId: int, ssid self.SetWiFiCredentials(ssid, credentials) return await self.ConnectBLE(discriminator, setupPinCode, nodeId, isShortDiscriminator) - def SetWiFiCredentials(self, ssid: str, credentials: str): + def SetWiFiCredentials(self, ssid: str, credentials: str) -> None: ''' Set the Wi-Fi credentials to set during commissioning. @@ -3056,7 +2977,7 @@ def SetWiFiCredentials(self, ssid: str, credentials: str): ssid.encode("utf-8"), credentials.encode("utf-8")) ).raise_on_error() - def SetThreadOperationalDataset(self, threadOperationalDataset): + def SetThreadOperationalDataset(self, threadOperationalDataset: bytes) -> None: ''' Set the Thread operational dataset to set during commissioning. @@ -3073,7 +2994,7 @@ def SetThreadOperationalDataset(self, threadOperationalDataset): threadOperationalDataset, len(threadOperationalDataset)) ).raise_on_error() - def ResetCommissioningParameters(self): + def ResetCommissioningParameters(self) -> None: ''' Sets the commissioning parameters back to the default values. @@ -3085,7 +3006,7 @@ def ResetCommissioningParameters(self): lambda: self._dmLib.pychip_DeviceController_ResetCommissioningParameters() ).raise_on_error() - def SetTimeZone(self, offset: int, validAt: int, name: str = ""): + def SetTimeZone(self, offset: int, validAt: int, name: str = "") -> None: ''' Set the time zone to set during commissioning. Currently only one time zone entry is supported. @@ -3102,7 +3023,7 @@ def SetTimeZone(self, offset: int, validAt: int, name: str = ""): lambda: self._dmLib.pychip_DeviceController_SetTimeZone(offset, validAt, name.encode("utf-8")) ).raise_on_error() - def SetDSTOffset(self, offset: int, validStarting: int, validUntil: int): + def SetDSTOffset(self, offset: int, validStarting: int, validUntil: int) -> None: ''' Set the DST offset to set during commissioning. Currently only one DST entry is supported. @@ -3119,7 +3040,7 @@ def SetDSTOffset(self, offset: int, validStarting: int, validUntil: int): lambda: self._dmLib.pychip_DeviceController_SetDSTOffset(offset, validStarting, validUntil) ).raise_on_error() - def SetTCAcknowledgements(self, tcAcceptedVersion: int, tcUserResponse: int): + def SetTCAcknowledgements(self, tcAcceptedVersion: int, tcUserResponse: int) -> None: ''' Set the TC acknowledgements to set during commissioning. @@ -3135,7 +3056,7 @@ def SetTCAcknowledgements(self, tcAcceptedVersion: int, tcUserResponse: int): lambda: self._dmLib.pychip_DeviceController_SetTermsAcknowledgements(tcAcceptedVersion, tcUserResponse) ).raise_on_error() - def SetSkipCommissioningComplete(self, skipCommissioningComplete: bool): + def SetSkipCommissioningComplete(self, skipCommissioningComplete: bool) -> None: ''' Set whether to skip the commissioning complete callback. @@ -3150,7 +3071,7 @@ def SetSkipCommissioningComplete(self, skipCommissioningComplete: bool): lambda: self._dmLib.pychip_DeviceController_SetSkipCommissioningComplete(skipCommissioningComplete) ).raise_on_error() - def SetDefaultNTP(self, defaultNTP: str): + def SetDefaultNTP(self, defaultNTP: str) -> None: ''' Set the DefaultNTP to set during commissioning. @@ -3165,7 +3086,7 @@ def SetDefaultNTP(self, defaultNTP: str): lambda: self._dmLib.pychip_DeviceController_SetDefaultNtp(defaultNTP.encode("utf-8")) ).raise_on_error() - def SetTrustedTimeSource(self, nodeId: int, endpoint: int): + def SetTrustedTimeSource(self, nodeId: int, endpoint: int) -> None: ''' Set the trusted time source nodeId to set during commissioning. This must be a node on the commissioner fabric. @@ -3181,7 +3102,7 @@ def SetTrustedTimeSource(self, nodeId: int, endpoint: int): lambda: self._dmLib.pychip_DeviceController_SetTrustedTimeSource(nodeId, endpoint) ).raise_on_error() - def SetCheckMatchingFabric(self, check: bool): + def SetCheckMatchingFabric(self, check: bool) -> None: ''' Instructs the auto-commissioner to perform a matching fabric check before commissioning. @@ -3196,7 +3117,7 @@ def SetCheckMatchingFabric(self, check: bool): lambda: self._dmLib.pychip_DeviceController_SetCheckMatchingFabric(check) ).raise_on_error() - def GenerateICDRegistrationParameters(self): + def GenerateICDRegistrationParameters(self) -> ICDRegistrationParameters: ''' Generates ICD registration parameters for this controller. @@ -3210,9 +3131,9 @@ def GenerateICDRegistrationParameters(self): self._nodeId, self._nodeId, 30, - Clusters.IcdManagement.Enums.ClientTypeEnum.kPermanent) + Clusters.IcdManagement.Enums.ClientTypeEnum(Clusters.IcdManagement.Enums.ClientTypeEnum.kPermanent)) - def EnableICDRegistration(self, parameters: ICDRegistrationParameters): + def EnableICDRegistration(self, parameters: ICDRegistrationParameters) -> None: ''' Enables ICD registration for the following commissioning session. Args: @@ -3232,7 +3153,7 @@ def EnableICDRegistration(self, parameters: ICDRegistrationParameters): True, pointer(parameters.to_c())) ).raise_on_error() - def DisableICDRegistration(self): + def DisableICDRegistration(self) -> None: ''' Disables ICD registration. @@ -3285,14 +3206,14 @@ async def CommissionOnNetwork(self, nodeId: int, setupPinCode: int, if isinstance(filter, int): filter = str(filter) - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_OnNetworkCommission( self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, int(filterType), str(filter).encode("utf-8") if filter is not None else None, discoveryTimeoutMsec) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) async def CommissionThreadMeshcop(self, nodeId: int, setupPinCode: int, discriminator: int, borderAgentIPAddr: str, @@ -3312,16 +3233,16 @@ async def CommissionThreadMeshcop(self, nodeId: int, setupPinCode: int, self.CheckIsActive() self.SetThreadOperationalDataset(threadOperationalDataset) - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_ThreadMeshcopCommission( self.devCtrl, self.pairingDelegate, nodeId, setupPinCode, discriminator, borderAgentIPAddr.encode("utf-8"), borderAgentPort) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) - def get_rcac(self): + def get_rcac(self) -> bytes | None: ''' Passes captured RCAC data back to Python test modules for validation - Setting buffer size to max size mentioned in spec: @@ -3352,7 +3273,8 @@ def get_rcac(self): return None return rcac_bytes - async def CommissionWithCode(self, setupPayload: str, nodeId: int, discoveryType: DiscoveryType = DiscoveryType.DISCOVERY_ALL) -> int: + async def CommissionWithCode(self, setupPayload: str, nodeId: int, + discoveryType: DiscoveryType = DiscoveryType.DISCOVERY_ALL) -> int: ''' Commission with the given node ID from the setupPayload. setupPayload may be a QR or manual code. @@ -3370,14 +3292,14 @@ async def CommissionWithCode(self, setupPayload: str, nodeId: int, discoveryType ''' self.CheckIsActive() - async with self._commissioning_context as ctx: + async with self._commissioning_context as ctx_future: self._enablePairingCompleteCallback(True) await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_ConnectWithCode( self.devCtrl, setupPayload.encode("utf-8"), nodeId, discoveryType.value) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) def NOCChainCallback(self, nocChain): ''' @@ -3395,7 +3317,7 @@ def NOCChainCallback(self, nocChain): self._issue_node_chain_context.future.set_result(nocChain) return - async def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRResponse, nodeId: int): + async def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRResponse, nodeId: int) -> asyncio.Future: ''' Issue an NOC chain using the associated OperationalCredentialsDelegate. The NOC chain will be provided in TLV cert format. @@ -3409,15 +3331,15 @@ async def IssueNOCChain(self, csr: Clusters.OperationalCredentials.Commands.CSRR ''' self.CheckIsActive() - async with self._issue_node_chain_context as ctx: + async with self._issue_node_chain_context as ctx_future: await self._ChipStack.CallAsync( lambda: self._dmLib.pychip_DeviceController_IssueNOCChain( self.devCtrl, py_object(self), csr.NOCSRElements, len(csr.NOCSRElements), nodeId) ) - return await asyncio.futures.wrap_future(ctx.future) + return await asyncio.futures.wrap_future(ctx_future) - def SetDACRevocationSetPath(self, dacRevocationSetPath: typing.Optional[str]): + def SetDACRevocationSetPath(self, dacRevocationSetPath: str | None) -> None: ''' Set the path to the device attestation revocation set JSON file. @@ -3440,7 +3362,7 @@ class BareChipDeviceController(ChipDeviceControllerBase): ''' def __init__(self, operationalKey: p256keypair.P256Keypair, noc: bytes, - icac: typing.Union[bytes, None], rcac: bytes, ipk: typing.Union[bytes, None], adminVendorId: int, name: typing.Optional[str] = None): + icac: bytes | None, rcac: bytes, ipk: bytes | None, adminVendorId: int, name: str | None = None): ''' Creates a controller without AutoCommissioner. diff --git a/src/controller/python/matter/clusters/Attribute.py b/src/controller/python/matter/clusters/Attribute.py index e3eb1a2b51f7e1..29913a8050f760 100644 --- a/src/controller/python/matter/clusters/Attribute.py +++ b/src/controller/python/matter/clusters/Attribute.py @@ -63,7 +63,7 @@ class AttributePath: AttributeId: Optional[int] = None @staticmethod - def from_cluster(EndpointId: int, Cluster: Cluster) -> AttributePath: + def from_cluster(EndpointId: int, Cluster: type[Cluster]) -> AttributePath: if Cluster is None: raise ValueError("Cluster cannot be None") return AttributePath(EndpointId=EndpointId, ClusterId=Cluster.id) @@ -85,7 +85,7 @@ class DataVersionFilter: DataVersion: Optional[int] = None @staticmethod - def from_cluster(EndpointId: int, Cluster: Cluster, DataVersion: int) -> DataVersionFilter: + def from_cluster(EndpointId: int, Cluster: type[Cluster], DataVersion: int) -> DataVersionFilter: if Cluster is None: raise ValueError("Cluster cannot be None") return DataVersionFilter(EndpointId=EndpointId, ClusterId=Cluster.id, DataVersion=DataVersion) @@ -352,7 +352,7 @@ def UpdateTLV(self, path: AttributePath, dataVersion: int, data: Union[bytes, Va # For this path the attribute cache still requires an update. self._attributeCacheUpdateNeeded.add(path) - def GetUpdatedAttributeCache(self) -> Dict[int, List[Cluster]]: + def GetUpdatedAttributeCache(self) -> dict[int, list[Cluster]]: ''' This converts the raw TLV data into a cluster object format. Two formats are available: @@ -685,7 +685,7 @@ def _BuildEventIndex(): class AsyncReadTransaction: @dataclass class ReadResponse: - attributes: dict[Any, Any] + attributes: dict[int, list[Cluster]] events: list[ClusterEvent] tlvAttributes: dict[int, Any] diff --git a/src/controller/python/matter/clusters/__init__.py b/src/controller/python/matter/clusters/__init__.py index 14dc6f3e1d4185..c431dba6ddf3c8 100644 --- a/src/controller/python/matter/clusters/__init__.py +++ b/src/controller/python/matter/clusters/__init__.py @@ -15,5 +15,5 @@ # limitations under the License. # -from . import Attribute, CHIPClusters, Command, Objects # noqa: F401 +from . import Attribute, CHIPClusters, ClusterObjects, Command, Objects # noqa: F401 from .Objects import * # noqa: F401, F403 diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py b/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py index 86a773194161bf..9a543381dfe5c1 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/apps.py @@ -16,7 +16,7 @@ from dataclasses import dataclass from sys import stderr, stdout from tempfile import NamedTemporaryFile -from typing import BinaryIO, Optional, Union +from typing import Any, BinaryIO from matter.testing.tasks import Subprocess @@ -53,7 +53,7 @@ class AppServerSubprocess(Subprocess): err_log_file: BinaryIO = stderr.buffer def __init__(self, app: str, storage_dir: str, discriminator: int, - passcode: int, port: int = 5540, extra_args: list[str] = [], kvs_path: Optional[str] = None, + passcode: int, port: int = 5540, extra_args: list[str] | None = None, kvs_path: str | None = None, f_stdout: BinaryIO = stdout.buffer, f_stderr: BinaryIO = stderr.buffer): if kvs_path is None: @@ -82,27 +82,27 @@ def __init__(self, app: str, storage_dir: str, discriminator: int, class IcdAppServerSubprocess(AppServerSubprocess): """Wrapper class for starting an ICD application server in a subprocess.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.paused = False - def pause(self, check_state: bool = True): + def pause(self, check_state: bool = True) -> None: if check_state and self.paused: raise ValueError("ICD TH Server unexpectedly is already paused") - if not self.paused: - # Stop (halt) the ICD server process by sending a SIGTOP signal. + if not self.paused and self.p is not None: + # Stop (halt) the ICD server process by sending a SIGSTOP signal. self.p.send_signal(signal.SIGSTOP) self.paused = True - def resume(self, check_state: bool = True): + def resume(self, check_state: bool = True) -> None: if check_state and not self.paused: raise ValueError("ICD TH Server unexpectedly is already running") - if self.paused: + if self.paused and self.p is not None: # Resume (continue) the ICD server process by sending a SIGCONT signal. self.p.send_signal(signal.SIGCONT) self.paused = False - def terminate(self): + def terminate(self) -> None: # Make sure the ICD server process is not paused before terminating it. self.resume(check_state=False) super().terminate() @@ -116,7 +116,7 @@ class JFAdministratorSubprocess(Subprocess): err_log_file: BinaryIO = stderr.buffer def __init__(self, app: str, prefix: str, storage_dir: str, discriminator: int, - passcode: int, port: int = 5540, extra_args: list[str] = [], kvs_path: Optional[str] = None, + passcode: int, port: int = 5540, extra_args: list[str] | None = None, kvs_path: str | None = None, f_stdout: BinaryIO = stdout.buffer, f_stderr: BinaryIO = stderr.buffer): if kvs_path is None: @@ -146,7 +146,7 @@ class JFControllerSubprocess(Subprocess): """Wrapper class for starting a joint fabric controller in a subprocess.""" def __init__(self, app: str, prefix: str, rpc_server_port: int, storage_dir: str, - vendor_id: int, extra_args: list[str] = []): + vendor_id: int, extra_args: list[str] | None = None): # Build the command list command = [app] @@ -170,9 +170,8 @@ class OTAProviderSubprocess(AppServerSubprocess): # Prefix for log messages from the OTA provider application. PREFIX = b"[OTA-PROVIDER]" - def __init__(self, app: str, storage_dir: str, discriminator: int, - passcode: int, ota_source: Union[OtaImagePath, ImageListPath], - port: int = 5541, extra_args: list[str] = [], kvs_path: Optional[str] = None, + def __init__(self, app: str, storage_dir: str, discriminator: int, passcode: int, ota_source: OtaImagePath | ImageListPath, + port: int = 5541, extra_args: list[str] | None = None, kvs_path: str | None = None, log_file: str | BinaryIO = stdout.buffer, err_log_file: str | BinaryIO = stderr.buffer): """Initialize the OTA Provider subprocess. @@ -202,21 +201,24 @@ def __init__(self, app: str, storage_dir: str, discriminator: int, err_log_file = self._err_log_file # Build OTA-specific arguments using the ota_source property - combined_extra_args = ota_source.ota_args + extra_args + combined_extra_args = ota_source.ota_args + (extra_args or []) # Initialize with the combined arguments super().__init__(app=app, storage_dir=storage_dir, discriminator=discriminator, passcode=passcode, port=port, extra_args=combined_extra_args, kvs_path=kvs_path, f_stdout=log_file, f_stderr=err_log_file) - def terminate(self): + def terminate(self) -> None: if self._log_file is not None: self._log_file.close() if self._err_log_file is not None: self._err_log_file.close() return super().terminate() - def kill(self): - self.p.send_signal(signal.SIGKILL) + def kill(self) -> None: + if self.p is not None: + self.p.send_signal(signal.SIGKILL) - def get_pid(self) -> int: + def get_pid(self) -> int | None: + if self.p is None: + return None return self.p.pid diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py b/src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py index 484fc5413a091a..cff15971ac3c5d 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/basic_composition.py @@ -31,11 +31,9 @@ import matter.clusters as Clusters import matter.tlv -from matter.ChipDeviceCtrl import ChipDeviceController from matter.clusters.Attribute import AttributeCache, ValueDecodeFailure from matter.MatterTlvJson import TLVJsonConverter from matter.testing.conformance import ConformanceException -from matter.testing.matter_test_config import MatterTestConfig from matter.testing.matter_testing import MatterBaseTest from matter.testing.problem_notices import ProblemNotice from matter.testing.spec_parsing import PrebuiltDataModelDirectory, build_xml_clusters, build_xml_device_types, dm_from_spec_version @@ -85,7 +83,7 @@ def MatterTlvToJson(tlv_data: dict[int, Any]) -> dict[str, Any]: type(None): "NULL", } - def ConvertValue(value) -> Any: + def ConvertValue(value: Any) -> Any: if isinstance(value, ValueDecodeFailure): raise ValueError(f"Bad Value: {str(value)}") @@ -139,18 +137,6 @@ def JsonToMatterTlv(json_filename: str) -> AttributeCache: class BasicCompositionTests(MatterBaseTest): - # These attributes are initialized/provided by the inheriting test class (MatterBaseTest) - # or its setup process. Providing type hints here for mypy. - default_controller: ChipDeviceController - matter_test_config: MatterTestConfig - user_params: dict[str, Any] - dut_node_id: int - problems: list[ProblemNotice] - endpoints: dict[int, Any] # Wildcard read result - endpoints_tlv: dict[int, Any] # Wildcard read result (raw TLV) - xml_clusters: dict[int, Any] - xml_device_types: dict[int, Any] - def dump_wildcard(self, dump_device_composition_path: typing.Optional[str]) -> tuple[str, str]: """ Dumps a json and a txt file of the attribute wildcard for this device if the dump_device_composition_path is supplied. Returns the json and txt as strings. @@ -166,12 +152,12 @@ def dump_wildcard(self, dump_device_composition_path: typing.Optional[str]) -> t pprint(self.endpoints, outfile, indent=1, width=200, compact=True) return (json_dump_string, pformat(self.endpoints, indent=1, width=200, compact=True)) - async def setup_class_helper(self, allow_pase: bool = True): + async def setup_class_helper(self, allow_pase: bool = True) -> None: dev_ctrl = self.default_controller self.problems: list[ProblemNotice] = [] self.test_from_file = self.user_params.get("test_from_file", None) - def log_test_start(): + def log_test_start() -> None: LOGGER.info("###########################################################") LOGGER.info("Start of actual tests") LOGGER.info("###########################################################") @@ -259,7 +245,7 @@ def _get_dm(self) -> PrebuiltDataModelDirectory: # type: ignore[return] except ConformanceException as e: asserts.fail(f"Unable to identify specification version: {e}") - def build_spec_xmls(self): + def build_spec_xmls(self) -> None: dm = self._get_dm() LOGGER.info("----------------------------------------------------------------------------------") LOGGER.info(f"-- Running tests against Specification version {dm.dirname}") diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py b/src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py index d06965619985f5..f565fdb54512d9 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/choice_conformance.py @@ -30,7 +30,8 @@ def __init__(self, location: AttributePathLocation, choice: Choice, count: int): self.count = count -def _add_to_counts_if_required(conformance_decision_with_choice: ConformanceDecisionWithChoice, element_present: bool, counts: dict[Choice, int]): +def _add_to_counts_if_required(conformance_decision_with_choice: ConformanceDecisionWithChoice, element_present: bool, + counts: dict[Choice, int]) -> None: choice = conformance_decision_with_choice.choice if not choice: return diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/conformance.py b/src/python_testing/matter_testing_infrastructure/matter/testing/conformance.py index 7d78ac7fbc7bcf..7a62376a024153 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/conformance.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/conformance.py @@ -37,7 +37,7 @@ import xml.etree.ElementTree as ElementTree from dataclasses import dataclass from enum import Enum, auto -from typing import Callable, Optional +from typing import Callable from matter.tlv import uint @@ -90,7 +90,7 @@ def __str__(self): return '.' + self.marker + more_str -def parse_choice(element: ElementTree.Element) -> Optional[Choice]: +def parse_choice(element: ElementTree.Element) -> Choice | None: choice = element.get('choice', '') if not choice: return None @@ -111,7 +111,7 @@ class ConformanceDecision(Enum): @dataclass class ConformanceDecisionWithChoice: decision: ConformanceDecision - choice: Optional[Choice] = None + choice: Choice | None = None def is_mandatory(self) -> bool: return self.decision == ConformanceDecision.MANDATORY @@ -144,7 +144,8 @@ def __init__(self): EMPTY_CLUSTER_GLOBAL_ATTRIBUTES = EmptyClusterGlobalAttributes() -def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, allow_provisional_test_event_only_disallowed_for_certification: bool): +def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, + allow_provisional_test_event_only_disallowed_for_certification: bool) -> bool: if conformance_decision.decision in [ConformanceDecision.NOT_APPLICABLE, ConformanceDecision.DISALLOWED]: return False if conformance_decision.decision == ConformanceDecision.PROVISIONAL: @@ -152,12 +153,12 @@ def conformance_allowed(conformance_decision: ConformanceDecisionWithChoice, all return True -def is_disallowed(conformance: Callable): +def is_disallowed(conformance: Callable[[ConformanceAssessmentData], ConformanceDecisionWithChoice]) -> bool: # Deprecated and disallowed conformances will come back as disallowed regardless of the implemented features / attributes / etc. return conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES).decision == ConformanceDecision.DISALLOWED -def is_provisional(conformance: Callable): +def is_provisional(conformance: Callable[[ConformanceAssessmentData], ConformanceDecisionWithChoice]) -> bool: return conformance(EMPTY_CLUSTER_GLOBAL_ATTRIBUTES).decision == ConformanceDecision.PROVISIONAL @@ -174,7 +175,7 @@ def __call__(self, conformance_assessment_data: ConformanceAssessmentData) -> Co Raises: ConformanceException if the conformance is invalid ''' raise ConformanceException('Base conformance called') - choice: Optional[Choice] = None + choice: Choice | None = None class zigbee(Conformance): @@ -194,7 +195,7 @@ def __str__(self): class optional(Conformance): - def __init__(self, choice: Optional[Choice] = None): + def __init__(self, choice: Choice | None = None): self.choice = choice def __call__(self, conformance_assessment_data: ConformanceAssessmentData) -> ConformanceDecisionWithChoice: @@ -229,7 +230,7 @@ def __str__(self): class ValueConformance(Conformance): - def __call__(self, conformance_assessment_data: ConformanceAssessmentData): + def __call__(self, conformance_assessment_data: ConformanceAssessmentData) -> ConformanceDecisionWithChoice: # This should never be called raise ConformanceException('Value conformance function should not be called - this is simply a value holder') @@ -253,7 +254,7 @@ def get_value(self, conformance_assessment_data: ConformanceAssessmentData) -> i class revision(ValueConformance): def __init__(self, value: str): - self.value: Optional[int] + self.value: int | None if value.lower() == 'current': self.value = None else: @@ -346,7 +347,7 @@ def strip_outer_parentheses(inner: str) -> str: class optional_wrapper(Conformance): - def __init__(self, op: Conformance, choice: Optional[Choice] = None): + def __init__(self, op: Conformance, choice: Choice | None = None): self.op = op self.choice = choice @@ -454,7 +455,7 @@ def __str__(self): class ArithmeticConformance(Conformance): ''' Base class for arithmetic operations - do not use directly.''' - def _type_ok(self, op1: Conformance, op2: Conformance): + def _type_ok(self, op1: Conformance, op2: Conformance) -> bool: def _is_valid_operand(op: Conformance) -> bool: return issubclass(type(op), ValueConformance) or type(op) == attribute return _is_valid_operand(op1) and _is_valid_operand(op2) diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py b/src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py index 56111c783c627e..adede567dde30d 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/decorators.py @@ -25,7 +25,7 @@ import logging from enum import IntFlag from functools import partial -from typing import TYPE_CHECKING, Callable, Type +from typing import TYPE_CHECKING, Awaitable, Callable, Concatenate, ParamSpec, Type, TypeVar from mobly import asserts @@ -247,13 +247,19 @@ def has_feature(cluster: ClusterObjects.ClusterObjectDescriptor, feature: IntFla return partial(_has_feature, cluster=cluster, feature=feature) -def _async_runner(body, test_instance, *args, **kwargs): +TestInstanceT = TypeVar("TestInstanceT", bound="MatterBaseTest") +P = ParamSpec("P") +R = TypeVar("R") + + +def _async_runner(body: Callable[Concatenate[TestInstanceT, P], Awaitable[R]], test_instance: TestInstanceT, *args: P.args, + **kwargs: P.kwargs) -> R: timeout = getattr(test_instance.matter_test_config, 'timeout', None) or test_instance.default_timeout return test_instance.event_loop.run_until_complete(asyncio.wait_for(body(test_instance, *args, **kwargs), timeout=timeout)) -def async_test_body(body): +def async_test_body(body: Callable[Concatenate[TestInstanceT, P], Awaitable[R]]) -> Callable[Concatenate[TestInstanceT, P], R]: """Decorator required to be applied whenever a `test_*` method is `async def`. Since Mobly doesn't support asyncio directly, and the test methods are called @@ -261,19 +267,19 @@ def async_test_body(body): a asyncio-run synchronous method. This decorator does the wrapping. """ - def async_runner(self: "MatterBaseTest", *args, **kwargs): + def async_runner(self: TestInstanceT, *args: P.args, **kwargs: P.kwargs) -> R: return _async_runner(body, self, *args, **kwargs) return async_runner -async def _get_all_matching_endpoints(test_instance, accept_function: EndpointCheckFunction) -> list[int]: +async def _get_all_matching_endpoints(test_instance: "MatterBaseTest", accept_function: EndpointCheckFunction) -> list[int]: """ Returns a list of endpoints matching the accept condition. """ wildcard = await test_instance.default_controller.Read(test_instance.dut_node_id, [(Clusters.Descriptor), Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.FEATURE_MAP_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)]) return [e for e in wildcard.attributes if accept_function(wildcard, e)] -async def should_run_test_on_endpoint(test_instance, accept_function: EndpointCheckFunction) -> bool: +async def should_run_test_on_endpoint(test_instance: "MatterBaseTest", accept_function: EndpointCheckFunction) -> bool: """ Helper function for the run_if_endpoint_matches decorator. Returns True if test_instance.matter_test_config.endpoint matches the accept function. @@ -287,14 +293,16 @@ async def should_run_test_on_endpoint(test_instance, accept_function: EndpointCh return test_instance.matter_test_config.endpoint in matching -def run_on_singleton_matching_endpoint(accept_function: EndpointCheckFunction): +def run_on_singleton_matching_endpoint(accept_function: EndpointCheckFunction) -> Callable[ + [Callable[Concatenate[TestInstanceT, P], Awaitable[object]]], Callable[Concatenate[TestInstanceT, P], None]]: """ Test decorator for a test that needs to be run on the endpoint that matches the given accept function. This decorator should be used for tests where the endpoint is not known a-priori (dynamic endpoints). Note that currently this test is limited to devices with a SINGLE matching endpoint. """ - def run_on_singleton_matching_endpoint_internal(body): - def matching_runner(self: "MatterBaseTest", *args, **kwargs): + def run_on_singleton_matching_endpoint_internal( + body: Callable[Concatenate[TestInstanceT, P], Awaitable[R]]) -> Callable[Concatenate[TestInstanceT, P], None]: + def matching_runner(self: TestInstanceT, *args: P.args, **kwargs: P.kwargs) -> None: # Import locally to avoid circular dependency from matter.testing.matter_testing import MatterBaseTest assert isinstance(self, MatterBaseTest) @@ -309,22 +317,22 @@ def matching_runner(self: "MatterBaseTest", *args, **kwargs): "Test is not applicable to any endpoint - skipping test") asserts.skip('No endpoint matches test requirements') return + old_endpoint = self.matter_test_config.endpoint try: - old_endpoint = self.matter_test_config.endpoint self.matter_test_config.endpoint = matching[0] LOGGER.info( f'Running test on endpoint {self.matter_test_config.endpoint}') timeout = getattr(self.matter_test_config, 'timeout', None) or self.default_timeout - self.event_loop.run_until_complete(asyncio.wait_for( - body(self, *args, **kwargs), timeout=timeout)) + self.event_loop.run_until_complete(asyncio.wait_for(body(self, *args, **kwargs), timeout=timeout)) finally: self.matter_test_config.endpoint = old_endpoint return matching_runner return run_on_singleton_matching_endpoint_internal -def run_if_endpoint_matches(accept_function: EndpointCheckFunction): +def run_if_endpoint_matches(accept_function: EndpointCheckFunction) -> Callable[ + [Callable[Concatenate[TestInstanceT, P], Awaitable[object]]], Callable[Concatenate[TestInstanceT, P], None]]: """ Test decorator for a test that needs to be run only if the endpoint meets the accept_function criteria. Place this decorator above the test_ method to have the test framework run this test only if the endpoint matches. @@ -350,8 +358,9 @@ def run_if_endpoint_matches(accept_function: EndpointCheckFunction): Tests that use this decorator cannot use a pics_ method for test selection and should not reference any PICS values internally. """ - def run_if_endpoint_matches_internal(body): - def per_endpoint_runner(test_instance, *args, **kwargs): + def run_if_endpoint_matches_internal( + body: Callable[Concatenate[TestInstanceT, P], Awaitable[R]]) -> Callable[Concatenate[TestInstanceT, P], None]: + def per_endpoint_runner(test_instance: TestInstanceT, *args: P.args, **kwargs: P.kwargs) -> None: runner_with_timeout = asyncio.wait_for( should_run_test_on_endpoint(test_instance, accept_function), timeout=60) should_run_test = test_instance.event_loop.run_until_complete( diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py index 740c9474b89c62..961960f6823984 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/matter_testing.py @@ -33,7 +33,9 @@ from dataclasses import asdict, dataclass from datetime import datetime, timedelta, timezone from enum import IntFlag -from typing import Any, Callable, List, Optional, Type, Union +from typing import Any, Callable + +from mobly import records import matter.testing.matchers as matchers @@ -69,8 +71,8 @@ # TODO: Add utilities to keep track of controllers/fabrics # Type aliases for common patterns to improve readability -StepNumber = Union[int, str] # Test step numbers can be integers or strings -OptionalTimeout = Optional[int] # Optional timeout values +StepNumber = int | str # Test step numbers can be integers or strings +OptionalTimeout = int | None # Optional timeout values LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.INFO) @@ -84,7 +86,7 @@ class TestError(Exception): pass -def clear_queue(report_queue: queue.Queue): +def clear_queue(report_queue: queue.Queue) -> None: """Flush all contents of a report queue. Useful to get back to empty point.""" while not report_queue.empty(): try: @@ -93,7 +95,7 @@ def clear_queue(report_queue: queue.Queue): break -def get_first_setup_code(dev_ctrl: ChipDeviceCtrl.ChipDeviceControllerBase, matter_test_config: MatterTestConfig) -> Optional[str]: +def get_first_setup_code(dev_ctrl: ChipDeviceCtrl.ChipDeviceControllerBase, matter_test_config: MatterTestConfig) -> str | None: created_codes = [] for idx, discriminator in enumerate(matter_test_config.discriminators): created_codes.append(dev_ctrl.CreateManualCode(discriminator, matter_test_config.setup_passcodes[idx])) @@ -109,7 +111,7 @@ class AttributeValue: endpoint_id: int attribute: ClusterObjects.ClusterAttributeDescriptor value: Any - timestamp_utc: Optional[datetime] = None + timestamp_utc: datetime | None = None class AttributeMatcher: @@ -120,7 +122,7 @@ class AttributeMatcher: A match is considered as having occurred when the `matches` method returns True for an `AttributeValue` report. """ - def __init__(self, description: str): + def __init__(self, description: str) -> None: self._description: str = description def matches(self, report: AttributeValue) -> bool: @@ -131,14 +133,14 @@ def matches(self, report: AttributeValue) -> bool: return False @property - def description(self): + def description(self) -> str: return self._description @staticmethod def from_callable(description: str, matcher: Callable[[AttributeValue], bool]) -> "AttributeMatcher": """Take a single callable and wrap it into an AttributeMatcher object. Useful to wrap closures.""" class AttributeMatcherFromCallable(AttributeMatcher): - def __init__(self, description, matcher: Callable[[AttributeValue], bool]): + def __init__(self, description: str, matcher: Callable[[AttributeValue], bool]) -> None: super().__init__(description) self._matcher = matcher @@ -159,22 +161,24 @@ class SetupParameters: version: int = 0 @property - def qr_code(self): + def qr_code(self) -> str: return SetupPayload().GenerateQrCode(self.passcode, self.vendor_id, self.product_id, self.discriminator, self.custom_flow, self.capabilities, self.version) @property - def manual_code(self): + def manual_code(self) -> str: return SetupPayload().GenerateManualPairingCode(self.passcode, self.vendor_id, self.product_id, self.discriminator, self.custom_flow, self.capabilities, self.version) class MatterBaseTest(base_test.BaseTestClass): - def __init__(self, *args): + event_loop: asyncio.AbstractEventLoop + + def __init__(self, *args: Any) -> None: super().__init__(*args) # List of accumulated problems across all tests - self.problems = [] + self.problems: list[ProblemNotice] = [] self.is_commissioning = False self.cached_steps: dict[str, list[TestStep]] = {} @@ -193,7 +197,7 @@ def __init__(self, *args): # teardown_ methods should call the super() method at the end # - def setup_class(self): + def setup_class(self) -> None: """Set up the test class before running any tests. Initializes cluster mapping, step tracking, and global test state. @@ -220,7 +224,7 @@ def setup_class(self): # where the read is deferred until the first guard function call that requires global attributes. self.stored_global_wildcard = None - def teardown_class(self): + def teardown_class(self) -> None: """Final teardown after all tests: log all problems and dump device attributes if available. Test authors may overwrite this method in the derived class to perform teardown that is common for all tests This function is called only once per class. To perform teardown after each test, use teardown_test. @@ -258,7 +262,7 @@ def _format_summary_value(self, key: str, value: Any) -> str: return "Please request if needed" return repr(value) - def _log_execution_parameters_summary(self): + def _log_execution_parameters_summary(self) -> None: """Log execution parameters at test end to aid result triage.""" try: meta = asdict(self.matter_test_config) @@ -302,7 +306,7 @@ def _log_execution_parameters_summary(self): LOGGER.info("===== EXECUTION FLAGS SUMMARY END =====") - def _dump_device_attributes_on_failure(self): + def _dump_device_attributes_on_failure(self) -> None: """ Dump device attribute data when problems are found for debugging purposes. @@ -321,7 +325,7 @@ def _dump_device_attributes_on_failure(self): # Don't let data access or serialization errors interfere with the original test failure pass - def log_structured_data(self, start_tag: str, dump_string: str): + def log_structured_data(self, start_tag: str, dump_string: str) -> None: """Log structured data with a clear start and end marker. This function is used to output device attribute dumps and other structured @@ -337,7 +341,7 @@ def log_structured_data(self, start_tag: str, dump_string: str): LOGGER.info(f'{start_tag}{line}') LOGGER.info(f'{start_tag}END ====') - def setup_test(self): + def setup_test(self) -> None: """Set up for each individual test execution. Resets test state, starts timers, and notifies runner hooks. @@ -367,7 +371,7 @@ def setup_test(self): if steps is None: self.step(1) - def on_fail(self, record): + def on_fail(self, record: records.TestResultRecord) -> None: """Handle test failure callback from Mobly framework. This is called by the base framework. @@ -462,7 +466,7 @@ def extract_error_text() -> tuple[str, str]: ******************************************************************* """)) - def on_pass(self, record): + def on_pass(self, record: records.TestResultRecord) -> None: """Handle test success callback from Mobly framework. This is called by the base framework. @@ -494,7 +498,7 @@ def on_pass(self, record): if self.runner_hook and not self.is_commissioning: self.runner_hook.test_stop(exception=None, duration=test_duration) - def on_skip(self, record): + def on_skip(self, record: records.TestResultRecord) -> None: """Handle test skip callback from Mobly framework. This is called by the base framework. @@ -553,7 +557,7 @@ def dut_node_id(self) -> int: return self.matter_test_config.dut_node_ids[0] @property - def first_setup_code(self) -> Optional[str]: + def first_setup_code(self) -> str | None: return get_first_setup_code(self.default_controller, self.matter_test_config) @property @@ -592,11 +596,11 @@ def get_credentials(self, default: str = "") -> str: ''' return self.matter_test_config.wifi_passphrase if self.matter_test_config.wifi_passphrase is not None else default - def get_setup_payload_info(self) -> List[SetupPayloadInfo]: + def get_setup_payload_info(self) -> list[SetupPayloadInfo]: """ Get and builds the payload info provided in the execution. Returns: - List[SetupPayloadInfo]: List of Payload used by the test case + list[SetupPayloadInfo]: List of Payload used by the test case """ return get_setup_payload_info_config(self.matter_test_config) @@ -619,7 +623,7 @@ def get_test_steps(self, test: str) -> list[TestStep]: steps = self.get_defined_test_steps(test) return [TestStep(1, "Run entire test")] if steps is None else steps - def get_defined_test_steps(self, test: str) -> Optional[list[TestStep]]: + def get_defined_test_steps(self, test: str) -> list[TestStep] | None: """Retrieves test steps from a 'steps_*' function, using a cache.""" steps_name = f'steps_{test.removeprefix("test_")}' if test in self.cached_steps: @@ -633,7 +637,7 @@ def get_defined_test_steps(self, test: str) -> Optional[list[TestStep]]: except AttributeError: return None - def get_restart_flag_file(self) -> Optional[str]: + def get_restart_flag_file(self) -> str | None: if self.matter_test_config.restart_flag_file is None: return None return str(self.matter_test_config.restart_flag_file) @@ -650,7 +654,7 @@ def get_test_pics(self, test: str) -> list[str]: pics = self._get_defined_pics(test) return [] if pics is None else pics - def _get_defined_pics(self, test: str) -> Optional[list[str]]: + def _get_defined_pics(self, test: str) -> list[str] | None: """Retrieve PICS list from a 'pics_*' function if it exists. Args: @@ -692,7 +696,7 @@ def get_test_desc(self, test: str) -> str: # These methods are used to mark test progress for the test harness and logs, to help with test # debugging, issue creation and log analysis by the test labs. - def step(self, step: typing.Union[int, str]): + def step(self, step: int | str) -> None: """Execute a test step and manage step progression. Validates step order, prints step information, and notifies runner hooks. @@ -728,7 +732,7 @@ def step(self, step: typing.Union[int, str]): self.current_step_index = self.current_step_index + 1 self.step_skipped = False - def print_step(self, stepnum: typing.Union[int, str], title: str) -> None: + def print_step(self, stepnum: int | str, title: str) -> None: """Print test step information to logs. Args: @@ -737,7 +741,7 @@ def print_step(self, stepnum: typing.Union[int, str], title: str) -> None: """ LOGGER.info(f'***** Test Step {stepnum} : {title}') - def skip_step(self, step): + def skip_step(self, step: int | str) -> None: """Execute and immediately mark a step as skipped. Args: @@ -746,7 +750,7 @@ def skip_step(self, step): self.step(step) self.mark_current_step_skipped() - def mark_current_step_skipped(self): + def mark_current_step_skipped(self) -> None: """Mark the current step as skipped and log the skip.""" try: steps = self.get_test_steps(self.current_test_info.name) @@ -764,7 +768,7 @@ def mark_current_step_skipped(self): LOGGER.info(f'**** Skipping: {num}') self.step_skipped = True - def mark_all_remaining_steps_skipped(self, starting_step_number: typing.Union[int, str]) -> None: + def mark_all_remaining_steps_skipped(self, starting_step_number: int | str) -> None: """Mark all remaining test steps starting with provided starting step starting_step_number gives the first step to be skipped, as defined in the TestStep.test_plan_number starting_step_number must be provided, and is not derived intentionally. @@ -777,7 +781,7 @@ def mark_all_remaining_steps_skipped(self, starting_step_number: typing.Union[in """ self.mark_step_range_skipped(starting_step_number, None) - def mark_step_range_skipped(self, starting_step_number: typing.Union[int, str], ending_step_number: typing.Union[int, str, None]) -> None: + def mark_step_range_skipped(self, starting_step_number: int | str, ending_step_number: int | str | None) -> None: """Mark a range of remaining test steps starting with provided starting step starting_step_number gives the first step to be skipped, as defined in the TestStep.test_plan_number starting_step_number must be provided, and is not derived intentionally. @@ -803,7 +807,7 @@ def mark_step_range_skipped(self, starting_step_number: typing.Union[int, str], asserts.assert_is_not_none(starting_step_idx, "mark_step_ranges_skipped was provided with invalid starting_step_num") starting_index: int = typing.cast(int, starting_step_idx) - ending_step_idx = None + ending_step_idx: int | None = None # If ending_step_number is None, we skip all steps until the end of the test if ending_step_number is not None: for idx, step in enumerate(steps): @@ -837,7 +841,7 @@ def check_pics(self, pics_key: str) -> bool: """ return self.matter_test_config.pics.get(pics_key.strip(), False) - def pics_guard(self, pics_condition: bool): + def pics_guard(self, pics_condition: bool) -> bool: """Checks a condition and if False marks the test step as skipped and returns False, otherwise returns True. For example can be used to check if a test step should be run: @@ -854,14 +858,17 @@ def pics_guard(self, pics_condition: bool): self.mark_current_step_skipped() return pics_condition - async def _populate_wildcard(self): + async def _populate_wildcard(self) -> None: """ Populates self.stored_global_wildcard if not already filled. """ if self.stored_global_wildcard is None: - global_wildcard = asyncio.wait_for(self.default_controller.Read(self.dut_node_id, [(Clusters.Descriptor), Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID), Attribute.AttributePath( - None, None, GlobalAttributeIds.FEATURE_MAP_ID), Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)]), timeout=60) + global_wildcard = asyncio.wait_for(self.default_controller.Read(self.dut_node_id, [ + (Clusters.Descriptor,), + Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID), + Attribute.AttributePath(None, None, GlobalAttributeIds.FEATURE_MAP_ID), + Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID)]), timeout=60) self.stored_global_wildcard = await global_wildcard - async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.ClusterAttributeDescriptor): + async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.ClusterAttributeDescriptor) -> bool: """Similar to pics_guard above, except checks a condition and if False marks the test step as skipped and returns False using attributes against attributes_list, otherwise returns True. For example can be used to check if a test step should be run: @@ -880,7 +887,7 @@ async def attribute_guard(self, endpoint: int, attribute: ClusterObjects.Cluster self.mark_current_step_skipped() return attr_condition - async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterCommand): + async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterCommand) -> bool: """Similar to attribute_guard above, except checks a condition and if False marks the test step as skipped and returns False using command id against AcceptedCmdsList, otherwise returns True. For example can be used to check if a test step should be run: @@ -899,7 +906,7 @@ async def command_guard(self, endpoint: int, command: ClusterObjects.ClusterComm self.mark_current_step_skipped() return cmd_condition - async def feature_guard(self, endpoint: int, cluster: ClusterObjects.ClusterObjectDescriptor, feature_int: IntFlag): + async def feature_guard(self, endpoint: int, cluster: ClusterObjects.ClusterObjectDescriptor, feature_int: IntFlag) -> bool: """Similar to command_guard and attribute_guard above, except checks a condition and if False marks the test step as skipped and returns False using feature id against feature_map, otherwise returns True. For example can be used to check if a test step should be run: @@ -932,8 +939,8 @@ async def commission_devices(self) -> bool: True if commissioning succeeded, False otherwise. """ dev_ctrl: ChipDeviceCtrl.ChipDeviceController = self.default_controller - dut_node_ids: List[int] = self.matter_test_config.dut_node_ids - setup_payloads: List[SetupPayloadInfo] = self.get_setup_payload_info() + dut_node_ids: list[int] = self.matter_test_config.dut_node_ids + setup_payloads: list[SetupPayloadInfo] = self.get_setup_payload_info() commissioning_info: CommissioningInfo = CommissioningInfo( commissionee_ip_address_just_for_testing=self.matter_test_config.commissionee_ip_address_just_for_testing, commissioning_method=self.matter_test_config.commissioning_method, @@ -948,7 +955,8 @@ async def commission_devices(self) -> bool: return await commission_devices(dev_ctrl, dut_node_ids, setup_payloads, commissioning_info) - async def open_commissioning_window(self, dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, timeout: int = 900) -> CustomCommissioningParameters: + async def open_commissioning_window(self, dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None, + node_id: int | None = None, timeout: int = 900) -> CustomCommissioningParameters: """Open a commissioning window on the target device. Args: @@ -976,8 +984,9 @@ async def open_commissioning_window(self, dev_ctrl: Optional[ChipDeviceCtrl.Chip asserts.fail(e.status, 'Failed to open commissioning window') raise # Help mypy understand this never returns - async def read_single_attribute( - self, dev_ctrl: ChipDeviceCtrl.ChipDeviceController, node_id: int, endpoint: int, attribute: Type[ClusterObjects.ClusterAttributeDescriptor], fabricFiltered: bool = True) -> object: + async def read_single_attribute(self, dev_ctrl: ChipDeviceCtrl.ChipDeviceController, node_id: int, endpoint: int, + attribute: type[ClusterObjects.ClusterAttributeDescriptor], + fabricFiltered: bool = True) -> object: """Read a single attribute value from a device. Args: @@ -995,8 +1004,8 @@ async def read_single_attribute( return list(data.values())[0][attribute] async def read_single_attribute_all_endpoints( - self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor], - dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None): + self, cluster: ClusterObjects.Cluster, attribute: type[ClusterObjects.ClusterAttributeDescriptor], + dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None, node_id: int | None = None) -> dict[Any, Any]: """Reads a single attribute of a specified cluster across all endpoints. Returns: @@ -1017,8 +1026,10 @@ async def read_single_attribute_all_endpoints( return attrs async def read_single_attribute_check_success( - self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor], - dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None, fabric_filtered: bool = True, assert_on_error: bool = True, test_name: str = "", payloadCapability: int = ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD) -> object: + self, cluster: type[ClusterObjects.Cluster], attribute: type[ClusterObjects.ClusterAttributeDescriptor], + dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None, node_id: int | None = None, endpoint: int | None = None, + fabric_filtered: bool = True, assert_on_error: bool = True, test_name: str = "", + payloadCapability: int = ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD) -> object: if dev_ctrl is None: dev_ctrl = self.default_controller if node_id is None: @@ -1047,8 +1058,8 @@ async def read_single_attribute_check_success( return attr_ret async def read_single_attribute_expect_error( - self, cluster: ClusterObjects.Cluster, attribute: Type[ClusterObjects.ClusterAttributeDescriptor], - error: Status, dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None, + self, cluster: ClusterObjects.Cluster, attribute: type[ClusterObjects.ClusterAttributeDescriptor], error: Status, + dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None, node_id: int | None = None, endpoint: int | None = None, fabric_filtered: bool = True, assert_on_error: bool = True, test_name: str = "") -> object: if dev_ctrl is None: dev_ctrl = self.default_controller @@ -1072,7 +1083,8 @@ async def read_single_attribute_expect_error( return attr_ret - async def write_single_attribute(self, attribute_value: ClusterObjects.ClusterAttributeDescriptor, endpoint_id: Optional[int] = None, expect_success: bool = True) -> Status: + async def write_single_attribute(self, attribute_value: ClusterObjects.ClusterAttributeDescriptor, + endpoint_id: int | None = None, expect_success: bool = True) -> Status: """Write a single `attribute_value` on a given `endpoint_id` and assert on failure. If `endpoint_id` is None, the default DUT endpoint for the test is selected. @@ -1092,14 +1104,8 @@ async def write_single_attribute(self, attribute_value: ClusterObjects.ClusterAt f"Expected write success for write to attribute {attribute_value} on endpoint {endpoint_id}") return write_result[0].Status - def read_from_app_pipe( - self, - app_pipe_out: Optional[str] = None, - timeout: float = 2.0, - max_bytes: int = 66536, - chunk: int = 4096, - ip_env_var: Optional[str] = None, - ) -> Any: + def read_from_app_pipe(self, app_pipe_out: str | None = None, timeout: float = 2.0, max_bytes: int = 66536, chunk: int = 4096, + ip_env_var: str | None = None) -> Any: """ Read an out-of-band command from a Matter app. @@ -1121,7 +1127,7 @@ def read_from_app_pipe( LOGGER.error("Named pipe %r does NOT exist", app_pipe_out) raise FileNotFoundError("CANNOT FIND %r" % app_pipe_out) - dut_ip: Optional[str] = os.getenv(ip_env_var) if ip_env_var else None + dut_ip = os.getenv(ip_env_var) if ip_env_var else None # If no DUT IP is provided, the Matter app is assumed to be local and the command # is read directly from the named pipe. If a DUT IP is present, the pipe is read @@ -1187,14 +1193,14 @@ def read_from_app_pipe( out_str = out.decode("utf-8").strip() return json.loads(out_str) - def write_to_app_pipe(self, command_dict: dict, app_pipe: Optional[str] = None, ip_env_var: Optional[str] = None): + def write_to_app_pipe(self, command_dict: dict, app_pipe: str | None = None, ip_env_var: str | None = None) -> None: """ Send an out-of-band command to a Matter app. Args: command_dict (dict): dictionary with the command and data. - app_pipe (Optional[str], optional): Name of the cluster pipe file (i.e. /tmp/chip_all_clusters_fifo_55441 or /tmp/chip_rvc_fifo_11111). Raises + app_pipe (str | None, optional): Name of the cluster pipe file (i.e. /tmp/chip_all_clusters_fifo_55441 or /tmp/chip_rvc_fifo_11111). Raises FileNotFoundError if pipe file is not found. If None takes the value from the CI argument --app-pipe, arg --app-pipe has his own file exists check. - ip_env_var: Optional[str]: is an optional argument. Name of the environment variable containing the DUT IP. + ip_env_var: str | None: is an optional argument. Name of the environment variable containing the DUT IP. This method uses the following environment variables: @@ -1224,7 +1230,7 @@ def write_to_app_pipe(self, command_dict: dict, app_pipe: Optional[str] = None, command = json.dumps(command_dict) - dut_ip: Optional[str] = os.getenv(ip_env_var) if ip_env_var else None + dut_ip = os.getenv(ip_env_var) if ip_env_var else None # If no DUT IP is provided, the Matter app is assumed to be local and the command # is read directly from the named pipe. If a DUT IP is present, the pipe is read @@ -1247,9 +1253,8 @@ def write_to_app_pipe(self, command_dict: dict, app_pipe: Optional[str] = None, os.system(cmd) async def send_single_cmd( - self, cmd: Clusters.ClusterObjects.ClusterCommand, - dev_ctrl: Optional[ChipDeviceCtrl.ChipDeviceController] = None, node_id: Optional[int] = None, endpoint: Optional[int] = None, - timedRequestTimeoutMs: OptionalTimeout = None, + self, cmd: Clusters.ClusterObjects.ClusterCommand, dev_ctrl: ChipDeviceCtrl.ChipDeviceController | None = None, + node_id: int | None = None, endpoint: int | None = None, timedRequestTimeoutMs: OptionalTimeout = None, payloadCapability: int = ChipDeviceCtrl.TransportPayloadCapability.MRP_PAYLOAD) -> object: """Send a single command to a Matter device. @@ -1274,7 +1279,7 @@ async def send_single_cmd( return await dev_ctrl.SendCommand(nodeId=node_id, endpoint=endpoint, payload=cmd, timedRequestTimeoutMs=timedRequestTimeoutMs, payloadCapability=payloadCapability) - async def send_test_event_triggers(self, eventTrigger: int, enableKey: Optional[bytes] = None): + async def send_test_event_triggers(self, eventTrigger: int, enableKey: bytes | None = None) -> None: """This helper function sends a test event trigger to the General Diagnostics cluster on endpoint 0 The enableKey can be passed into the function, or omitted which will then @@ -1300,7 +1305,7 @@ async def send_test_event_triggers(self, eventTrigger: int, enableKey: Optional[ asserts.fail( f"Sending TestEventTrigger resulted in Unexpected error. Are they enabled in DUT? Command returned - {e.status}") - async def check_test_event_triggers_enabled(self): + async def check_test_event_triggers_enabled(self) -> None: """This cluster checks that the General Diagnostics cluster TestEventTriggersEnabled attribute is True. It will assert and fail the test if not True.""" full_attr = Clusters.GeneralDiagnostics.Attributes.TestEventTriggersEnabled @@ -1344,7 +1349,7 @@ def _update_legacy_test_event_triggers(self, eventTrigger: int) -> int: # Matter Test API - Utility Helpers (Problem Recording, User Input) # - def record_error(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = ""): + def record_error(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = "") -> None: """Record an error-level problem during test execution. Args: @@ -1355,7 +1360,7 @@ def record_error(self, test_name: str, location: ProblemLocation, problem: str, """ self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.ERROR, problem, spec_location)) - def record_warning(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = ""): + def record_warning(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = "") -> None: """Record a warning-level problem during test execution. Args: @@ -1366,7 +1371,7 @@ def record_warning(self, test_name: str, location: ProblemLocation, problem: str """ self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.WARNING, problem, spec_location)) - def record_note(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = ""): + def record_note(self, test_name: str, location: ProblemLocation, problem: str, spec_location: str = "") -> None: """Record a note-level problem during test execution. Args: @@ -1377,10 +1382,8 @@ def record_note(self, test_name: str, location: ProblemLocation, problem: str, s """ self.problems.append(ProblemNotice(test_name, location, ProblemSeverity.NOTE, problem, spec_location)) - def wait_for_user_input(self, - prompt_msg: str, - prompt_msg_placeholder: str = "Submit anything to continue", - default_value: str = "y") -> Optional[str]: + def wait_for_user_input(self, prompt_msg: str, prompt_msg_placeholder: str = "Submit anything to continue", + default_value: str = "y") -> str | None: """Ask for user input and wait for it. Args: @@ -1532,7 +1535,7 @@ def user_verify_push_av_stream(self, prompt_msg: str) -> bool: error_message='Push AV Stream validation failed' ) - def _expire_sessions_on_all_controllers(self): + def _expire_sessions_on_all_controllers(self) -> None: """Helper method to expire sessions on all active controllers via the fabric admin interface. This method iterates through all certificate authorities and their fabric admins to expire @@ -1550,7 +1553,7 @@ def _expire_sessions_on_all_controllers(self): except ChipStackError as e: # chipstack-ok LOGGER.warning(f"Failed to expire sessions on controller {controller.nodeId}: {e}") - async def request_device_reboot(self): + async def request_device_reboot(self) -> None: """Request a reboot of the Device Under Test (DUT). This method handles device reboots in both CI and development environments (via run_python_test.py test runner script) @@ -1636,7 +1639,8 @@ async def request_device_factory_reset(self, reset_ctrl: bool = False) -> None: LOGGER.error(err) asserts.fail(err) - async def wait_for_restart_flag_file_removal(self, restart_flag_file, restart_flag_text, timeout_sec=30.0): + async def wait_for_restart_flag_file_removal(self, restart_flag_file: str, restart_flag_text: str, + timeout_sec: float = 30.0) -> None: # Wait for the monitor thread to remove the flag file # The monitor deletes the flag file AFTER the restart completes, so this ensures # the app has fully rebooted and is ready before we continue @@ -1647,29 +1651,7 @@ async def wait_for_restart_flag_file_removal(self, restart_flag_file, restart_fl await asyncio.sleep(0.1) -def _async_runner(body, self: MatterBaseTest, *args, **kwargs): - """Runs an async function within the test's event loop with a timeout. - - This helper function takes an awaitable (async function) and executes it - using the test's event loop (`self.event_loop.run_until_complete`). - It applies a timeout based on the test configuration (`self.matter_test_config.timeout`) - or the default timeout (`self.default_timeout`) if not specified. - - Args: - body: The async function (coroutine) to execute. It will be called - with `self` as the first argument, followed by `*args` and `**kwargs`. - self: The instance of the MatterBaseTest class. - *args: Positional arguments to pass to the `body` function. - **kwargs: Keyword arguments to pass to the `body` function. - - Returns: - The result returned by the awaited `body` function. - """ - timeout = self.matter_test_config.timeout if self.matter_test_config.timeout is not None else self.default_timeout - return self.event_loop.run_until_complete(asyncio.wait_for(body(self, *args, **kwargs), timeout=timeout)) - - -EndpointCheckFunction = typing.Callable[[Clusters.Attribute.AsyncReadTransaction.ReadResponse, int], bool] +EndpointCheckFunction = Callable[[Clusters.Attribute.AsyncReadTransaction.ReadResponse, int], bool] def get_cluster_from_attribute(attribute: ClusterObjects.ClusterAttributeDescriptor) -> ClusterObjects.Cluster: @@ -1680,15 +1662,3 @@ def get_cluster_from_attribute(attribute: ClusterObjects.ClusterAttributeDescrip def get_cluster_from_command(command: ClusterObjects.ClusterCommand) -> ClusterObjects.Cluster: """Returns the cluster object for a given command object.""" return ClusterObjects.ALL_CLUSTERS[command.cluster_id] - - -async def _get_all_matching_endpoints(self: MatterBaseTest, accept_function: EndpointCheckFunction) -> list[uint]: - """ Returns a list of endpoints matching the accept condition. """ - wildcard = await self.default_controller.Read(self.dut_node_id, [ - (Clusters.Descriptor,), # single-element tuple needs trailing comma - Attribute.AttributePath(None, None, GlobalAttributeIds.ATTRIBUTE_LIST_ID), - Attribute.AttributePath(None, None, GlobalAttributeIds.FEATURE_MAP_ID), - Attribute.AttributePath(None, None, GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID) - ]) - return [e for e in wildcard.attributes - if accept_function(wildcard, e)] diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py b/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py index 129738000a78e8..b659e57d692595 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/runner.py @@ -32,7 +32,7 @@ from datetime import datetime, timedelta, timezone from itertools import chain from pathlib import Path -from typing import Any, List, Optional, Tuple +from typing import Any, Self, Tuple from unittest.mock import MagicMock from mobly import signals, utils @@ -55,24 +55,27 @@ class TestRunnerHooks: # type: ignore[no-redef] # Conditional fallback, not a t from matter.tracing import TracingContext except ImportError: class TracingContext: # type: ignore[no-redef] # Conditional fallback, not a true redefinition - def __enter__(self): + def __enter__(self) -> Self: return self - def __exit__(self, *args): + def __exit__(self, *args: Any) -> None: pass - def StartFromString(self, destination): + def StartFromString(self, destination: str) -> None: pass from typing import TYPE_CHECKING if TYPE_CHECKING: + from matter.ChipDeviceCtrl import ChipDeviceController + from matter.testing.matter_stack_state import MatterStackState from matter.testing.matter_test_config import MatterTestConfig + from matter.testing.matter_testing import MatterBaseTest LOGGER = logging.getLogger(__name__) -def default_paa_rootstore_from_root(root_path: pathlib.Path) -> Optional[pathlib.Path]: +def default_paa_rootstore_from_root(root_path: pathlib.Path) -> pathlib.Path | None: """Attempt to find a PAA trust store following SDK convention at `root_path` This attempts to find {root_path}/credentials/development/paa-root-certs. @@ -119,7 +122,7 @@ class InternalTestRunnerHooks(TestRunnerHooks): status, including test starts, stops, steps, and failures. """ - def start(self, count: int): + def start(self, count: int) -> None: """ Called when the test runner starts a new test set. @@ -128,7 +131,7 @@ def start(self, count: int): """ LOGGER.info(f'Starting test set, running {count} tests') - def stop(self, duration: int): + def stop(self, duration: float) -> None: """ Called when the test runner finishes a test set. @@ -137,12 +140,7 @@ def stop(self, duration: int): """ LOGGER.info(f'Finished test set, ran for {duration}ms') - def test_start( - self, - filename: str, - name: str, - count: int, - steps: list[str] = []): + def test_start(self, filename: str, name: str, count: int, steps: list[str] | None = None) -> None: """ Called when an individual test starts. @@ -154,7 +152,7 @@ def test_start( """ LOGGER.info(f'Starting test from {filename}: {name} - {count} steps') - def test_stop(self, exception: Exception, duration: int): + def test_stop(self, exception: Exception | None, duration: int) -> None: """ Called when an individual test completes. @@ -164,7 +162,7 @@ def test_stop(self, exception: Exception, duration: int): """ LOGGER.info(f'Finished test in {duration}ms') - def step_skipped(self, name: str, expression: str): + def step_skipped(self, name: str, expression: str) -> None: """ Called when a test step is skipped. @@ -176,7 +174,7 @@ def step_skipped(self, name: str, expression: str): # this in code very easily LOGGER.info(f'\t\t**** Skipping: {name}') - def step_start(self, name: str): + def step_start(self, name: str) -> None: """ Called when a test step starts. @@ -187,7 +185,7 @@ def step_start(self, name: str): # number, but it seems like it might be good to separate these LOGGER.info(f'\t\t***** Test Step {name}') - def step_success(self, logger, logs, duration: int, request): + def step_success(self, logger: Any, logs: list | None, duration: float, request: dict[str, str] | None = None) -> None: """ Called when a test step completes successfully. @@ -197,9 +195,9 @@ def step_success(self, logger, logs, duration: int, request): duration: Step execution duration in milliseconds request: The original test request """ - pass - def step_failure(self, logger, logs, duration: int, request, received): + def step_failure(self, logger: Any, logs: list | None, duration: float, request: dict[str, str] | None = None, + received: dict[str, str] | None = None) -> None: """ Called when a test step fails. @@ -216,16 +214,15 @@ def step_failure(self, logger, logs, duration: int, request, received): if request is not None: LOGGER.info(f'\t\t Expected: {request}') - def step_unknown(self): + def step_unknown(self) -> None: """ This method is called when the result of running a step is unknown. For example during a dry-run. """ - pass def show_prompt(self, msg: str, - placeholder: Optional[str] = None, - default_value: Optional[str] = None) -> None: + placeholder: str | None = None, + default_value: str | None = None) -> None: """ This method is called when the test runner needs to prompt the user for input. @@ -234,9 +231,8 @@ def show_prompt(self, placeholder: Optional placeholder for user input default_value: Optional default value for user input """ - pass - def test_skipped(self, filename: str, name: str): + def test_skipped(self, filename: str, name: str) -> None: """ Called when a test is skipped. @@ -254,7 +250,7 @@ class TestStep: expectation: str = "" is_commissioning: bool = False - def __str__(self): + def __str__(self) -> str: return f'{self.test_plan_number}: {self.description}\tExpected outcome: {self.expectation}' @@ -266,7 +262,7 @@ class TestInfo: pics: list[str] -def generate_mobly_test_config(matter_test_config): +def generate_mobly_test_config(matter_test_config: "MatterTestConfig") -> TestRunConfig: """ Generate a Mobly test configuration from Matter test configuration. @@ -281,12 +277,12 @@ def generate_mobly_test_config(matter_test_config): # freestanding without relying test_run_config.testbed_name = "MatterTest" - log_path = matter_test_config.logs_path + log_path: Path | str = matter_test_config.logs_path log_path = TestingDefaults.LOG_PATH if log_path is None else log_path if ENV_MOBLY_LOGPATH in os.environ: log_path = os.environ[ENV_MOBLY_LOGPATH] - test_run_config.log_path = log_path + test_run_config.log_path = str(log_path) # TODO: For later, configure controllers test_run_config.controller_configs = {} @@ -295,7 +291,7 @@ def generate_mobly_test_config(matter_test_config): return test_run_config -def _find_test_class(): +def _find_test_class() -> "type[MatterBaseTest]": """Finds the test class in a test script. Walk through module members and find the subclass of MatterBaseTest. Only one subclass is allowed in a test script. @@ -304,15 +300,15 @@ def _find_test_class(): Raises: SystemExit: Raised if the number of test classes is not exactly one. """ - from matter.testing.matter_testing import MatterBaseTest - def get_subclasses(cls: Any): + def get_subclasses(cls: type) -> list[type]: subclasses = utils.find_subclasses_in_module([cls], sys.modules['__main__']) return [c for c in subclasses if c.__name__ != cls.__name__] - def has_subclasses(cls: Any): + def has_subclasses(cls: type) -> bool: return get_subclasses(cls) != [] + from matter.testing.matter_testing import MatterBaseTest subclasses_matter_test_base = get_subclasses(MatterBaseTest) leaf_subclasses = [s for s in subclasses_matter_test_base if not has_subclasses(s)] @@ -325,7 +321,7 @@ def has_subclasses(cls: Any): return leaf_subclasses[0] -def default_matter_test_main(): +def default_matter_test_main() -> None: """Execute the test class in a test module. This is the default entry point for running a test script file directly. In this case, only one test class in a test script is allowed. @@ -347,7 +343,7 @@ def default_matter_test_main(): run_tests(test_class, matter_test_config, hooks) -def get_test_info(test_class, matter_test_config) -> list[TestInfo]: +def get_test_info(test_class: "type[MatterBaseTest]", matter_test_config: "MatterTestConfig") -> list[TestInfo]: test_config = generate_mobly_test_config(matter_test_config) base = test_class(test_config) @@ -364,13 +360,10 @@ def get_test_info(test_class, matter_test_config) -> list[TestInfo]: return info -def run_tests_no_exit( - test_class, - matter_test_config, - event_loop: asyncio.AbstractEventLoop, - hooks: TestRunnerHooks, - default_controller=None, - external_stack=None) -> bool: +def run_tests_no_exit(test_class: "type[MatterBaseTest]", matter_test_config: "MatterTestConfig", + event_loop: asyncio.AbstractEventLoop, hooks: InternalTestRunnerHooks | None, + default_controller: ChipDeviceController | None = None, + external_stack: MatterStackState | None = None) -> bool: """ Run Matter tests without exiting the process on failure. @@ -442,7 +435,7 @@ def run_tests_no_exit( # Execute the test class with the config ok = True - def _handler(loop, context): + def _handler(loop: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None: loop.default_exception_handler(context) nonlocal ok # Fail the test run on unhandled exceptions. @@ -486,12 +479,11 @@ def _handler(loop, context): ok = False if hooks: - duration = (datetime.now(timezone.utc) - - runner_start_time) / timedelta(microseconds=1) + duration = (datetime.now(timezone.utc) - runner_start_time) / timedelta(milliseconds=1) hooks.stop(duration=duration) if not external_stack: - async def shutdown(): + async def shutdown() -> None: stack.Shutdown() # Shutdown the stack when all done. Use the async runner to ensure that # during the shutdown callbacks can use tha same async context which was used @@ -505,12 +497,8 @@ async def shutdown(): return ok -def run_tests( - test_class, - matter_test_config, - hooks: TestRunnerHooks, - default_controller=None, - external_stack=None) -> None: +def run_tests(test_class: "type[MatterBaseTest]", matter_test_config: "MatterTestConfig", hooks: InternalTestRunnerHooks | None, + default_controller: ChipDeviceController | None = None, external_stack: MatterStackState | None = None) -> None: """ Run Matter tests and exit the process with status code 1 on failure. @@ -542,7 +530,7 @@ class AsyncMock(MagicMock): This is useful for testing async code without actual async execution. """ - async def __call__(self, *args, **kwargs): + async def __call__(self, *args: Any, **kwargs: Any) -> Any: return super(AsyncMock, self).__call__(*args, **kwargs) @@ -554,8 +542,8 @@ class MockTestRunner(): mocking the controller's Read method and other interactions. """ - def __init__(self, abs_filename: str, classname: str, test: str, endpoint: Optional[int] = None, - pics: Optional[dict[str, bool]] = None, paa_trust_store_path=None): + def __init__(self, abs_filename: str, classname: str, test: str, endpoint: int | None = None, + pics: dict[str, bool] | None = None, paa_trust_store_path: Path | None = None) -> None: from matter.testing.matter_stack_state import MatterStackState from matter.testing.matter_test_config import MatterTestConfig @@ -575,12 +563,12 @@ def __init__(self, abs_filename: str, classname: str, test: str, endpoint: Optio catTags=self.config.controller_cat_tags ) - def set_test(self, abs_filename: str, classname: str, test: str): + def set_test(self, abs_filename: str, classname: str, test: str) -> None: self.test = test self.config.tests = [self.test] + filename_path = Path(abs_filename) try: - filename_path = Path(abs_filename) module = importlib.import_module(filename_path.stem) except ModuleNotFoundError: sys.path.append(str(filename_path.parent.resolve())) @@ -588,7 +576,7 @@ def set_test(self, abs_filename: str, classname: str, test: str): self.test_class = getattr(module, classname) - def set_test_config(self, test_config: Optional['MatterTestConfig'] = None): + def set_test_config(self, test_config: "MatterTestConfig | None" = None) -> None: from matter.testing.matter_test_config import MatterTestConfig if test_config is None: test_config = MatterTestConfig() @@ -599,10 +587,11 @@ def set_test_config(self, test_config: Optional['MatterTestConfig'] = None): if not self.config.dut_node_ids: self.config.dut_node_ids = [1] - def Shutdown(self): + def Shutdown(self) -> None: self.stack.Shutdown() - def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.ReadResponse, hooks=None): + def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.ReadResponse, + hooks: InternalTestRunnerHooks | None = None) -> bool: self.default_controller.Read = AsyncMock(return_value=read_cache) # This doesn't need to do anything since we are overriding the read anyway self.default_controller.FindOrEstablishPASESession = AsyncMock(return_value=None) @@ -614,7 +603,7 @@ def run_test_with_mock_read(self, read_cache: Attribute.AsyncReadTransaction.Rea # Argument parsing helper functions -def populate_commissioning_args(args: argparse.Namespace, config) -> bool: +def populate_commissioning_args(args: argparse.Namespace, config: "MatterTestConfig") -> bool: config.root_of_trust_index = args.root_index # Follow root of trust index if ID not provided to have same behavior as legacy # chip-tool that fabricID == commissioner_name == root of trust index @@ -738,7 +727,7 @@ def populate_commissioning_args(args: argparse.Namespace, config) -> bool: return True -def convert_args_to_matter_config(args: argparse.Namespace): +def convert_args_to_matter_config(args: argparse.Namespace) -> "MatterTestConfig": # Lazy import to avoid circular dependency from matter.testing.matter_test_config import MatterTestConfig @@ -938,7 +927,7 @@ def root_index(s: str) -> int: return root_index -def parse_matter_test_args(argv: Optional[List[str]] = None): +def parse_matter_test_args(argv: list[str] | None = None) -> "MatterTestConfig": parser = argparse.ArgumentParser(description='Matter standalone Python test') basic_group = parser.add_argument_group(title="Basic arguments", description="Overall test execution arguments") diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py index 1532b877b0c602..b49f9c3eb9350d 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/spec_parsing.py @@ -28,7 +28,7 @@ from dataclasses import dataclass, field from enum import Enum, StrEnum, auto from importlib.resources.abc import Traversable -from typing import Optional, Union +from typing import TypeAlias, Union import matter.clusters as Clusters import matter.testing.conformance as conformance_support @@ -59,7 +59,7 @@ def to_access_code(privilege: int) -> str: return _PRIVILEGE_STR.get(privilege, "") -def get_access_privilege_or_unknown(access_value: Optional[int]) -> int: +def get_access_privilege_or_unknown(access_value: int | None) -> int: """ Returns the given access_value if not None, otherwise returns the default unknown privilege. """ @@ -73,7 +73,7 @@ class SpecParsingException(Exception): # passing in feature map, attribute list, command list -ConformanceCallable = conformance_support.Conformance +ConformanceCallable: TypeAlias = conformance_support.Conformance class DataTypeEnum(StrEnum): @@ -90,11 +90,11 @@ class XmlDataTypeComponent: name: str conformance: ConformanceCallable # Additional datatype component fields from cluster XML's - summary: Optional[str] = None # For descriptions/documentation - type_info: Optional[str] = None # Data type for struct fields + summary: str | None = None # For descriptions/documentation + type_info: str | None = None # Data type for struct fields is_optional: bool = False # Whether field is optional is_nullable: bool = False # Whether field can be null - constraints: Optional[dict] = None # For min/max values, lists, etc. + constraints: dict | None = None # For min/max values, lists, etc. @dataclass @@ -107,7 +107,7 @@ class XmlDataType: name: str components: dict[uint, XmlDataTypeComponent] # if this is None, this is a global struct - cluster_ids: Optional[list[uint]] + cluster_ids: list[uint] | None @dataclass @@ -116,7 +116,7 @@ class XmlFeature: name: str conformance: ConformanceCallable - def __str__(self): + def __str__(self) -> str: return f'{self.code}: {self.name} conformance {str(self.conformance)}' @@ -129,14 +129,14 @@ class XmlAttribute: write_access: int write_optional: bool - def access_string(self): + def access_string(self) -> str: read_marker = "R" if self.read_access is not ACCESS_CONTROL_PRIVILEGE_ENUM.kUnknownEnumValue else "" write_marker = "W" if self.write_access is not ACCESS_CONTROL_PRIVILEGE_ENUM.kUnknownEnumValue else "" read_access_marker = f'{to_access_code(self.read_access)}' write_access_marker = f'{to_access_code(self.write_access)}' return f'{read_marker}{write_marker} {read_access_marker}{write_access_marker}' - def __str__(self): + def __str__(self) -> str: return f'{self.name}: datatype: {self.datatype} conformance: {str(self.conformance)}, access = {self.access_string()}' @@ -147,7 +147,7 @@ class XmlCommand: conformance: ConformanceCallable privilege: int - def __str__(self): + def __str__(self) -> str: return f'{self.name} id:0x{self.id:02X} {self.id} conformance: {str(self.conformance)} privilege: {str(self.privilege)}' @@ -161,7 +161,7 @@ class XmlEvent: class XmlCluster: name: str revision: int - derived: Optional[str] + derived: str | None feature_map: dict[str, uint] attribute_map: dict[str, uint] command_map: dict[str, uint] @@ -196,7 +196,7 @@ class XmlDeviceTypeClusterRequirements: attribute_overrides: dict[uint, ConformanceCallable] = field(default_factory=dict) command_overrides: dict[uint, ConformanceCallable] = field(default_factory=dict) - def str_overrides(self): + def str_overrides(self) -> str: ret = "" if self.feature_overrides: overrides = "" @@ -217,7 +217,7 @@ def str_overrides(self): return ret - def __str__(self): + def __str__(self) -> str: return f'{self.name} {self.side}: {str(self.conformance)} {self.str_overrides()}' @@ -239,7 +239,7 @@ class XmlTag: """Represents a tag within a namespace""" id: int = 0 name: str = "" - description: Optional[str] = None + description: str | None = None def __str__(self) -> str: desc = f" - {self.description}" if self.description else "" @@ -256,10 +256,10 @@ class XmlDeviceType: classification_class: str classification_scope: str revision_desc: dict[int, str] - superset_of_device_type_name: Optional[str] = None + superset_of_device_type_name: str | None = None superset_of_device_type_id: int = 0 - def __str__(self): + def __str__(self) -> str: msg = f'{self.name} - Revision {self.revision}, Class {self.classification_class}, Scope {self.classification_scope}\n' if self.superset_of_device_type_name: msg += f'superset of {self.superset_of_device_type_name} ({self.superset_of_device_type_id})' @@ -300,12 +300,12 @@ class CommandType(Enum): # fuzzy match to name because some of the old specs weren't careful here -def _fuzzy_name(to_fuzz: str): +def _fuzzy_name(to_fuzz: str) -> str: to_fuzz = re.sub(r"\(.*?\)|\[.*?\]", "", to_fuzz) return to_fuzz.lower().strip().replace(' ', '').replace('/', '') -def get_location_from_element(element: ElementTree.Element, cluster_id: Optional[int]): +def get_location_from_element(element: ElementTree.Element, cluster_id: int | None) -> ClusterPathLocation | DeviceTypePathLocation: if cluster_id is None: cluster_id = 0 cluster_location = ClusterPathLocation(endpoint_id=0, cluster_id=cluster_id) @@ -328,7 +328,7 @@ def get_location_from_element(element: ElementTree.Element, cluster_id: Optional return cluster_location -def get_conformance(element: ElementTree.Element, cluster_id: Optional[uint]) -> tuple[ElementTree.Element, typing.Optional[ProblemNotice]]: +def get_conformance(element: ElementTree.Element, cluster_id: uint | None) -> tuple[ElementTree.Element, ProblemNotice | None]: for sub in element: if sub.tag in TOP_LEVEL_CONFORMANCE_TAGS: return sub, None @@ -339,7 +339,7 @@ def get_conformance(element: ElementTree.Element, cluster_id: Optional[uint]) -> # Tuple of the root element, the conformance xml element within the root and the optional access element within the root -XmlElementDescriptor = tuple[ElementTree.Element, ElementTree.Element, Optional[ElementTree.Element]] +XmlElementDescriptor = tuple[ElementTree.Element, ElementTree.Element, ElementTree.Element | None] def parse_revision_history(top_level: ElementTree.Element) -> tuple[dict[int, str], list[ProblemNotice]]: @@ -360,7 +360,7 @@ def parse_revision_history(top_level: ElementTree.Element) -> tuple[dict[int, st class ClusterParser: # Cluster ID is optional to support base clusters that have no ID of their own. - def __init__(self, cluster: ElementTree.Element, cluster_id: Optional[uint], name: str): + def __init__(self, cluster: ElementTree.Element, cluster_id: uint | None, name: str): self._problems: list[ProblemNotice] = [] self._cluster = cluster self._cluster_id = cluster_id @@ -381,7 +381,7 @@ def __init__(self, cluster: ElementTree.Element, cluster_id: Optional[uint], nam if id.attrib['name'] == name and list(id.iter('provisionalConform')): self._is_provisional = True - self._pics: Optional[str] = None + self._pics: str | None = None try: classification = next(cluster.iter('classification')) self._pics = classification.attrib['picsCode'] @@ -407,7 +407,7 @@ def get_conformance(self, element: ElementTree.Element) -> ElementTree.Element: self._problems.append(problem) return element - def get_access(self, element: ElementTree.Element) -> Optional[ElementTree.Element]: + def get_access(self, element: ElementTree.Element) -> ElementTree.Element | None: for sub in element: if sub.tag == 'access': return sub @@ -463,7 +463,7 @@ def create_command_map(self) -> dict[str, uint]: commands[element.attrib['name']] = uint(int(element.attrib['id'], 0)) return commands - def parse_conformance(self, conformance_xml: ElementTree.Element) -> Optional[ConformanceCallable]: + def parse_conformance(self, conformance_xml: ElementTree.Element) -> ConformanceCallable | None: try: return parse_callable_from_xml(conformance_xml, self.params) except ConformanceException as ex: @@ -473,12 +473,13 @@ def parse_conformance(self, conformance_xml: ElementTree.Element) -> Optional[Co severity=ProblemSeverity.WARNING, problem=str(ex))) return None - def parse_write_optional(self, element_xml: ElementTree.Element, access_xml: Optional[ElementTree.Element]) -> bool: + def parse_write_optional(self, element_xml: ElementTree.Element, access_xml: ElementTree.Element | None) -> bool: if access_xml is None: return False return access_xml.attrib['write'] == 'optional' - def parse_access(self, element_xml: ElementTree.Element, access_xml: Optional[ElementTree.Element], conformance: ConformanceCallable) -> tuple[Optional[int], Optional[int], Optional[int]]: + def parse_access(self, element_xml: ElementTree.Element, access_xml: ElementTree.Element | None, + conformance: ConformanceCallable) -> tuple[int | None, int | None, int | None]: ''' Returns a tuple of access types for read / write / invoke''' def str_to_access_type(privilege_str: str) -> int: if privilege_str == 'view': @@ -627,7 +628,7 @@ def _parse_field_constraints(self, xml_field: ElementTree.Element) -> dict | Non constraints['maxCountAttribute'] = attr_element.attrib['name'] # Return None instead of {} to clearly distinguish "no constraints found" from "empty constraints" - # This matches the Optional[dict] type in XmlDataTypeComponent.constraints + # This matches the dict | None type in XmlDataTypeComponent.constraints return constraints if constraints else None def _parse_field_conformance(self, xml_field: ElementTree.Element) -> ConformanceCallable: @@ -903,7 +904,7 @@ def add_cluster_data_from_xml(xml: ElementTree.Element, clusters: dict[uint, Xml for id in ids: name = id.get('name') cluster_id_str = id.get('id') - cluster_id: Optional[uint] = None + cluster_id: uint | None = None if cluster_id_str: cluster_id = uint(int(cluster_id_str, 0)) @@ -928,7 +929,7 @@ def add_cluster_data_from_xml(xml: ElementTree.Element, clusters: dict[uint, Xml pure_base_clusters[name] = new -def check_clusters_for_unknown_commands(clusters: dict[uint, XmlCluster], problems: list[ProblemNotice]): +def check_clusters_for_unknown_commands(clusters: dict[uint, XmlCluster], problems: list[ProblemNotice]) -> None: for id, cluster in clusters.items(): for cmd in cluster.unknown_commands: problems.append(ProblemNotice(test_name="Spec XML parsing", location=CommandPathLocation( @@ -946,7 +947,7 @@ class PrebuiltDataModelDirectory(Enum): k1_6 = auto() @property - def dirname(self): + def dirname(self) -> str: if self == PrebuiltDataModelDirectory.k1_2: return "1.2" if self == PrebuiltDataModelDirectory.k1_3: @@ -973,7 +974,7 @@ class DataModelLevel(Enum): kNamespace = auto() @property - def dirname(self): + def dirname(self) -> str: if self == DataModelLevel.kCluster: return "clusters" if self == DataModelLevel.kDeviceType: @@ -1054,7 +1055,7 @@ def build_xml_clusters(data_model_directory: Union[PrebuiltDataModelDirectory, T # Actions cluster - all commands - these need to be listed in the ActionsList attribute to be supported. # We do not currently have a test for this. Please see https://github.com/CHIP-Specifications/chip-test-plans/issues/3646. - def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocation]): + def remove_problem(location: typing.Union[CommandPathLocation, FeaturePathLocation]) -> None: nonlocal problems problems = [p for p in problems if p.location != location] @@ -1411,13 +1412,13 @@ def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: clusters = [] for c in clusters: try: - try: - cid = uint(int(c.attrib['id'], 0)) - except ValueError: - location = DeviceTypePathLocation(device_type_id=id) - problems.append(ProblemNotice("Parse Device Type XML", location=location, - severity=ProblemSeverity.WARNING, problem=f"Unknown cluster id {c.attrib['id']}")) - continue + cid = uint(int(c.attrib['id'], 0)) + except ValueError: + location = DeviceTypePathLocation(device_type_id=id) + problems.append(ProblemNotice("Parse Device Type XML", location=location, + severity=ProblemSeverity.WARNING, problem=f"Unknown cluster id {c.attrib['id']}")) + continue + try: # Workaround for 1.3 device types with zigbee clusters and old scenes # This is OK because there are other tests that ensure that unknown clusters do not appear on the device if cid not in cluster_definition_xml: @@ -1437,7 +1438,7 @@ def parse_single_device_type(root: ElementTree.Element, cluster_definition_xml: cluster_name = CLUSTER_NAME_FIXES[cid] cluster = XmlDeviceTypeClusterRequirements(name=cluster_name, side=side, conformance=conformance) - def append_overrides(override_element_type: str): + def append_overrides(override_element_type: str) -> None: if override_element_type == 'feature': # The device types use feature name rather than feature code. So we need to build a new map. name_to_id_map = {f.name: id for id, f in cluster_definition_xml[cid].features.items()} @@ -1519,7 +1520,8 @@ def append_overrides(override_element_type: str): return device_types, problems -def build_xml_device_types(data_model_directory: typing.Union[PrebuiltDataModelDirectory, Traversable], cluster_definition_xml: Optional[dict[uint, XmlCluster]] = None) -> tuple[dict[int, XmlDeviceType], list[ProblemNotice]]: +def build_xml_device_types(data_model_directory: typing.Union[PrebuiltDataModelDirectory, Traversable], + cluster_definition_xml: dict[uint, XmlCluster] | None = None) -> tuple[dict[int, XmlDeviceType], list[ProblemNotice]]: top = get_data_model_directory(data_model_directory, DataModelLevel.kDeviceType) device_types: dict[int, XmlDeviceType] = {} problems: list[ProblemNotice] = [] @@ -1555,7 +1557,7 @@ def build_xml_device_types(data_model_directory: typing.Union[PrebuiltDataModelD # Fix up supersets for id, d in device_types.items(): - def standardize_name(name: str): + def standardize_name(name: str) -> str: return name.replace(' ', '').replace('/', '').lower() if d.superset_of_device_type_name is None: continue diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py b/src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py index 03e8c5c631582a..f62109d41222a0 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/taglist_and_topology_test.py @@ -146,7 +146,7 @@ def create_device_type_list_for_root(direct_children: Set[int], endpoint_dict: D return device_types -def cmp_tag_list(a: Clusters.Globals.Structs.SemanticTagStruct, b: Clusters.Globals.Structs.SemanticTagStruct): +def cmp_tag_list(a: Clusters.Globals.Structs.SemanticTagStruct, b: Clusters.Globals.Structs.SemanticTagStruct) -> int: if type(a.mfgCode) != type(b.mfgCode): return -1 if type(a.mfgCode) is Nullable else 1 if a.mfgCode != b.mfgCode: diff --git a/src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py b/src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py index fc9196234b0e7c..5277abd0380038 100644 --- a/src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py +++ b/src/python_testing/matter_testing_infrastructure/matter/testing/tasks.py @@ -21,15 +21,12 @@ import threading from dataclasses import dataclass, replace from enum import StrEnum -from typing import BinaryIO, Callable, Optional, Union +from typing import BinaryIO, Callable, Pattern, Self LOGGER = logging.getLogger(__name__) -def forward_f(f_in: BinaryIO, - f_out: BinaryIO, - cb: Optional[Callable[[bytes, bool], bytes]] = None, - is_stderr: bool = False): +def forward_f(f_in: BinaryIO, f_out: BinaryIO, cb: Callable[[bytes, bool], bytes] | None = None, is_stderr: bool = False) -> None: """Forward f_in to f_out. This function can optionally post-process received lines using a callback @@ -57,13 +54,13 @@ class SubprocessInfo: wrapper: tuple[str, ...] = () args: tuple[str, ...] = () - def __post_init__(self): + def __post_init__(self) -> None: self.path = pathlib.Path(self.path) - def with_args(self, *args: str): + def with_args(self, *args: str) -> Self: return replace(self, args=self.args + tuple(args)) - def wrap_with(self, *args: str): + def wrap_with(self, *args: str) -> Self: return replace(self, wrapper=tuple(args) + self.wrapper) def to_cmd(self) -> list[str]: @@ -73,10 +70,8 @@ def to_cmd(self) -> list[str]: class Subprocess(threading.Thread): """Run a subprocess in a thread.""" - def __init__(self, program: str, *args, - output_cb: Optional[Callable[[bytes, bool], bytes]] = None, - f_stdout: BinaryIO = sys.stdout.buffer, - f_stderr: BinaryIO = sys.stderr.buffer): + def __init__(self, program: str, *args: str, output_cb: Callable[[bytes, bool], bytes] | None = None, + f_stdout: BinaryIO = sys.stdout.buffer, f_stderr: BinaryIO = sys.stderr.buffer) -> None: """Initialize the subprocess. Args: @@ -96,31 +91,31 @@ def __init__(self, program: str, *args, self.output_cb = output_cb self.f_stdout = f_stdout self.f_stderr = f_stderr - self.output_match: Optional[re.Pattern] = None - self.returncode = None + self.output_match: Pattern | None = None + self.returncode: int | None = None + self.p: subprocess.Popen[bytes] | None = None - def set_output_match(self, pattern: Union[str, re.Pattern]): + def set_output_match(self, pattern: str | Pattern) -> None: if isinstance(pattern, str): self.output_match = re.compile(re.escape(pattern.encode())) else: self.output_match = pattern - def _check_output(self, line: bytes, is_stderr: bool): + def _check_output(self, line: bytes, is_stderr: bool) -> bytes: if self.output_match is not None and self.output_match.search(line): self.event.set() if self.output_cb is not None: line = self.output_cb(line, is_stderr) return line - def run(self): + def run(self) -> None: """Thread entry point.""" command = [self.program] + list(self.args) LOGGER.info("RUN: %s", shlex.join(command)) - self.p = None - forwarding_stdout_thread = None - forwarding_stderr_thread = None + forwarding_stdout_thread: threading.Thread | None = None + forwarding_stderr_thread: threading.Thread | None = None try: self.p = subprocess.Popen(command, stdin=subprocess.PIPE, @@ -160,9 +155,7 @@ def run(self): if forwarding_stderr_thread is not None: forwarding_stderr_thread.join() - def start(self, - expected_output: Optional[Union[str, re.Pattern]] = None, - timeout: Optional[float] = None): + def start(self, expected_output: str | Pattern | None = None, timeout: float | None = None) -> None: """Start a subprocess and optionally wait for a specific output.""" if expected_output is not None: @@ -174,37 +167,40 @@ def start(self, self.event_started.wait() if expected_output is not None: - if self.event.wait(timeout) is False: + if not self.event.wait(timeout): # Terminate the process, so the Python interpreter will not # hang on the join call in our thread entry point in case of # Python process termination (not-caught exception). - self.p.terminate() + if self.p is not None: + self.p.terminate() raise TimeoutError("Expected output '%r' not found within %s seconds" % (expected_output, timeout)) self.expected_output = None - def send(self, message: str, end: str = "\n", - expected_output: Optional[Union[str, re.Pattern]] = None, - timeout: float = 300): + def send(self, message: str, end: str = "\n", expected_output: str | Pattern | None = None, timeout: float = 300) -> None: """Send a message to a process and optionally wait for a response.""" if expected_output is not None: self.set_output_match(expected_output) self.event.clear() + if self.p is None or self.p.stdin is None: + raise RuntimeError("Process has not been initialized properly") + self.p.stdin.write((message + end).encode()) self.p.stdin.flush() if expected_output is not None: - if self.event.wait(timeout) is False: + if not self.event.wait(timeout): raise TimeoutError("Expected output not found") self.expected_output = None - def terminate(self): + def terminate(self) -> None: """Terminate the subprocess and wait for it to finish.""" - self.p.terminate() + if self.p is not None: + self.p.terminate() self.join() - def wait(self, timeout: Optional[float] = None) -> Optional[int]: + def wait(self, timeout: float | None = None) -> int | None: """Wait for the subprocess to finish.""" self.join(timeout) return self.returncode diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/__init__.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/__init__.pyi deleted file mode 100644 index 7b5e914c55cafe..00000000000000 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/__init__.pyi +++ /dev/null @@ -1,2 +0,0 @@ -# This file is a stub for the matter.testing package - diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi deleted file mode 100644 index 0b0f3b03ed1bb7..00000000000000 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.pyi +++ /dev/null @@ -1,50 +0,0 @@ -# src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/apps.py - -from dataclasses import dataclass -from sys import stderr, stdout -from typing import Any, BinaryIO, List, Optional, Union - -from matter.testing.tasks import Subprocess - - -@dataclass -class OtaImagePath: - path: str - @property - def ota_args(self) -> List[str]: ... - - -@dataclass -class ImageListPath: - path: str - @property - def ota_args(self) -> List[str]: ... - - -class AppServerSubprocess(Subprocess): - PREFIX: bytes - log_file = "" - err_log_file = "" - def __init__(self, app: str, storage_dir: str, discriminator: int, - passcode: int, port: int = 5540, extra_args: List[str] = ...) -> None: ... - - -class IcdAppServerSubprocess(AppServerSubprocess): - def __init__(self, *args: Any, **kwargs: Any) -> None: ... - def pause(self, check_state: bool = True) -> None: ... - def resume(self, check_state: bool = True) -> None: ... - def terminate(self) -> None: ... - - -class OTAProviderSubprocess(AppServerSubprocess): - DEFAULT_ADMIN_NODE_ID: int - PREFIX: bytes - - def __init__(self, app: str, storage_dir: str, discriminator: int, - passcode: int, ota_source: Union[OtaImagePath, ImageListPath], - port: int = 5541, extra_args: list[str] = [], kvs_path: Optional[str] = None, - log_file: Union[str, BinaryIO] = stdout.buffer, err_log_file: Union[str, BinaryIO] = stderr.buffer): ... - - def kill(self) -> None: ... - - def get_pid(self) -> int: ... diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/decorators.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/decorators.pyi deleted file mode 100644 index 9377b393f86475..00000000000000 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/decorators.pyi +++ /dev/null @@ -1,48 +0,0 @@ -# src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/decorators.pyi - -from enum import IntFlag -from typing import TYPE_CHECKING, Callable - -# Assume types from matter.clusters can be imported. -from matter.clusters import Attribute -from matter.clusters import ClusterObjects as ClusterObjects - -# Type alias matching the one in decorators.py -EndpointCheckFunction = Callable[[Attribute.AsyncReadTransaction.ReadResponse, int], bool] - -# --- Public Factory Functions --- - - -def has_cluster(cluster: ClusterObjects.ClusterObjectDescriptor) -> EndpointCheckFunction: ... - - -def has_attribute(attribute: ClusterObjects.ClusterAttributeDescriptor) -> EndpointCheckFunction: ... - - -def has_command(command: ClusterObjects.ClusterCommand) -> EndpointCheckFunction: ... - -# The 'cluster' parameter is the descriptor (e.g., Clusters.OnOff) - - -def has_feature(cluster: ClusterObjects.ClusterObjectDescriptor, feature: IntFlag) -> EndpointCheckFunction: ... - - -# --- Other Public Decorators/Functions --- - -# Forward reference MatterBaseTest using string to avoid circular import issues if necessary -if TYPE_CHECKING: - pass - - -def async_test_body(body: Callable) -> Callable: ... - - -def run_if_endpoint_matches(accept_function: EndpointCheckFunction) -> Callable: ... - - -def run_on_singleton_matching_endpoint(accept_function: EndpointCheckFunction) -> Callable: ... - -# NOTE: We don't define the internal functions (_has_cluster, _has_feature, etc.) -# in the stub file, as mypy primarily checks the external interface based on the stub. -# Type errors *within* the actual decorators.py implementation might still occur -# if the internal logic clashes with types inferred from the outside context provided by this stub. diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/pics.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/pics.pyi deleted file mode 100644 index 90c61a82645d40..00000000000000 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/pics.pyi +++ /dev/null @@ -1,13 +0,0 @@ -# src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/pics.py - -import typing - -def attribute_pics_str(pics_base: str, id: int) -> str: ... -def accepted_cmd_pics_str(pics_base: str, id: int) -> str: ... -def generated_cmd_pics_str(pics_base: str, id: int) -> str: ... -def feature_pics_str(pics_base: str, bit: int) -> str: ... -def server_pics_str(pics_base: str) -> str: ... -def client_pics_str(pics_base: str) -> str: ... -def parse_pics(lines: typing.List[str]) -> dict[str, bool]: ... -def parse_pics_xml(contents: str) -> dict[str, bool]: ... -def read_pics_from_file(path: str) -> dict[str, bool]: ... diff --git a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/tasks.pyi b/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/tasks.pyi deleted file mode 100644 index d8c33ccf29a99e..00000000000000 --- a/src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/tasks.pyi +++ /dev/null @@ -1,45 +0,0 @@ -# src/python_testing/matter_testing_infrastructure/matter/typings/matter/testing/tasks.py - -import threading -from typing import Any, BinaryIO, Callable, Optional, Pattern, Union - -def forward_f(f_in: BinaryIO, f_out: BinaryIO, - cb: Optional[Callable[[bytes, bool], bytes]] = ..., - is_stderr: bool = ...) -> None: ... - - -class Subprocess(threading.Thread): - program: str - args: tuple[str, ...] - output_cb: Optional[Callable[[bytes, bool], bytes]] - f_stdout: BinaryIO - f_stderr: BinaryIO - output_match: Optional[Pattern[bytes]] - returncode: Optional[int] - p: Any - event: threading.Event - event_started: threading.Event - expected_output: Optional[Union[str, Pattern[bytes]]] - - def __init__(self, program: str, *args: str, - output_cb: Optional[Callable[[bytes, bool], bytes]] = ..., - f_stdout: BinaryIO = ..., - f_stderr: BinaryIO = ...) -> None: ... - - def _set_output_match(self, pattern: Union[str, Pattern[bytes]]) -> None: ... - - def _check_output(self, line: bytes, is_stderr: bool) -> bytes: ... - - def run(self) -> None: ... - - def start(self, - expected_output: Optional[Union[str, Pattern[bytes]]] = ..., - timeout: Optional[float] = ...) -> None: ... - - def send(self, message: str, end: str = ..., - expected_output: Optional[Union[str, Pattern[bytes]]] = ..., - timeout: Optional[float] = ...) -> None: ... - - def terminate(self) -> None: ... - - def wait(self, timeout: Optional[float] = ...) -> Optional[int]: ...