aiohomematic 2025.10.21__py3-none-any.whl → 2025.10.24__py3-none-any.whl

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.

Potentially problematic release.


This version of aiohomematic might be problematic. Click here for more details.

@@ -143,7 +143,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
143
143
  "_custom_id",
144
144
  "_data_point_updated_callbacks",
145
145
  "_device_removed_callbacks",
146
- "_fired_event_at",
146
+ "_emitted_event_at",
147
147
  "_modified_at",
148
148
  "_path_data",
149
149
  "_refreshed_at",
@@ -163,7 +163,7 @@ class CallbackDataPoint(ABC, LogContextMixin):
163
163
  self._device_removed_callbacks: list[Callable] = []
164
164
  self._custom_id: str | None = None
165
165
  self._path_data = self._get_path_data()
166
- self._fired_event_at: datetime = INIT_DATETIME
166
+ self._emitted_event_at: datetime = INIT_DATETIME
167
167
  self._modified_at: datetime = INIT_DATETIME
168
168
  self._refreshed_at: datetime = INIT_DATETIME
169
169
  self._signature: Final = self._get_signature()
@@ -191,16 +191,16 @@ class CallbackDataPoint(ABC, LogContextMixin):
191
191
  return self._custom_id
192
192
 
193
193
  @property
194
- def fired_event_at(self) -> datetime:
195
- """Return the data point updated fired an event at."""
196
- return self._fired_event_at
194
+ def emitted_event_at(self) -> datetime:
195
+ """Return the data point updated emitted an event at."""
196
+ return self._emitted_event_at
197
197
 
198
198
  @state_property
199
- def fired_event_recently(self) -> bool:
200
- """Return the data point fired an event within 500 milliseconds."""
201
- if self._fired_event_at == INIT_DATETIME:
199
+ def emitted_event_recently(self) -> bool:
200
+ """Return the data point emitted an event within 500 milliseconds."""
201
+ if self._emitted_event_at == INIT_DATETIME:
202
202
  return False
203
- return (datetime.now() - self._fired_event_at).total_seconds() < 0.5
203
+ return (datetime.now() - self._emitted_event_at).total_seconds() < 0.5
204
204
 
205
205
  @classmethod
206
206
  def default_category(cls) -> DataPointCategory:
@@ -357,11 +357,11 @@ class CallbackDataPoint(ABC, LogContextMixin):
357
357
  self._device_removed_callbacks.remove(cb)
358
358
 
359
359
  @loop_check
360
- def fire_data_point_updated_callback(self, **kwargs: Any) -> None:
360
+ def emit_data_point_updated_event(self, **kwargs: Any) -> None:
361
361
  """Do what is needed when the value of the data_point has been updated/refreshed."""
362
- if not self._should_fire_data_point_updated_callback:
362
+ if not self._should_emit_data_point_updated_callback:
363
363
  return
364
- self._fired_event_at = datetime.now()
364
+ self._emitted_event_at = datetime.now()
365
365
  for callback_handler, custom_id in self._data_point_updated_callbacks.items():
366
366
  try:
367
367
  # Add the data_point reference once to kwargs to avoid per-callback writes.
@@ -369,19 +369,19 @@ class CallbackDataPoint(ABC, LogContextMixin):
369
369
  kwargs[KWARGS_ARG_CUSTOM_ID] = custom_id
370
370
  callback_handler(**kwargs)
371
371
  except Exception as exc:
372
- _LOGGER.warning("FIRE_DATA_POINT_UPDATED_EVENT failed: %s", extract_exc_args(exc=exc))
372
+ _LOGGER.warning("EMIT_DATA_POINT_UPDATED_EVENT failed: %s", extract_exc_args(exc=exc))
373
373
 
374
374
  @loop_check
375
- def fire_device_removed_callback(self) -> None:
375
+ def emit_device_removed_event(self) -> None:
376
376
  """Do what is needed when the data_point has been removed."""
377
377
  for callback_handler in self._device_removed_callbacks:
378
378
  try:
379
379
  callback_handler()
380
380
  except Exception as exc:
381
- _LOGGER.warning("FIRE_DEVICE_REMOVED_EVENT failed: %s", extract_exc_args(exc=exc))
381
+ _LOGGER.warning("EMIT_DEVICE_REMOVED_EVENT failed: %s", extract_exc_args(exc=exc))
382
382
 
383
383
  @property
384
- def _should_fire_data_point_updated_callback(self) -> bool:
384
+ def _should_emit_data_point_updated_callback(self) -> bool:
385
385
  """Check if a data point has been updated or refreshed."""
386
386
  return True
387
387
 
@@ -895,7 +895,7 @@ class BaseParameterDataPoint[
895
895
  if value == NO_CACHE_ENTRY:
896
896
  if self.refreshed_at != INIT_DATETIME:
897
897
  self._state_uncertain = True
898
- self.fire_data_point_updated_callback()
898
+ self.emit_data_point_updated_event()
899
899
  return (old_value, None) # type: ignore[return-value]
900
900
 
901
901
  new_value = self._convert_value(value=value)
@@ -906,7 +906,7 @@ class BaseParameterDataPoint[
906
906
  self._previous_value = old_value
907
907
  self._current_value = new_value
908
908
  self._state_uncertain = False
909
- self.fire_data_point_updated_callback()
909
+ self.emit_data_point_updated_event()
910
910
  return (old_value, new_value)
911
911
 
912
912
  def write_temporary_value(self, *, value: Any, write_at: datetime) -> None:
@@ -920,7 +920,7 @@ class BaseParameterDataPoint[
920
920
  self._set_temporary_modified_at(modified_at=write_at)
921
921
  self._temporary_value = temp_value
922
922
  self._state_uncertain = True
923
- self.fire_data_point_updated_callback()
923
+ self.emit_data_point_updated_event()
924
924
 
925
925
  def update_parameter_data(self) -> None:
926
926
  """Update parameter data."""
@@ -598,7 +598,7 @@ class Device(LogContextMixin, PayloadMixin):
598
598
  if self._forced_availability != forced_availability:
599
599
  self._forced_availability = forced_availability
600
600
  for dp in self.generic_data_points:
601
- dp.fire_data_point_updated_callback()
601
+ dp.emit_data_point_updated_event()
602
602
 
603
603
  @inspector
604
604
  async def export_device_definition(self) -> None:
@@ -674,17 +674,17 @@ class Device(LogContextMixin, PayloadMixin):
674
674
  await self._central.save_files(save_paramset_descriptions=True)
675
675
  for dp in self.generic_data_points:
676
676
  dp.update_parameter_data()
677
- self.fire_device_updated_callback()
677
+ self.emit_device_updated_callback()
678
678
 
679
679
  @loop_check
680
- def fire_device_updated_callback(self) -> None:
680
+ def emit_device_updated_callback(self) -> None:
681
681
  """Do what is needed when the state of the device has been updated."""
682
682
  self._set_modified_at()
683
683
  for callback_handler in self._device_updated_callbacks:
684
684
  try:
685
685
  callback_handler()
686
686
  except Exception as exc:
687
- _LOGGER.warning("FIRE_DEVICE_UPDATED failed: %s", extract_exc_args(exc=exc))
687
+ _LOGGER.warning("EMIT_DEVICE_UPDATED failed: %s", extract_exc_args(exc=exc))
688
688
 
689
689
  def __str__(self) -> str:
690
690
  """Provide some useful information."""
@@ -963,7 +963,7 @@ class Channel(LogContextMixin, PayloadMixin):
963
963
  self._calculated_data_points[data_point.dpk] = data_point
964
964
  if isinstance(data_point, GenericDataPoint):
965
965
  self._generic_data_points[data_point.dpk] = data_point
966
- self._device.register_device_updated_callback(cb=data_point.fire_data_point_updated_callback)
966
+ self._device.register_device_updated_callback(cb=data_point.emit_data_point_updated_event)
967
967
  if isinstance(data_point, hmce.CustomDataPoint):
968
968
  self._custom_data_point = data_point
969
969
  if isinstance(data_point, GenericEvent):
@@ -977,12 +977,12 @@ class Channel(LogContextMixin, PayloadMixin):
977
977
  del self._calculated_data_points[data_point.dpk]
978
978
  if isinstance(data_point, GenericDataPoint):
979
979
  del self._generic_data_points[data_point.dpk]
980
- self._device.unregister_device_updated_callback(cb=data_point.fire_data_point_updated_callback)
980
+ self._device.unregister_device_updated_callback(cb=data_point.emit_data_point_updated_event)
981
981
  if isinstance(data_point, hmce.CustomDataPoint):
982
982
  self._custom_data_point = None
983
983
  if isinstance(data_point, GenericEvent):
984
984
  del self._generic_events[data_point.dpk]
985
- data_point.fire_device_removed_callback()
985
+ data_point.emit_device_removed_event()
986
986
 
987
987
  def remove(self) -> None:
988
988
  """Remove data points from collections and central."""
@@ -8,7 +8,7 @@ button presses, device errors, and impulse notifications to applications.
8
8
 
9
9
  Included classes:
10
10
  - GenericEvent: Base event that integrates with the common data point API
11
- (category, usage, names/paths, callbacks) and provides fire_event handling.
11
+ (category, usage, names/paths, callbacks) and provides emit_event handling.
12
12
  - ClickEvent: Represents key press events (EventType.KEYPRESS).
13
13
  - DeviceErrorEvent: Represents device error signaling with special value change
14
14
  semantics before emitting an event (EventType.DEVICE_ERROR).
@@ -102,14 +102,14 @@ class GenericEvent(BaseParameterDataPoint[Any, Any]):
102
102
  async def event(self, *, value: Any, received_at: datetime) -> None:
103
103
  """Handle event for which this handler has subscribed."""
104
104
  if self.event_type in DATA_POINT_EVENTS:
105
- self.fire_data_point_updated_callback()
105
+ self.emit_data_point_updated_event()
106
106
  self._set_modified_at(modified_at=received_at)
107
- self.fire_event(value=value)
107
+ self.emit_event(value=value)
108
108
 
109
109
  @loop_check
110
- def fire_event(self, *, value: Any) -> None:
111
- """Do what is needed to fire an event."""
112
- self._central.fire_homematic_callback(event_type=self.event_type, event_data=self.get_event_data(value=value))
110
+ def emit_event(self, *, value: Any) -> None:
111
+ """Do what is needed to emit an event."""
112
+ self._central.emit_homematic_callback(event_type=self.event_type, event_data=self.get_event_data(value=value))
113
113
 
114
114
  def _get_data_point_name(self) -> DataPointNameData:
115
115
  """Create the name for the data_point."""
@@ -149,7 +149,7 @@ class DeviceErrorEvent(GenericEvent):
149
149
  isinstance(new_value, int)
150
150
  and ((old_value is None and new_value > 0) or (isinstance(old_value, int) and old_value != new_value))
151
151
  ):
152
- self.fire_event(value=new_value)
152
+ self.emit_event(value=new_value)
153
153
 
154
154
 
155
155
  class ImpulseEvent(GenericEvent):
@@ -83,8 +83,8 @@ class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: Generi
83
83
  Parameter.UN_REACH,
84
84
  Parameter.STICKY_UN_REACH,
85
85
  ):
86
- self._device.fire_device_updated_callback()
87
- self._central.fire_homematic_callback(
86
+ self._device.emit_device_updated_callback()
87
+ self._central.emit_homematic_callback(
88
88
  event_type=EventType.DEVICE_AVAILABILITY,
89
89
  event_data=self.get_event_data(value=new_value),
90
90
  )
@@ -204,7 +204,7 @@ class Hub:
204
204
  new_programs.append(program_dp.switch)
205
205
 
206
206
  if new_programs:
207
- self._central.fire_backend_system_callback(
207
+ self._central.emit_backend_system_callback(
208
208
  system_event=BackendSystemEvent.HUB_REFRESHED,
209
209
  new_data_points=_get_new_hub_data_points(data_points=new_programs),
210
210
  )
@@ -240,7 +240,7 @@ class Hub:
240
240
  new_sysvars.append(self._create_system_variable(data=sysvar))
241
241
 
242
242
  if new_sysvars:
243
- self._central.fire_backend_system_callback(
243
+ self._central.emit_backend_system_callback(
244
244
  system_event=BackendSystemEvent.HUB_REFRESHED,
245
245
  new_data_points=_get_new_hub_data_points(data_points=new_sysvars),
246
246
  )
@@ -230,7 +230,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
230
230
  self._previous_value = old_value
231
231
  self._current_value = new_value
232
232
  self._state_uncertain = False
233
- self.fire_data_point_updated_callback()
233
+ self.emit_data_point_updated_event()
234
234
 
235
235
  def _write_temporary_value(self, *, value: Any, write_at: datetime) -> None:
236
236
  """Update the temporary value of the data_point."""
@@ -243,7 +243,7 @@ class GenericSysvarDataPoint(GenericHubDataPoint):
243
243
  self._set_temporary_modified_at(modified_at=write_at)
244
244
  self._temporary_value = temp_value
245
245
  self._state_uncertain = True
246
- self.fire_data_point_updated_callback()
246
+ self.emit_data_point_updated_event()
247
247
 
248
248
  def _convert_value(self, *, old_value: Any, new_value: Any) -> Any:
249
249
  """Convert to value to SYSVAR_TYPE."""
@@ -333,7 +333,7 @@ class GenericProgramDataPoint(GenericHubDataPoint):
333
333
  self._last_execute_time = data.last_execute_time
334
334
  do_update = True
335
335
  if do_update:
336
- self.fire_data_point_updated_callback()
336
+ self.emit_data_point_updated_event()
337
337
 
338
338
  def _get_path_data(self) -> PathData:
339
339
  """Return the path data of the data_point."""
@@ -117,7 +117,7 @@ class _GenericProperty[GETTER, SETTER](property):
117
117
  kind=self.kind,
118
118
  cached=self._cached,
119
119
  log_context=self.log_context,
120
- ) # pragma: no cover
120
+ )
121
121
 
122
122
  def setter(self, fset: Callable[[Any, SETTER], None], /) -> _GenericProperty:
123
123
  """Return generic setter."""
@@ -155,7 +155,7 @@ class _GenericProperty[GETTER, SETTER](property):
155
155
  return cast(GETTER, self)
156
156
 
157
157
  if (fget := self.fget) is None:
158
- raise AttributeError("unreadable attribute") # pragma: no cover
158
+ raise AttributeError("unreadable attribute")
159
159
 
160
160
  if not self._cached:
161
161
  return fget(instance)
@@ -194,7 +194,7 @@ class _GenericProperty[GETTER, SETTER](property):
194
194
  delattr(instance, self._cache_attr)
195
195
 
196
196
  if self.fset is None:
197
- raise AttributeError("can't set attribute") # pragma: no cover
197
+ raise AttributeError("can't set attribute")
198
198
  self.fset(instance, value)
199
199
 
200
200
  def __delete__(self, instance: Any, /) -> None:
@@ -210,7 +210,7 @@ class _GenericProperty[GETTER, SETTER](property):
210
210
  delattr(instance, self._cache_attr)
211
211
 
212
212
  if self.fdel is None:
213
- raise AttributeError("can't delete attribute") # pragma: no cover
213
+ raise AttributeError("can't delete attribute")
214
214
  self.fdel(instance)
215
215
 
216
216
 
@@ -392,36 +392,12 @@ class PingPongCache:
392
392
  return self._allowed_delta
393
393
 
394
394
  @property
395
- def high_pending_pongs(self) -> bool:
396
- """Check, if store contains too many pending pongs."""
397
- self._cleanup_pending_pongs()
398
- return len(self._pending_pongs) > self._allowed_delta
399
-
400
- @property
401
- def high_unknown_pongs(self) -> bool:
402
- """Check, if store contains too many unknown pongs."""
403
- self._cleanup_unknown_pongs()
404
- return len(self._unknown_pongs) > self._allowed_delta
405
-
406
- @property
407
- def low_pending_pongs(self) -> bool:
408
- """Return True when pending pong count is at or below the allowed delta (i.e., not high)."""
409
- self._cleanup_pending_pongs()
410
- return len(self._pending_pongs) <= self._allowed_delta
411
-
412
- @property
413
- def low_unknown_pongs(self) -> bool:
414
- """Return True when unknown pong count is at or below the allowed delta (i.e., not high)."""
415
- self._cleanup_unknown_pongs()
416
- return len(self._unknown_pongs) <= self._allowed_delta
417
-
418
- @property
419
- def pending_pong_count(self) -> int:
395
+ def _pending_pong_count(self) -> int:
420
396
  """Return the pending pong count."""
421
397
  return len(self._pending_pongs)
422
398
 
423
399
  @property
424
- def unknown_pong_count(self) -> int:
400
+ def _unknown_pong_count(self) -> int:
425
401
  """Return the unknown pong count."""
426
402
  return len(self._unknown_pongs)
427
403
 
@@ -435,18 +411,16 @@ class PingPongCache:
435
411
  def handle_send_ping(self, *, ping_ts: datetime) -> None:
436
412
  """Handle send ping timestamp."""
437
413
  self._pending_pongs.add(ping_ts)
414
+ self._cleanup_pending_pongs()
438
415
  # Throttle event emission to every second ping to avoid spamming callbacks,
439
416
  # but always emit when crossing the high threshold.
440
- count = self.pending_pong_count
417
+ count = self._pending_pong_count
441
418
  if (count > self._allowed_delta) or (count % 2 == 0):
442
- self._check_and_fire_pong_event(
443
- event_type=InterfaceEventType.PENDING_PONG,
444
- pong_mismatch_count=count,
445
- )
419
+ self._check_and_emit_pong_event(event_type=InterfaceEventType.PENDING_PONG)
446
420
  _LOGGER.debug(
447
421
  "PING PONG CACHE: Increase pending PING count: %s - %i for ts: %s",
448
422
  self._interface_id,
449
- self.pending_pong_count,
423
+ count,
450
424
  ping_ts,
451
425
  )
452
426
 
@@ -454,63 +428,61 @@ class PingPongCache:
454
428
  """Handle received pong timestamp."""
455
429
  if pong_ts in self._pending_pongs:
456
430
  self._pending_pongs.remove(pong_ts)
457
- self._check_and_fire_pong_event(
458
- event_type=InterfaceEventType.PENDING_PONG,
459
- pong_mismatch_count=self.pending_pong_count,
460
- )
431
+ self._cleanup_pending_pongs()
432
+ count = self._pending_pong_count
433
+ self._check_and_emit_pong_event(event_type=InterfaceEventType.PENDING_PONG)
461
434
  _LOGGER.debug(
462
435
  "PING PONG CACHE: Reduce pending PING count: %s - %i for ts: %s",
463
436
  self._interface_id,
464
- self.pending_pong_count,
437
+ count,
438
+ pong_ts,
439
+ )
440
+ else:
441
+ self._unknown_pongs.add(pong_ts)
442
+ self._cleanup_unknown_pongs()
443
+ count = self._unknown_pong_count
444
+ self._check_and_emit_pong_event(event_type=InterfaceEventType.UNKNOWN_PONG)
445
+ _LOGGER.debug(
446
+ "PING PONG CACHE: Increase unknown PONG count: %s - %i for ts: %s",
447
+ self._interface_id,
448
+ count,
465
449
  pong_ts,
466
450
  )
467
- return
468
-
469
- self._unknown_pongs.add(pong_ts)
470
- self._check_and_fire_pong_event(
471
- event_type=InterfaceEventType.UNKNOWN_PONG,
472
- pong_mismatch_count=self.unknown_pong_count,
473
- )
474
- _LOGGER.debug(
475
- "PING PONG CACHE: Increase unknown PONG count: %s - %i for ts: %s",
476
- self._interface_id,
477
- self.unknown_pong_count,
478
- pong_ts,
479
- )
480
451
 
481
452
  def _cleanup_pending_pongs(self) -> None:
482
453
  """Cleanup too old pending pongs."""
483
454
  dt_now = datetime.now()
484
- for pong_ts in list(self._pending_pongs):
455
+ for pp_pong_ts in list(self._pending_pongs):
485
456
  # Only expire entries that are actually older than the TTL.
486
- if (dt_now - pong_ts).total_seconds() > self._ttl:
487
- self._pending_pongs.remove(pong_ts)
457
+ if (dt_now - pp_pong_ts).total_seconds() > self._ttl:
458
+ self._pending_pongs.remove(pp_pong_ts)
488
459
  _LOGGER.debug(
489
460
  "PING PONG CACHE: Removing expired pending PONG: %s - %i for ts: %s",
490
461
  self._interface_id,
491
- self.pending_pong_count,
492
- pong_ts,
462
+ self._pending_pong_count,
463
+ pp_pong_ts,
493
464
  )
494
465
 
495
466
  def _cleanup_unknown_pongs(self) -> None:
496
467
  """Cleanup too old unknown pongs."""
497
468
  dt_now = datetime.now()
498
- for pong_ts in list(self._unknown_pongs):
469
+ for up_pong_ts in list(self._unknown_pongs):
499
470
  # Only expire entries that are actually older than the TTL.
500
- if (dt_now - pong_ts).total_seconds() > self._ttl:
501
- self._unknown_pongs.remove(pong_ts)
471
+ if (dt_now - up_pong_ts).total_seconds() > self._ttl:
472
+ self._unknown_pongs.remove(up_pong_ts)
502
473
  _LOGGER.debug(
503
474
  "PING PONG CACHE: Removing expired unknown PONG: %s - %i or ts: %s",
504
475
  self._interface_id,
505
- self.unknown_pong_count,
506
- pong_ts,
476
+ self._unknown_pong_count,
477
+ up_pong_ts,
507
478
  )
508
479
 
509
- def _check_and_fire_pong_event(self, *, event_type: InterfaceEventType, pong_mismatch_count: int) -> None:
510
- """Fire an event about the pong status."""
480
+ def _check_and_emit_pong_event(self, *, event_type: InterfaceEventType) -> None:
481
+ """Emit an event about the pong status."""
511
482
 
512
- def _fire_event(mismatch_count: int) -> None:
513
- self._central.fire_homematic_callback(
483
+ def _emit_event(mismatch_count: int) -> None:
484
+ """Emit event."""
485
+ self._central.emit_homematic_callback(
514
486
  event_type=EventType.INTERFACE,
515
487
  event_data=cast(
516
488
  dict[EventKey, Any],
@@ -520,52 +492,60 @@ class PingPongCache:
520
492
  EventKey.TYPE: event_type,
521
493
  EventKey.DATA: {
522
494
  EventKey.CENTRAL_NAME: self._central.name,
523
- EventKey.PONG_MISMATCH_ALLOWED: mismatch_count <= self._allowed_delta,
495
+ EventKey.PONG_MISMATCH_ACCEPTABLE: mismatch_count <= self._allowed_delta,
524
496
  EventKey.PONG_MISMATCH_COUNT: mismatch_count,
525
497
  },
526
498
  }
527
499
  ),
528
500
  ),
529
501
  )
502
+ _LOGGER.debug(
503
+ "PING PONG CACHE: Emitting event %s for %s with mismatch_count: %i with %i acceptable",
504
+ event_type,
505
+ self._interface_id,
506
+ mismatch_count,
507
+ self._allowed_delta,
508
+ )
530
509
 
531
- if self.low_pending_pongs and event_type == InterfaceEventType.PENDING_PONG:
510
+ if event_type == InterfaceEventType.PENDING_PONG:
511
+ self._cleanup_pending_pongs()
512
+ if (count := self._pending_pong_count) > self._allowed_delta:
513
+ # Emit interface event to inform subscribers about high pending pong count.
514
+ _emit_event(mismatch_count=count)
515
+ if self._pending_pong_logged is False:
516
+ _LOGGER.warning(
517
+ "Pending PONG mismatch: There is a mismatch between send ping events and received pong events for instance %s. "
518
+ "Possible reason 1: You are running multiple instances with the same instance name configured for this integration. "
519
+ "Re-add one instance! Otherwise this instance will not receive update events from your CCU. "
520
+ "Possible reason 2: Something is stuck on the CCU or hasn't been cleaned up. Therefore, try a CCU restart."
521
+ "Possible reason 3: Your setup is misconfigured and this instance is not able to receive events from the CCU.",
522
+ self._interface_id,
523
+ )
524
+ self._pending_pong_logged = True
532
525
  # In low state:
533
526
  # - If we previously logged a high state, emit a reset event (mismatch=0) exactly once.
534
527
  # - Otherwise, throttle emission to every second ping (even counts > 0) to avoid spamming.
535
- if self._pending_pong_logged:
536
- _fire_event(mismatch_count=0)
528
+ elif self._pending_pong_logged:
529
+ _emit_event(mismatch_count=0)
537
530
  self._pending_pong_logged = False
538
- return
539
- if pong_mismatch_count > 0 and pong_mismatch_count % 2 == 0:
540
- _fire_event(mismatch_count=pong_mismatch_count)
541
- return
542
-
543
- if self.low_unknown_pongs and event_type == InterfaceEventType.UNKNOWN_PONG:
544
- # For unknown pongs, only reset the logged flag when we drop below the threshold.
545
- # We do not emit an event here since there is no explicit expectation for a reset notification.
546
- self._unknown_pong_logged = False
547
- return
548
-
549
- if self.high_pending_pongs and event_type == InterfaceEventType.PENDING_PONG:
550
- _fire_event(mismatch_count=pong_mismatch_count)
551
- if self._pending_pong_logged is False:
552
- _LOGGER.warning(
553
- "Pending PONG mismatch: There is a mismatch between send ping events and received pong events for instance %s. "
554
- "Possible reason 1: You are running multiple instances with the same instance name configured for this integration. "
555
- "Re-add one instance! Otherwise this instance will not receive update events from your CCU. "
556
- "Possible reason 2: Something is stuck on the CCU or hasn't been cleaned up. Therefore, try a CCU restart."
557
- "Possible reason 3: Your setup is misconfigured and this instance is not able to receive events from the CCU.",
558
- self._interface_id,
559
- )
560
- self._pending_pong_logged = True
561
-
562
- if self.high_unknown_pongs and event_type == InterfaceEventType.UNKNOWN_PONG:
563
- if self._unknown_pong_logged is False:
564
- _LOGGER.warning(
565
- "Unknown PONG Mismatch: Your instance %s receives PONG events, that it hasn't send. "
566
- "Possible reason 1: You are running multiple instances with the same instance name configured for this integration. "
567
- "Re-add one instance! Otherwise the other instance will not receive update events from your CCU. "
568
- "Possible reason 2: Something is stuck on the CCU or hasn't been cleaned up. Therefore, try a CCU restart.",
569
- self._interface_id,
570
- )
571
- self._unknown_pong_logged = True
531
+ elif count > 0 and count % 2 == 0:
532
+ _emit_event(mismatch_count=count)
533
+ elif event_type == InterfaceEventType.UNKNOWN_PONG:
534
+ self._cleanup_unknown_pongs()
535
+ count = self._unknown_pong_count
536
+ if self._unknown_pong_count > self._allowed_delta:
537
+ # Emit interface event to inform subscribers about high unknown pong count.
538
+ _emit_event(mismatch_count=count)
539
+ if self._unknown_pong_logged is False:
540
+ _LOGGER.warning(
541
+ "Unknown PONG Mismatch: Your instance %s receives PONG events, that it hasn't send. "
542
+ "Possible reason 1: You are running multiple instances with the same instance name configured for this integration. "
543
+ "Re-add one instance! Otherwise the other instance will not receive update events from your CCU. "
544
+ "Possible reason 2: Something is stuck on the CCU or hasn't been cleaned up. Therefore, try a CCU restart.",
545
+ self._interface_id,
546
+ )
547
+ self._unknown_pong_logged = True
548
+ else:
549
+ # For unknown pongs, only reset the logged flag when we drop below the threshold.
550
+ # We do not emit an event here since there is no explicit expectation for a reset notification.
551
+ self._unknown_pong_logged = False
@@ -317,7 +317,7 @@ _IGNORE_PARAMETERS_BY_DEVICE: Final[Mapping[Parameter, frozenset[TModelName]]] =
317
317
  "HmIP-WGT",
318
318
  }
319
319
  ),
320
- Parameter.VALVE_STATE: frozenset({"HmIPW-FALMOT-C12", "HmIP-FALMOT-C12"}),
320
+ Parameter.VALVE_STATE: frozenset({"HmIP-FALMOT-C8", "HmIPW-FALMOT-C12", "HmIP-FALMOT-C12"}),
321
321
  }
322
322
 
323
323
  _IGNORE_PARAMETERS_BY_DEVICE_LOWER: Final[dict[TParameterName, frozenset[TModelName]]] = {
@@ -423,8 +423,7 @@ class ParameterVisibilityCache:
423
423
  self, *, model_l: TModelName, mapping: Mapping[str, object], cache_dict: dict[TModelName, str | None]
424
424
  ) -> str | None:
425
425
  """Resolve and memoize the first key in mapping that prefixes model_l."""
426
- dt_short_key = cache_dict.get(model_l)
427
- if dt_short_key is None and model_l not in cache_dict:
426
+ if (dt_short_key := cache_dict.get(model_l)) is None and model_l not in cache_dict:
428
427
  dt_short_key = next((k for k in mapping if model_l.startswith(k)), None)
429
428
  cache_dict[model_l] = dt_short_key
430
429
  return dt_short_key
@@ -486,7 +485,7 @@ class ParameterVisibilityCache:
486
485
  return False
487
486
 
488
487
  if parameter in self._custom_un_ignore_complex[model_l][channel.no][ParamsetKey.MASTER]:
489
- return False # pragma: no cover
488
+ return False
490
489
 
491
490
  dt_short_key = self._resolve_prefix_key(
492
491
  model_l=model_l,
@@ -545,7 +544,7 @@ class ParameterVisibilityCache:
545
544
  for ml, cno in search_matrix:
546
545
  if parameter in self._custom_un_ignore_complex[ml][cno][paramset_key]:
547
546
  self._param_un_ignored_cache[cache_key] = True
548
- return True # pragma: no cover
547
+ return True
549
548
 
550
549
  # check if parameter is in _UN_IGNORE_PARAMETERS_BY_DEVICE
551
550
  result = bool(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiohomematic
3
- Version: 2025.10.21
3
+ Version: 2025.10.24
4
4
  Summary: Homematic interface for Home Assistant running on Python 3.
5
5
  Home-page: https://github.com/sukramj/aiohomematic
6
6
  Author-email: SukramJ <sukramj@icloud.com>, Daniel Perna <danielperna84@gmail.com>
@@ -15,10 +15,16 @@ Classifier: Intended Audience :: End Users/Desktop
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
17
17
  Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
18
21
  Classifier: Programming Language :: Python :: 3.13
19
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: Implementation :: CPython
24
+ Classifier: Framework :: AsyncIO
25
+ Classifier: Typing :: Typed
20
26
  Classifier: Topic :: Home Automation
21
- Requires-Python: >=3.13.0
27
+ Requires-Python: >=3.13
22
28
  Description-Content-Type: text/markdown
23
29
  License-File: LICENSE
24
30
  Requires-Dist: aiohttp>=3.12.0