ucapi-framework 1.9.0b3__tar.gz → 1.9.0b4__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 (50) hide show
  1. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/PKG-INFO +1 -1
  2. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/pyproject.toml +1 -1
  3. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/driver.py +12 -6
  4. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entity.py +27 -16
  5. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework.egg-info/PKG-INFO +1 -1
  6. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/README.md +0 -0
  7. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/setup.cfg +0 -0
  8. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_button_entity.py +0 -0
  9. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_climate_entity.py +0 -0
  10. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_config.py +0 -0
  11. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_cover_entity.py +0 -0
  12. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_device.py +0 -0
  13. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_discovery.py +0 -0
  14. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_driver.py +0 -0
  15. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_entity.py +0 -0
  16. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_factory_pattern.py +0 -0
  17. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_helpers.py +0 -0
  18. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_ir_emitter_entity.py +0 -0
  19. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_light_entity.py +0 -0
  20. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_media_player_entity.py +0 -0
  21. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_migration.py +0 -0
  22. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_remote_entity.py +0 -0
  23. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_select_entity.py +0 -0
  24. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_sensor_entity.py +0 -0
  25. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_setup.py +0 -0
  26. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_switch_entity.py +0 -0
  27. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/tests/test_voice_assistant_entity.py +0 -0
  28. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/__init__.py +0 -0
  29. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/config.py +0 -0
  30. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/device.py +0 -0
  31. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/discovery.py +0 -0
  32. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/__init__.py +0 -0
  33. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/button.py +0 -0
  34. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/climate.py +0 -0
  35. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/cover.py +0 -0
  36. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/ir_emitter.py +0 -0
  37. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/light.py +0 -0
  38. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/media_player.py +0 -0
  39. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/remote.py +0 -0
  40. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/select.py +0 -0
  41. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/sensor.py +0 -0
  42. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/switch.py +0 -0
  43. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/entities/voice_assistant.py +0 -0
  44. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/helpers.py +0 -0
  45. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/migration.py +0 -0
  46. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework/setup.py +0 -0
  47. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework.egg-info/SOURCES.txt +0 -0
  48. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework.egg-info/dependency_links.txt +0 -0
  49. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework.egg-info/requires.txt +0 -0
  50. {ucapi_framework-1.9.0b3 → ucapi_framework-1.9.0b4}/ucapi_framework.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucapi-framework
3
- Version: 1.9.0b3
3
+ Version: 1.9.0b4
4
4
  Summary: ucapi framework that provides core functionality for building integrations.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ucapi-framework"
3
- version = "1.9.0b3"
3
+ version = "1.9.0b4"
4
4
  description = "ucapi framework that provides core functionality for building integrations."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -594,8 +594,8 @@ class BaseIntegrationDriver(Generic[DeviceT, ConfigT]):
594
594
  cast(FrameworkEntity, configured_entity) if has_update else None
595
595
  )
596
596
 
597
- # Short-circuit: if entity has overridden sync_state(), delegate entirely to it.
598
- # This is the coordinator pattern — the entity knows how to read its own device.
597
+ # Coordinator pattern: entity owns its state via sync_state() + subscribe_to_device().
598
+ # Call sync_state() directly and skip all legacy attribute-routing paths.
599
599
  if (
600
600
  framework_entity
601
601
  and type(framework_entity).sync_state is not FrameworkEntity.sync_state
@@ -1216,9 +1216,10 @@ class BaseIntegrationDriver(Generic[DeviceT, ConfigT]):
1216
1216
  """
1217
1217
  Handle device connection.
1218
1218
 
1219
- Sets integration device state to CONNECTED and refreshes all entity states
1220
- for this device. This ensures entity states are updated after the device
1221
- has connected and populated its attributes via get_device_attributes().
1219
+ Sets integration device state to CONNECTED and refreshes state for legacy-pattern
1220
+ entities. Coordinator-pattern entities (those with ``sync_state()`` overridden)
1221
+ are skipped here — they receive state via ``push_update()`` emitted by the
1222
+ device's own ``connect()`` implementation, avoiding a redundant double-sync.
1222
1223
 
1223
1224
  :param device_id: Device identifier
1224
1225
  """
@@ -1230,8 +1231,13 @@ class BaseIntegrationDriver(Generic[DeviceT, ConfigT]):
1230
1231
 
1231
1232
  await self.api.set_device_state(ucapi.DeviceStates.CONNECTED)
1232
1233
 
1233
- # Refresh entity states now that device is connected and has populated attributes
1234
1234
  for entity_id in self.get_entity_ids_for_device(device_id):
1235
+ configured_entity = self.api.configured_entities.get(entity_id)
1236
+ if isinstance(configured_entity, FrameworkEntity) and (
1237
+ type(configured_entity).sync_state is not FrameworkEntity.sync_state
1238
+ ):
1239
+ # Coordinator pattern: entity syncs via push_update() from device.connect()
1240
+ continue
1235
1241
  await self.refresh_entity_state(entity_id)
1236
1242
 
1237
1243
  async def on_device_disconnected(self, device_id: str) -> None:
@@ -173,6 +173,10 @@ class Entity(ABC):
173
173
  :param update: dictionary containing the updated properties.
174
174
  :param force: if True, update attributes even if they haven't changed.
175
175
  """
176
+ # Skip entirely if this entity is not configured on the Remote.
177
+ if not self._api.configured_entities.contains(self._framework_entity_id):
178
+ return
179
+
176
180
  # Strip None values - they represent attributes that have never been set
177
181
  # and should not be sent to the Remote. Empty strings and False are valid.
178
182
  update = {k: v for k, v in update.items() if v is not None}
@@ -289,8 +293,9 @@ class Entity(ABC):
289
293
  def on_device_disconnected(self):
290
294
  self.set_unavailable()
291
295
  """
292
- self.attributes[media_player.Attributes.STATE] = media_player.States.UNAVAILABLE # type: ignore[attr-defined]
293
- self.update(self.attributes) # type: ignore[no-member]
296
+ # All ucapi States enums share the same UNAVAILABLE string value,
297
+ # so media_player.States.UNAVAILABLE works as a proxy for all entity types.
298
+ self.update({media_player.Attributes.STATE: media_player.States.UNAVAILABLE})
294
299
 
295
300
  def subscribe_to_device(self, device: "BaseDeviceInterface") -> None:
296
301
  """
@@ -317,11 +322,10 @@ class Entity(ABC):
317
322
  self.subscribe_to_device(device)
318
323
 
319
324
  async def sync_state(self) -> None:
320
- self.attributes[light.Attributes.STATE] = self.map_entity_states(
321
- self._device.state
322
- )
323
- self.attributes[light.Attributes.BRIGHTNESS] = self._device.brightness
324
- self.update(self.attributes)
325
+ self.update({
326
+ light.Attributes.STATE: self.map_entity_states(self._device.state),
327
+ light.Attributes.BRIGHTNESS: self._device.brightness,
328
+ })
325
329
  """
326
330
  from .device import DeviceEvents # local import to avoid circular dependency
327
331
 
@@ -335,9 +339,9 @@ class Entity(ABC):
335
339
  """
336
340
  Sync entity state from device to Remote.
337
341
 
338
- Override this method to read current values from ``self._device``,
339
- write them into ``self.attributes``, and call ``self.update(self.attributes)``
340
- to push the state to the Remote.
342
+ Override this method to read current values from ``self._device`` and call
343
+ ``self.update()`` with a **fresh dict or dataclass** — do not mutate
344
+ ``self.attributes`` directly, as that would defeat change-filtering.
341
345
 
342
346
  The framework calls this method automatically in two situations:
343
347
 
@@ -351,14 +355,21 @@ class Entity(ABC):
351
355
  manages its own state (i.e. the developer is not using the driver's
352
356
  default ``on_device_update`` attribute-routing logic).
353
357
 
354
- Example::
358
+ Example with dict::
355
359
 
356
360
  async def sync_state(self) -> None:
357
- self.attributes[light.Attributes.STATE] = self.map_entity_states(
358
- self._device.state
359
- )
360
- self.attributes[light.Attributes.BRIGHTNESS] = self._device.brightness
361
- self.update(self.attributes)
361
+ self.update({
362
+ light.Attributes.STATE: self.map_entity_states(self._device.state),
363
+ light.Attributes.BRIGHTNESS: self._device.brightness,
364
+ })
365
+
366
+ Example with dataclass (recommended)::
367
+
368
+ async def sync_state(self) -> None:
369
+ self.update(LightAttributes(
370
+ STATE=self.map_entity_states(self._device.state),
371
+ BRIGHTNESS=self._device.brightness,
372
+ ))
362
373
  """
363
374
  # No-op by default. Subclasses override to pull from device and push to Remote.
364
375
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucapi-framework
3
- Version: 1.9.0b3
3
+ Version: 1.9.0b4
4
4
  Summary: ucapi framework that provides core functionality for building integrations.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown