matter-python-client 1.4.1a0.dev20261008__tar.gz → 1.4.1a0.dev20261009__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/PKG-INFO +1 -1
  2. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/PKG-INFO +1 -1
  3. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/SOURCES.txt +2 -0
  4. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/client.py +9 -1
  5. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/models/node.py +245 -33
  6. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/pyproject.toml +1 -1
  7. matter_python_client-1.4.1a0.dev20261009/tests/test_bridge_node_fixture.py +258 -0
  8. matter_python_client-1.4.1a0.dev20261009/tests/test_bridge_topology.py +1307 -0
  9. matter_python_client-1.4.1a0.dev20261009/tests/test_event_dispatch.py +333 -0
  10. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_integration.py +219 -0
  11. matter_python_client-1.4.1a0.dev20261008/tests/test_event_dispatch.py +0 -35
  12. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/README.md +0 -0
  13. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/ChipUtility.py +0 -0
  14. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/__init__.py +0 -0
  15. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/ClusterObjects.py +0 -0
  16. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/Objects.py +0 -0
  17. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/Types.py +0 -0
  18. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/__init__.py +0 -0
  19. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AccessControl.py +0 -0
  20. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AccountLogin.py +0 -0
  21. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Actions.py +0 -0
  22. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ActivatedCarbonFilterMonitoring.py +0 -0
  23. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AdministratorCommissioning.py +0 -0
  24. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AirQuality.py +0 -0
  25. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AmbientContextSensing.py +0 -0
  26. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ApplicationBasic.py +0 -0
  27. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ApplicationLauncher.py +0 -0
  28. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AqaraAmbientSensingConfigurationCluster.py +0 -0
  29. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AqaraOccupantLocationCluster.py +0 -0
  30. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AqaraRadarSensingUnionCluster.py +0 -0
  31. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/AudioOutput.py +0 -0
  32. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/BasicInformation.py +0 -0
  33. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Binding.py +0 -0
  34. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/BooleanState.py +0 -0
  35. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/BooleanStateConfiguration.py +0 -0
  36. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/BridgedDeviceBasicInformation.py +0 -0
  37. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CameraAvSettingsUserLevelManagement.py +0 -0
  38. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CameraAvStreamManagement.py +0 -0
  39. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CarbonDioxideConcentrationMeasurement.py +0 -0
  40. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CarbonMonoxideConcentrationMeasurement.py +0 -0
  41. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Channel.py +0 -0
  42. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Chime.py +0 -0
  43. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ClosureControl.py +0 -0
  44. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ClosureDimension.py +0 -0
  45. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ColorControl.py +0 -0
  46. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CommissionerControl.py +0 -0
  47. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CommodityMetering.py +0 -0
  48. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CommodityPrice.py +0 -0
  49. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/CommodityTariff.py +0 -0
  50. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ContentAppObserver.py +0 -0
  51. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ContentControl.py +0 -0
  52. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ContentLauncher.py +0 -0
  53. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Descriptor.py +0 -0
  54. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DeviceEnergyManagement.py +0 -0
  55. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DeviceEnergyManagementMode.py +0 -0
  56. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DiagnosticLogs.py +0 -0
  57. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DishwasherAlarm.py +0 -0
  58. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DishwasherMode.py +0 -0
  59. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DoorLock.py +0 -0
  60. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/DraftElectricalMeasurementCluster.py +0 -0
  61. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EcosystemInformation.py +0 -0
  62. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ElectricalEnergyMeasurement.py +0 -0
  63. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ElectricalGridConditions.py +0 -0
  64. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ElectricalPowerMeasurement.py +0 -0
  65. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EnergyEvse.py +0 -0
  66. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EnergyEvseMode.py +0 -0
  67. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EnergyPreference.py +0 -0
  68. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EthernetNetworkDiagnostics.py +0 -0
  69. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/EveCluster.py +0 -0
  70. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/FanControl.py +0 -0
  71. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/FixedLabel.py +0 -0
  72. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/FlowMeasurement.py +0 -0
  73. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/FormaldehydeConcentrationMeasurement.py +0 -0
  74. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/GeneralCommissioning.py +0 -0
  75. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/GeneralDiagnostics.py +0 -0
  76. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Globals.py +0 -0
  77. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/GroupKeyManagement.py +0 -0
  78. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Groupcast.py +0 -0
  79. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Groups.py +0 -0
  80. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/HeimanCluster.py +0 -0
  81. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/HepaFilterMonitoring.py +0 -0
  82. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/IcdManagement.py +0 -0
  83. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Identify.py +0 -0
  84. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/IkeaValveCalibrationCluster.py +0 -0
  85. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/IlluminanceMeasurement.py +0 -0
  86. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/InovelliCluster.py +0 -0
  87. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/JointFabricAdministrator.py +0 -0
  88. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/JointFabricDatastore.py +0 -0
  89. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/KeypadInput.py +0 -0
  90. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LaundryDryerControls.py +0 -0
  91. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LaundryWasherControls.py +0 -0
  92. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LaundryWasherMode.py +0 -0
  93. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LevelControl.py +0 -0
  94. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LocalizationConfiguration.py +0 -0
  95. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/LowPower.py +0 -0
  96. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/MediaInput.py +0 -0
  97. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/MediaPlayback.py +0 -0
  98. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Messages.py +0 -0
  99. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/MeterIdentification.py +0 -0
  100. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/MicrowaveOvenControl.py +0 -0
  101. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/MicrowaveOvenMode.py +0 -0
  102. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ModeSelect.py +0 -0
  103. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/NeoCluster.py +0 -0
  104. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/NetworkCommissioning.py +0 -0
  105. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/NitrogenDioxideConcentrationMeasurement.py +0 -0
  106. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OccupancySensing.py +0 -0
  107. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OnOff.py +0 -0
  108. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OperationalCredentials.py +0 -0
  109. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OperationalState.py +0 -0
  110. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OtaSoftwareUpdateProvider.py +0 -0
  111. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OtaSoftwareUpdateRequestor.py +0 -0
  112. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OvenCavityOperationalState.py +0 -0
  113. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OvenMode.py +0 -0
  114. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/OzoneConcentrationMeasurement.py +0 -0
  115. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Pm10ConcentrationMeasurement.py +0 -0
  116. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Pm1ConcentrationMeasurement.py +0 -0
  117. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Pm25ConcentrationMeasurement.py +0 -0
  118. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PowerSource.py +0 -0
  119. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PowerSourceConfiguration.py +0 -0
  120. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PowerTopology.py +0 -0
  121. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PressureMeasurement.py +0 -0
  122. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PumpConfigurationAndControl.py +0 -0
  123. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/PushAvStreamTransport.py +0 -0
  124. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RadonConcentrationMeasurement.py +0 -0
  125. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RefrigeratorAlarm.py +0 -0
  126. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RefrigeratorAndTemperatureControlledCabinetMode.py +0 -0
  127. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RelativeHumidityMeasurement.py +0 -0
  128. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RvcCleanMode.py +0 -0
  129. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RvcOperationalState.py +0 -0
  130. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/RvcRunMode.py +0 -0
  131. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ScenesManagement.py +0 -0
  132. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ServiceArea.py +0 -0
  133. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/SmokeCoAlarm.py +0 -0
  134. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/SoftwareDiagnostics.py +0 -0
  135. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/SoilMeasurement.py +0 -0
  136. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Switch.py +0 -0
  137. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TargetNavigator.py +0 -0
  138. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TclDehumidifierCluster.py +0 -0
  139. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TemperatureAlarm.py +0 -0
  140. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TemperatureControl.py +0 -0
  141. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TemperatureMeasurement.py +0 -0
  142. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/Thermostat.py +0 -0
  143. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ThermostatUserInterfaceConfiguration.py +0 -0
  144. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ThirdRealityMeteringCluster.py +0 -0
  145. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ThreadBorderRouterManagement.py +0 -0
  146. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ThreadNetworkDiagnostics.py +0 -0
  147. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ThreadNetworkDirectory.py +0 -0
  148. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TimeFormatLocalization.py +0 -0
  149. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TimeSynchronization.py +0 -0
  150. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TlsCertificateManagement.py +0 -0
  151. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TlsClientManagement.py +0 -0
  152. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/TotalVolatileOrganicCompoundsConcentrationMeasurement.py +0 -0
  153. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/UnitLocalization.py +0 -0
  154. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/UserLabel.py +0 -0
  155. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ValveConfigurationAndControl.py +0 -0
  156. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WagoCluster.py +0 -0
  157. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WakeOnLan.py +0 -0
  158. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WaterHeaterManagement.py +0 -0
  159. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WaterHeaterMode.py +0 -0
  160. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WaterTankLevelMonitoring.py +0 -0
  161. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WebRtcTransportDefinitions.py +0 -0
  162. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WebRtcTransportProvider.py +0 -0
  163. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WebRtcTransportRequestor.py +0 -0
  164. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WiFiNetworkDiagnostics.py +0 -0
  165. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WiFiNetworkManagement.py +0 -0
  166. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/WindowCovering.py +0 -0
  167. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/ZoneManagement.py +0 -0
  168. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/cluster_defs/__init__.py +0 -0
  169. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/clusters/enum.py +0 -0
  170. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/tlv/__init__.py +0 -0
  171. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/chip/tlv/tlvlist.py +0 -0
  172. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/dependency_links.txt +0 -0
  173. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/not-zip-safe +0 -0
  174. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/requires.txt +0 -0
  175. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_python_client.egg-info/top_level.txt +0 -0
  176. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/__init__.py +0 -0
  177. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/__init__.py +0 -0
  178. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/connection.py +0 -0
  179. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/exceptions.py +0 -0
  180. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/models/__init__.py +0 -0
  181. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/client/models/device_types.py +0 -0
  182. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/__init__.py +0 -0
  183. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/const.py +0 -0
  184. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/custom_clusters.py +0 -0
  185. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/errors.py +0 -0
  186. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/helpers/__init__.py +0 -0
  187. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/helpers/api.py +0 -0
  188. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/helpers/json.py +0 -0
  189. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/helpers/util.py +0 -0
  190. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/common/models.py +0 -0
  191. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/matter_server/py.typed +0 -0
  192. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/setup.cfg +0 -0
  193. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_bigint_wire.py +0 -0
  194. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_client.py +0 -0
  195. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_client_integration.py +0 -0
  196. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_credentials.py +0 -0
  197. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_icd.py +0 -0
  198. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_imports.py +0 -0
  199. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_network_topology.py +0 -0
  200. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_ota_upload.py +0 -0
  201. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_unix_socket.py +0 -0
  202. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_util.py +0 -0
  203. {matter_python_client-1.4.1a0.dev20261008 → matter_python_client-1.4.1a0.dev20261009}/tests/test_webrtc_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matter-python-client
3
- Version: 1.4.1a0.dev20261008
3
+ Version: 1.4.1a0.dev20261009
4
4
  Summary: Python Client for the OHF Matter Server
5
5
  Author-email: Open Home Foundation <hello@openhomefoundation.io>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matter-python-client
3
- Version: 1.4.1a0.dev20261008
3
+ Version: 1.4.1a0.dev20261009
4
4
  Summary: Python Client for the OHF Matter Server
5
5
  Author-email: Open Home Foundation <hello@openhomefoundation.io>
6
6
  License-Expression: Apache-2.0
@@ -184,6 +184,8 @@ matter_server/common/helpers/api.py
184
184
  matter_server/common/helpers/json.py
185
185
  matter_server/common/helpers/util.py
186
186
  tests/test_bigint_wire.py
187
+ tests/test_bridge_node_fixture.py
188
+ tests/test_bridge_topology.py
187
189
  tests/test_client.py
188
190
  tests/test_client_integration.py
189
191
  tests/test_credentials.py
@@ -908,7 +908,7 @@ class MatterClient:
908
908
  self._signal_event(EventType.ENDPOINT_REMOVED, data=msg.data, node_id=node_id)
909
909
  # cleanup endpoint only after signalling subscribers
910
910
  if node := self._nodes.get(node_id):
911
- node.endpoints.pop(endpoint_id, None)
911
+ node._remove_endpoint(endpoint_id)
912
912
  return
913
913
  if msg.event == EventType.ATTRIBUTE_UPDATED:
914
914
  # data is tuple[node_id, attribute_path, new_value]
@@ -932,6 +932,14 @@ class MatterClient:
932
932
  node_id = msg.data["node_id"]
933
933
  endpoint_id = msg.data["endpoint_id"]
934
934
  self.logger.debug("Endpoint added: %s/%s", node_id, endpoint_id)
935
+ # subscribers look the endpoint up; the node snapshot that carries it reaches them later
936
+ if (node := self._nodes.get(node_id)) is None or endpoint_id not in node.endpoints:
937
+ self.logger.debug(
938
+ "Not signalling endpoint %s/%s that the node model does not contain",
939
+ node_id,
940
+ endpoint_id,
941
+ )
942
+ return
935
943
  if msg.event == EventType.NODE_EVENT:
936
944
  if self.logger.isEnabledFor(logging.DEBUG):
937
945
  self.logger.debug(
@@ -24,10 +24,15 @@ from .device_types import (
24
24
  )
25
25
 
26
26
  if TYPE_CHECKING:
27
+ from collections.abc import Mapping
28
+
27
29
  from matter_server.common.models import MatterNodeData
28
30
 
29
31
  LOGGER = logging.getLogger(__name__)
30
32
 
33
+ # the Matter specification places the Root Node on endpoint 0 of every node
34
+ ROOT_ENDPOINT_ID = 0
35
+
31
36
  # pylint: disable=invalid-name
32
37
  _CLUSTER_T = TypeVar("_CLUSTER_T", bound=Clusters.Cluster)
33
38
  _ATTRIBUTE_T = TypeVar("_ATTRIBUTE_T", bound=Clusters.ClusterAttributeDescriptor)
@@ -44,6 +49,18 @@ def get_object_params(
44
49
  raise KeyError(f"No descriptor found for object {object_id}")
45
50
 
46
51
 
52
+ def _reaches(start_id: int, target_id: int, parents: Mapping[int, int]) -> bool:
53
+ """Return whether walking up the parent chain from an endpoint arrives at another."""
54
+ seen: set[int] = set()
55
+ current: int | None = start_id
56
+ while current is not None and current not in seen:
57
+ if current == target_id:
58
+ return True
59
+ seen.add(current)
60
+ current = parents.get(current)
61
+ return False
62
+
63
+
47
64
  @dataclass
48
65
  class MatterFabricData:
49
66
  """Data about a Matter fabric."""
@@ -84,18 +101,25 @@ class MatterEndpoint:
84
101
  @property
85
102
  def device_info(
86
103
  self,
87
- ) -> Clusters.BasicInformation | Clusters.BridgedDeviceBasicInformation:
104
+ ) -> Clusters.BasicInformation | Clusters.BridgedDeviceBasicInformation | None:
88
105
  """
89
106
  Return device info.
90
107
 
91
- If this endpoint represents a BridgedDevice, returns BridgedDeviceBasic.
92
- If this endpoint represents a ComposedDevice, returns the info of the compose device.
93
- Otherwise, returns BasicInformation from the Node itself (endpoint 0).
108
+ Returns the BridgedDeviceBasicInformation of the Bridged Node this endpoint is or is a part
109
+ of, else the BasicInformation of the Node itself. It is None when that Bridged Node reports
110
+ no BridgedDeviceBasicInformation, because the info of the bridge or of the Aggregator above
111
+ it belongs to a different device.
94
112
  """
95
- if self.is_bridged_device:
96
- return self.get_cluster(Clusters.BridgedDeviceBasicInformation)
97
- if compose_parent := self.node.get_compose_parent(self.endpoint_id):
98
- return compose_parent.device_info
113
+ endpoint = self
114
+ seen: set[int] = set()
115
+ while endpoint.endpoint_id not in seen:
116
+ seen.add(endpoint.endpoint_id)
117
+ if endpoint.is_bridged_device:
118
+ return endpoint.get_cluster(Clusters.BridgedDeviceBasicInformation)
119
+ parent = endpoint.node.get_closest_compose_parent(endpoint.endpoint_id)
120
+ if parent is None:
121
+ break
122
+ endpoint = parent
99
123
  return self.node.device_info
100
124
 
101
125
  def has_cluster(self, cluster: type[_CLUSTER_T] | int) -> bool:
@@ -217,11 +241,14 @@ class MatterEndpoint:
217
241
 
218
242
  def update(self, attributes_data: dict[str, Any]) -> None:
219
243
  """Update MatterEndpoint from (endpoint-specific) raw Attributes data."""
244
+ # a snapshot carries the complete state of the endpoint, never a delta
245
+ self.clusters = {}
220
246
  # unwrap cluster and clusterattributes from raw node data attributes
221
247
  for attribute_path, attribute_value in attributes_data.items():
222
248
  self.set_attribute_value(attribute_path, attribute_value)
223
- # extract device types from Descriptor Cluster
224
- if cluster := self.get_cluster(Clusters.Descriptor):
249
+ # a snapshot without the DeviceTypeList says nothing about the device types, so the previous ones stay
250
+ if f"{self.endpoint_id}/29/0" in attributes_data and (cluster := self.get_cluster(Clusters.Descriptor)):
251
+ self.device_types = set()
225
252
  for dev_info in cluster.deviceTypeList:
226
253
  device_type = DEVICE_TYPES.get(dev_info.deviceType)
227
254
  if device_type is None:
@@ -240,9 +267,10 @@ class MatterNode:
240
267
  def __init__(self, node_data: MatterNodeData) -> None:
241
268
  """Initialize MatterNode from MatterNodeData."""
242
269
  self.endpoints: dict[int, MatterEndpoint] = {}
243
- # composed devices reference to other endpoints through the partsList attribute
244
- # create a mapping table
245
270
  self._composed_endpoints: dict[int, int] = {}
271
+ self._bridge_parents: dict[int, int] = {}
272
+ self._snapshot_endpoint_ids: set[int] = set()
273
+ self._reported_missing_bridged_info: set[int] = set()
246
274
  self.update(node_data)
247
275
 
248
276
  @property
@@ -263,11 +291,12 @@ class MatterNode:
263
291
  return self.node_data.available
264
292
 
265
293
  @property
266
- def device_info(self) -> Clusters.BasicInformation:
294
+ def device_info(self) -> Clusters.BasicInformation | None:
267
295
  """
268
296
  Return device info for this Node.
269
297
 
270
- Returns BasicInformation from the Node itself (endpoint 0).
298
+ Returns BasicInformation from the Node itself (endpoint 0), or None while the node has not
299
+ reported it.
271
300
  """
272
301
  return self.get_cluster(0, Clusters.BasicInformation)
273
302
 
@@ -304,17 +333,49 @@ class MatterNode:
304
333
 
305
334
  Returns None is the Cluster is not present on the node.
306
335
  """
307
- return self.endpoints[endpoint].get_cluster(cluster)
336
+ if (endpoint_obj := self.endpoints.get(endpoint)) is None:
337
+ return None
338
+ return endpoint_obj.get_cluster(cluster)
308
339
 
309
340
  def get_compose_parent(self, endpoint_id: int) -> MatterEndpoint | None:
310
- """Return endpoint of parent if the endpoint belongs to a Composed device."""
311
- if parent_id := self._composed_endpoints.get(endpoint_id):
312
- return self.endpoints[parent_id]
313
- return None
341
+ """Return the endpoint of the composed device the endpoint is a part of, at any depth.
342
+
343
+ Consumers derive the device of a part from this endpoint, so a part nested below another
344
+ part resolves to the top of the composition, not to its closest parent.
345
+ """
346
+ if (parent_id := self._composed_endpoints.get(endpoint_id)) is None:
347
+ return None
348
+ seen = {endpoint_id, parent_id}
349
+ while (next_id := self._composed_endpoints.get(parent_id)) is not None and next_id not in seen:
350
+ seen.add(next_id)
351
+ parent_id = next_id
352
+ return self.endpoints.get(parent_id)
353
+
354
+ def get_closest_compose_parent(self, endpoint_id: int) -> MatterEndpoint | None:
355
+ """Return the closest endpoint the endpoint is a part of."""
356
+ if (parent_id := self._composed_endpoints.get(endpoint_id)) is None:
357
+ return None
358
+ return self.endpoints.get(parent_id)
359
+
360
+ def get_compose_child_ids(self, endpoint_id: int) -> tuple[int, ...]:
361
+ """Return endpoint IDs of the parts, at any depth, of the composed device the endpoint is the top of."""
362
+ return tuple(
363
+ sorted(
364
+ child_id
365
+ for child_id in self._composed_endpoints
366
+ if (parent := self.get_compose_parent(child_id)) is not None and parent.endpoint_id == endpoint_id
367
+ )
368
+ )
314
369
 
315
- def get_compose_child_ids(self, endpoint_id: int) -> tuple[int, ...] | None:
316
- """Return endpoint IDs of any child if the endpoint represents a Composed device."""
317
- return tuple(x for x, y in self._composed_endpoints.items() if y == endpoint_id)
370
+ def get_bridge_parent(self, endpoint_id: int) -> MatterEndpoint | None:
371
+ """Return the Aggregator endpoint that bridges the given endpoint, if any."""
372
+ if (parent_id := self._bridge_parents.get(endpoint_id)) is None:
373
+ return None
374
+ return self.endpoints.get(parent_id)
375
+
376
+ def get_bridge_child_ids(self, endpoint_id: int) -> tuple[int, ...]:
377
+ """Return endpoint IDs of the devices bridged by the given Aggregator endpoint."""
378
+ return tuple(sorted(x for x, y in self._bridge_parents.items() if y == endpoint_id))
318
379
 
319
380
  def update(self, node_data: MatterNodeData) -> None:
320
381
  """Update MatterNode from MatterNodeData."""
@@ -326,6 +387,7 @@ class MatterNode:
326
387
  if endpoint_id not in endpoint_data:
327
388
  endpoint_data[endpoint_id] = {}
328
389
  endpoint_data[endpoint_id][attribute_path] = attribute_data
390
+ self._snapshot_endpoint_ids = set(endpoint_data)
329
391
  for endpoint_id, attributes_data in endpoint_data.items():
330
392
  if endpoint_id in self.endpoints:
331
393
  self.endpoints[endpoint_id].update(attributes_data)
@@ -333,15 +395,64 @@ class MatterNode:
333
395
  self.endpoints[endpoint_id] = MatterEndpoint(
334
396
  endpoint_id=endpoint_id, attributes_data=attributes_data, node=self
335
397
  )
336
- # composed devices reference to other endpoints through the partsList attribute
337
- # create a mapping table to quickly map this
398
+ self._map_endpoint_parents()
399
+
400
+ def _remove_endpoint(self, endpoint_id: int) -> None:
401
+ """Remove an endpoint together with its parts, and every parent relation they are in.
402
+
403
+ This is the only way an endpoint leaves `endpoints`: a snapshot that no longer reports an
404
+ endpoint keeps its object until the server announces the removal, because consumers look
405
+ the endpoint up when handling that announcement. The parts of the endpoint, at any depth,
406
+ leave with it whether or not a snapshot still reports them: their device is gone, and left
407
+ behind they would resolve to no device when their own removal arrives. A device the
408
+ endpoint bridged stays, without its bridge parent, until its own removal arrives.
409
+
410
+ May only be called by logic that received data from the server.
411
+ """
412
+ if endpoint_id not in self.endpoints:
413
+ return
414
+ removed = {endpoint_id}
415
+ pending = [endpoint_id]
416
+ while pending:
417
+ parent_id = pending.pop()
418
+ for child_id, child_parent_id in self._composed_endpoints.items():
419
+ if child_parent_id == parent_id and child_id not in removed:
420
+ removed.add(child_id)
421
+ pending.append(child_id)
422
+ for removed_id in removed:
423
+ del self.endpoints[removed_id]
424
+ self._reported_missing_bridged_info.discard(removed_id)
425
+ self._composed_endpoints = {
426
+ child_id: parent_id for child_id, parent_id in self._composed_endpoints.items() if child_id not in removed
427
+ }
428
+ self._bridge_parents = {
429
+ child_id: parent_id
430
+ for child_id, parent_id in self._bridge_parents.items()
431
+ if child_id not in removed and parent_id not in removed
432
+ }
433
+
434
+ def _map_endpoint_parents(self) -> None:
435
+ """Resolve each endpoint to its closest parent endpoint, split by parent kind.
436
+
437
+ An endpoint's partsList may enumerate its whole family - every descendant, not just
438
+ the direct children - which is the pattern a bridge uses. Of the endpoints listing a
439
+ given endpoint, the closest one is therefore the endpoint that lists none of the
440
+ others - on contradictory lists the smaller family wins, then the lower endpoint
441
+ number - and a parent is only accepted while the relations stay a tree.
442
+
443
+ Only the endpoints of the last snapshot take part. A snapshot replaces the relation of an
444
+ endpoint only where it names a parent for it: one that bridges or composes it, or the
445
+ parent it already had, whose classification then wins. Where it names none - the endpoint
446
+ unlisted or unreported, its parent without a Descriptor or device types, or listed only by
447
+ endpoints that make it neither bridged nor a part - the endpoint keeps its relation until
448
+ its own removal or that of the parent is announced, so a consumer handling the
449
+ announcement still finds the device the endpoint is a part of. A kept relation that would
450
+ close a cycle with a named one is dropped.
451
+ """
452
+ snapshot_ids = self._snapshot_endpoint_ids & self.endpoints.keys()
453
+ families: dict[int, set[int]] = {}
338
454
  for endpoint in self.endpoints.values():
339
- if RootNode in endpoint.device_types:
340
- # ignore root endpoint
341
- continue
342
- if Aggregator in endpoint.device_types:
343
- # ignore Bridge endpoint
344
- # (as that will also use partsList to indicate its child's)
455
+ if endpoint.endpoint_id not in snapshot_ids:
345
456
  continue
346
457
  descriptor = endpoint.get_cluster(Clusters.Descriptor)
347
458
  if descriptor is None:
@@ -351,9 +462,110 @@ class MatterNode:
351
462
  endpoint.endpoint_id,
352
463
  )
353
464
  continue
354
- if descriptor.partsList:
355
- for endpoint_id in descriptor.partsList:
356
- self._composed_endpoints[endpoint_id] = endpoint.endpoint_id
465
+ families[endpoint.endpoint_id] = {
466
+ child_id
467
+ for child_id in descriptor.partsList or ()
468
+ if child_id in snapshot_ids and child_id != endpoint.endpoint_id
469
+ }
470
+
471
+ candidates: dict[int, set[int]] = {}
472
+ for parent_id, family in families.items():
473
+ for child_id in family:
474
+ candidates.setdefault(child_id, set()).add(parent_id)
475
+
476
+ parents: dict[int, int] = {}
477
+ for child_id in sorted(candidates):
478
+ parent_ids = candidates[child_id]
479
+ for parent_id in sorted(
480
+ parent_ids,
481
+ key=lambda pid: (
482
+ len((parent_ids - {pid}) & families[pid]),
483
+ len(families[pid]),
484
+ pid,
485
+ ),
486
+ ):
487
+ if _reaches(parent_id, child_id, parents):
488
+ LOGGER.warning(
489
+ "Ignoring cyclic partsList relation: Node %s, endpoint %s below %s",
490
+ self.node_id,
491
+ child_id,
492
+ parent_id,
493
+ )
494
+ continue
495
+ parents[child_id] = parent_id
496
+ break
497
+
498
+ previous_composed = self._composed_endpoints
499
+ previous_bridge = self._bridge_parents
500
+ self._composed_endpoints = {}
501
+ self._bridge_parents = {}
502
+ decided = {
503
+ child_id for child_id, parent_id in parents.items() if self._map_endpoint_parent(child_id, parent_id)
504
+ }
505
+ related = self._composed_endpoints | self._bridge_parents
506
+ for relations, previous in (
507
+ (self._composed_endpoints, previous_composed),
508
+ (self._bridge_parents, previous_bridge),
509
+ ):
510
+ for child_id, parent_id in sorted(previous.items()):
511
+ if child_id in related or (child_id in decided and parents[child_id] == parent_id):
512
+ continue
513
+ if _reaches(parent_id, child_id, related):
514
+ LOGGER.warning(
515
+ "Dropping parent relation that a snapshot made cyclic: Node %s, endpoint %s below %s",
516
+ self.node_id,
517
+ child_id,
518
+ parent_id,
519
+ )
520
+ continue
521
+ relations[child_id] = parent_id
522
+ related[child_id] = parent_id
523
+
524
+ self._report_bridged_devices_without_info()
525
+
526
+ def _report_bridged_devices_without_info(self) -> None:
527
+ """Warn about bridged devices the bridge does not describe, once per endpoint.
528
+
529
+ Such an endpoint has no device info of its own, and the info of the bridge describes a
530
+ different device, so it stays unnamed until the bridge reports it.
531
+ """
532
+ for endpoint_id, endpoint in self.endpoints.items():
533
+ if endpoint_id in self._reported_missing_bridged_info:
534
+ continue
535
+ if not endpoint.is_bridged_device:
536
+ continue
537
+ if endpoint.get_cluster(Clusters.BridgedDeviceBasicInformation) is not None:
538
+ continue
539
+ self._reported_missing_bridged_info.add(endpoint_id)
540
+ LOGGER.warning(
541
+ "Bridged device without BridgedDeviceBasicInformation: Node %s, endpoint %s",
542
+ self.node_id,
543
+ endpoint_id,
544
+ )
545
+
546
+ def _map_endpoint_parent(self, child_id: int, parent_id: int) -> bool:
547
+ """Record what the parent endpoint makes of the endpoint below it, return whether it decides.
548
+
549
+ A child of the root endpoint is top level. Below an Aggregator, a Bridged Node is a
550
+ bridged device; any other child stands alone, whether or not the Aggregator is itself a
551
+ Bridged Node (so ids derived from the compose parent match older clients). Below any other endpoint, a child
552
+ is a part of it - also when the device types of that endpoint are unknown to this client,
553
+ so only a parent that reports no device type at all decides nothing.
554
+ """
555
+ if parent_id == ROOT_ENDPOINT_ID:
556
+ return True
557
+ parent = self.endpoints[parent_id]
558
+ descriptor = parent.get_cluster(Clusters.Descriptor)
559
+ assert descriptor is not None # only endpoints with a Descriptor are parents
560
+ if not descriptor.deviceTypeList:
561
+ return False
562
+ if RootNode in parent.device_types:
563
+ return True
564
+ if Aggregator not in parent.device_types:
565
+ self._composed_endpoints[child_id] = parent_id
566
+ elif BridgedNode in self.endpoints[child_id].device_types:
567
+ self._bridge_parents[child_id] = parent_id
568
+ return True
357
569
 
358
570
  def update_attribute(self, attribute_path: str, new_value: Any) -> None:
359
571
  """Handle Attribute value update."""
@@ -25,7 +25,7 @@ license = "Apache-2.0"
25
25
  name = "matter-python-client"
26
26
  readme = "README.md"
27
27
  requires-python = ">=3.12"
28
- version = "1.4.1a0.dev20261008"
28
+ version = "1.4.1a0.dev20261009"
29
29
 
30
30
  [project.urls]
31
31
  Homepage = "https://github.com/matter-js/matterjs-server"