pywiim 2.3.1__tar.gz → 2.3.2__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 (86) hide show
  1. {pywiim-2.3.1/pywiim.egg-info → pywiim-2.3.2}/PKG-INFO +1 -1
  2. {pywiim-2.3.1 → pywiim-2.3.2}/pyproject.toml +1 -1
  3. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/__init__.py +1 -1
  4. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/base.py +9 -1
  5. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/properties.py +25 -7
  6. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/profiles.py +4 -21
  7. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/state.py +8 -1
  8. {pywiim-2.3.1 → pywiim-2.3.2/pywiim.egg-info}/PKG-INFO +1 -1
  9. {pywiim-2.3.1 → pywiim-2.3.2}/LICENSE +0 -0
  10. {pywiim-2.3.1 → pywiim-2.3.2}/README.md +0 -0
  11. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/__init__.py +0 -0
  12. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/audio_pro.py +0 -0
  13. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/audio_settings.py +0 -0
  14. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/bluetooth.py +0 -0
  15. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/constants.py +0 -0
  16. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/device.py +0 -0
  17. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/diagnostics.py +0 -0
  18. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/endpoints.py +0 -0
  19. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/eq.py +0 -0
  20. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/firmware.py +0 -0
  21. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/group.py +0 -0
  22. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/lms.py +0 -0
  23. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/loop_mode.py +0 -0
  24. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/misc.py +0 -0
  25. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/parser.py +0 -0
  26. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/peq.py +0 -0
  27. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/playback.py +0 -0
  28. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/preset.py +0 -0
  29. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/ssl.py +0 -0
  30. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/subwoofer.py +0 -0
  31. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/api/timer.py +0 -0
  32. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/backoff.py +0 -0
  33. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/capabilities.py +0 -0
  34. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/__init__.py +0 -0
  35. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/diagnostics.py +0 -0
  36. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/discovery_cli.py +0 -0
  37. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/group_test_cli.py +0 -0
  38. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/join_test_cli.py +0 -0
  39. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/monitor_cli.py +0 -0
  40. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/cli/verify_cli.py +0 -0
  41. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/client.py +0 -0
  42. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/device_capabilities.py +0 -0
  43. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/discovery.py +0 -0
  44. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/exceptions.py +0 -0
  45. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/group.py +0 -0
  46. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/group_helpers.py +0 -0
  47. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/__init__.py +0 -0
  48. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/__main__.py +0 -0
  49. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/config.example.json +0 -0
  50. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/config.py +0 -0
  51. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/context.py +0 -0
  52. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/mcp/server.py +0 -0
  53. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/metadata.py +0 -0
  54. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/model_names.py +0 -0
  55. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/models.py +0 -0
  56. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/normalize.py +0 -0
  57. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/__init__.py +0 -0
  58. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/audio.py +0 -0
  59. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/base.py +0 -0
  60. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/bluetooth.py +0 -0
  61. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/coverart.py +0 -0
  62. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/debounce.py +0 -0
  63. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/diagnostics.py +0 -0
  64. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/groupops.py +0 -0
  65. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/media.py +0 -0
  66. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/playback.py +0 -0
  67. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/source_capabilities.py +0 -0
  68. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/statemgr.py +0 -0
  69. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/stream.py +0 -0
  70. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/stream_enricher.py +0 -0
  71. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/player/volume.py +0 -0
  72. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/polling.py +0 -0
  73. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/py.typed +0 -0
  74. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/role.py +0 -0
  75. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/__init__.py +0 -0
  76. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/client.py +0 -0
  77. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/eventer.py +0 -0
  78. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/health.py +0 -0
  79. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/metadata.py +0 -0
  80. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim/upnp/playqueue.py +0 -0
  81. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/SOURCES.txt +0 -0
  82. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/dependency_links.txt +0 -0
  83. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/entry_points.txt +0 -0
  84. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/requires.txt +0 -0
  85. {pywiim-2.3.1 → pywiim-2.3.2}/pywiim.egg-info/top_level.txt +0 -0
  86. {pywiim-2.3.1 → pywiim-2.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pywiim"
7
- version = "2.3.1"
7
+ version = "2.3.2"
8
8
  description = "Python library for WiiM/LinkPlay device communication"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -102,7 +102,7 @@ from .profiles import (
102
102
  from .role import RoleDetectionResult, detect_role
103
103
  from .state import GroupStateSynchronizer, StateSynchronizer
104
104
 
105
- __version__ = "2.3.1"
105
+ __version__ = "2.3.2"
106
106
  __all__ = [
107
107
  # Main client
108
108
  "WiiMClient",
@@ -489,7 +489,15 @@ class BaseWiiMClient:
489
489
  except Exception: # noqa: BLE001
490
490
  pass # Device info not available, continue without it
491
491
 
492
- raise WiiMRequestError(
492
+ error_cls = (
493
+ WiiMConnectionError
494
+ if isinstance(
495
+ err,
496
+ (aiohttp.ClientConnectorError, aiohttp.ServerDisconnectedError, WiiMConnectionError),
497
+ )
498
+ else WiiMRequestError
499
+ )
500
+ raise error_cls(
493
501
  f"Request failed after {retry_count} attempts: {err}",
494
502
  endpoint=endpoint,
495
503
  attempts=retry_count,
@@ -238,11 +238,22 @@ class PlayerProperties:
238
238
  # === Media Metadata ===
239
239
 
240
240
  def _status_field(self, *names: str) -> str | None:
241
- """Return the first non-empty attribute from merged state or cached status."""
242
- # First try merged state (combines HTTP + UPnP)
243
- merged = self.player._state_synchronizer.get_merged_state()
241
+ """Return the first non-empty attribute from merged state or cached status.
242
+
243
+ Only fall back to the raw cached status for names the synchronizer has
244
+ never resolved. If it resolved a name to no value (live stream, source
245
+ change), that decision must not be overridden by leftover firmware
246
+ fields still sitting on ``_status_model`` (wiim #263).
247
+ """
248
+ state_obj = self.player._state_synchronizer.get_state_object()
249
+ names_unresolved_by_synchronizer: list[str] = []
244
250
  for n in names:
245
- val = merged.get(n)
251
+ field = getattr(state_obj, n, None)
252
+ if field is None:
253
+ names_unresolved_by_synchronizer.append(n)
254
+ continue
255
+
256
+ val = field.value
246
257
  if val is not None:
247
258
  # Field-aware filtering: artwork fields must be valid URLs,
248
259
  # other metadata must be non-placeholder values.
@@ -269,11 +280,18 @@ class PlayerProperties:
269
280
  if val not in (None, ""):
270
281
  return str(val) if val is not None else None
271
282
 
272
- # Fallback to cached status if synchronizer has no data yet
273
- if self.player._status_model is None:
283
+ # Fallback to cached status only before the first merge. After that the
284
+ # raw status model is a leftover firmware snapshot and must not reopen
285
+ # fields the synchronizer already decided are empty (wiim #263).
286
+ if (
287
+ self.player._status_model is None
288
+ or not names_unresolved_by_synchronizer
289
+ or state_obj.http_last_update is not None
290
+ or state_obj.upnp_last_update is not None
291
+ ):
274
292
  return None
275
293
 
276
- for n in names:
294
+ for n in names_unresolved_by_synchronizer:
277
295
  if hasattr(self.player._status_model, n):
278
296
  val = getattr(self.player._status_model, n)
279
297
  if n in {"image_url", "entity_picture", "cover_url"}:
@@ -520,18 +520,7 @@ def get_device_profile(device_info: DeviceInfo) -> DeviceProfile:
520
520
  device_info.firmware,
521
521
  )
522
522
  # Return a modified profile with WiFi Direct enabled
523
- return DeviceProfile(
524
- vendor=profile.vendor,
525
- generation=profile.generation,
526
- display_name=profile.display_name,
527
- loop_mode_scheme=profile.loop_mode_scheme,
528
- state_sources=profile.state_sources,
529
- connection=profile.connection,
530
- endpoints=profile.endpoints,
531
- grouping=GroupingConfig(
532
- uses_wifi_direct=True,
533
- ),
534
- )
523
+ return replace(profile, grouping=GroupingConfig(uses_wifi_direct=True))
535
524
 
536
525
  return profile
537
526
 
@@ -553,17 +542,11 @@ def get_device_profile(device_info: DeviceInfo) -> DeviceProfile:
553
542
  device_info.wmrm_version,
554
543
  device_info.firmware,
555
544
  )
556
- return DeviceProfile(
557
- vendor=profile.vendor,
545
+ return replace(
546
+ profile,
558
547
  generation="gen1",
559
548
  display_name=f"{profile.display_name} (Gen1)",
560
- loop_mode_scheme=profile.loop_mode_scheme,
561
- state_sources=profile.state_sources,
562
- connection=profile.connection,
563
- endpoints=profile.endpoints,
564
- grouping=GroupingConfig(
565
- uses_wifi_direct=True,
566
- ),
549
+ grouping=GroupingConfig(uses_wifi_direct=True),
567
550
  )
568
551
 
569
552
  return profile
@@ -951,10 +951,17 @@ class StateSynchronizer:
951
951
  Firmware often keeps the previous source's metadata in getPlayerStatusEx
952
952
  (Spotify → network stream). Clearing here prevents a permanently stale track.
953
953
  """
954
+ now = time.time()
954
955
  for field_name in ("title", "artist", "album", "image_url", "duration"):
955
956
  self._http_state.pop(field_name, None)
956
957
  self._upnp_state.pop(field_name, None)
957
- setattr(self._merged_state, field_name, None)
958
+ # Keep a resolved-to-None field so Player._status_field does not
959
+ # fall back to leftover firmware values on _status_model (#263).
960
+ setattr(
961
+ self._merged_state,
962
+ field_name,
963
+ TimestampedField(value=None, source="http", timestamp=now),
964
+ )
958
965
 
959
966
  def _should_clear_metadata(self) -> bool:
960
967
  """Determine if metadata should be cleared.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywiim
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Python library for WiiM/LinkPlay device communication
5
5
  Author-email: Michael Cumming <mjcumming@gmail.com>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes