ramses-rf 0.51.8__py3-none-any.whl → 0.52.0__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.
ramses_rf/device/heat.py CHANGED
@@ -144,7 +144,7 @@ class Actuator(DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
144
144
  if self._gwy.config.disable_discovery:
145
145
  return
146
146
 
147
- # TODO: why are we doing this here? Should simply use dscovery poller!
147
+ # TODO: why are we doing this here? Should simply use discovery poller!
148
148
  if msg.code == Code._3EF0 and msg.verb == I_ and not self.is_faked:
149
149
  # lf._send_cmd(Command.get_relay_demand(self.id), qos=QOS_LOW)
150
150
  self._send_cmd(
@@ -429,7 +429,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
429
429
  def _handle_msg(self, msg: Message) -> None:
430
430
  super()._handle_msg(msg)
431
431
 
432
- # Several assumptions ar emade, regarding 000C pkts:
432
+ # Several assumptions are made, regarding 000C pkts:
433
433
  # - UFC bound only to CTL (not, e.g. SEN)
434
434
  # - all circuits bound to the same controller
435
435
 
@@ -450,7 +450,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
450
450
  # )
451
451
  # self._send_cmd(cmd)
452
452
 
453
- elif msg.code == Code._0008: # relay_demand, TODO: use msg DB?
453
+ elif msg.code == Code._0008: # relay_demand, TODO: use msgIndex DB?
454
454
  if msg.payload.get(SZ_DOMAIN_ID) == FC:
455
455
  self._relay_demand = msg
456
456
  else: # FA
@@ -491,7 +491,6 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
491
491
 
492
492
  # elif msg.code not in (Code._10E0, Code._22D0):
493
493
  # print("xxx")
494
-
495
494
  # "0008|FA/FC", "22C9|array", "22D0|none", "3150|ZZ/array(/FC?)"
496
495
 
497
496
  # TODO: should be a private method
@@ -635,7 +634,7 @@ def _to_msg_id(data_id: OtDataId) -> MsgId:
635
634
  return f"{data_id:02X}"
636
635
 
637
636
 
638
- # NOTE: config.use_native_ot should enforces sends, but not reads from _msgz DB
637
+ # NOTE: config.use_native_ot should enforce sends, but not reads from _msgz DB
639
638
  class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
640
639
  """The OTB class, specifically an OpenTherm Bridge (R8810A Bridge)."""
641
640
 
@@ -668,7 +667,16 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
668
667
 
669
668
  self._child_id = FC # NOTE: domain_id
670
669
 
671
- self._msgz[Code._3220] = {RP: {}} # _msgz[Code._3220][RP][msg_id]
670
+ # TODO(eb): cleanup
671
+ # should fix src/ramses_rf/database.py _add_record try/except when activating next line
672
+ if self._gwy.msg_db:
673
+ self._add_record(
674
+ address=self.addr, code=Code._3220, verb="RP"
675
+ ) # << essential?
676
+ # adds a "sim" RP opentherm_msg to the SQLite MessageIndex with code _3220
677
+ # causes exc when fetching ALL, when no "real" msg was added to _msgs_. We skip those.
678
+ else:
679
+ self._msgz[Code._3220] = {RP: {}} # No ctx! (not None)
672
680
 
673
681
  # lf._use_ot = self._gwy.config.use_native_ot
674
682
  self._msgs_ot: dict[MsgId, Message] = {}
@@ -758,7 +766,7 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
758
766
  if msg.payload.get(SZ_VALUE) is None:
759
767
  return
760
768
 
761
- # msg_id is int in msg payload/opentherm.py, but MsgId (str) is in this module
769
+ # msg_id is int in msg payload/opentherm.py, but MsgId (str) in this module
762
770
  msg_id = _to_msg_id(msg.payload[SZ_MSG_ID])
763
771
  self._msgs_ot[msg_id] = msg
764
772
 
ramses_rf/device/hvac.py CHANGED
@@ -426,7 +426,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
426
426
  Also handles 2411 parameter messages for configuration.
427
427
  Since 2411 is not supported by all vendors, discovery is used to determine if it is supported.
428
428
  Since more than 1 different parameters can be sent on 2411 messages,
429
- we will process these in the dedicated _handle_2411_message method.
429
+ we process these in the dedicated _handle_2411_message method.
430
430
  """
431
431
 
432
432
  # Itho Daalderop (NL)
@@ -446,6 +446,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
446
446
  """
447
447
  super().__init__(*args, **kwargs)
448
448
  self._supports_2411 = False # Flag for 2411 parameter support
449
+ self._params_2411: dict[str, float] = {} # Store 2411 parameters here
449
450
  self._initialized_callback = None # Called when device is fully initialized
450
451
  self._param_update_callback = None # Called when 2411 parameters are updated
451
452
  self._hgi: Any | None = None # Will be set when HGI is available
@@ -517,7 +518,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
517
518
  :param param_id: The ID of the parameter that was updated
518
519
  :type param_id: str
519
520
  :param value: The new value of the parameter
520
- :type value: Any
521
+ :type value: float
521
522
  """
522
523
  if callable(self._param_update_callback):
523
524
  try:
@@ -541,12 +542,62 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
541
542
  The HGI device provides additional functionality for certain operations.
542
543
 
543
544
  :return: The HGI device instance, or None if not available
544
- :rtype: Any | None
545
+ :rtype: float | None
545
546
  """
546
547
  if self._hgi is None and self._gwy and hasattr(self._gwy, "hgi"):
547
548
  self._hgi = self._gwy.hgi
548
549
  return self._hgi
549
550
 
551
+ def get_2411_param(self, param_id: str) -> float | None:
552
+ """Get a 2411 parameter value.
553
+
554
+ :param param_id: The parameter ID to retrieve.
555
+ :type param_id: str
556
+ :return: The parameter value if found, None otherwise
557
+ :rtype: float | None
558
+ """
559
+ return self._params_2411.get(param_id)
560
+
561
+ def set_2411_param(self, param_id: str, value: float) -> bool:
562
+ """Set a 2411 parameter value.
563
+
564
+ :param param_id: The parameter ID to retrieve.
565
+ :type param_id: str
566
+ :param value: The parameter value to set.
567
+ :type value: float
568
+ :return: True if the parameter was set, False otherwise
569
+ :rtype: bool
570
+ """
571
+ if not self._supports_2411:
572
+ _LOGGER.warning("Device %s doesn't support 2411 parameters", self.id)
573
+ return False
574
+
575
+ self._params_2411[param_id] = value
576
+ return True
577
+
578
+ def get_fan_param(self, param_id: str) -> Any | None:
579
+ """Retrieve a fan parameter value from the device's message store.
580
+
581
+ This wrapper method gets a specific parameter value for a FAN device stored in
582
+ _params_2411 dict. It first makes sure we use the proper param_id format
583
+
584
+ :param param_id: The parameter ID to retrieve.
585
+ :type param_id: str
586
+ :return: The parameter value if found, None otherwise
587
+ :rtype: float | None
588
+ """
589
+ # Ensure param_id is uppercase and strip leading zeros for consistency
590
+ param_id = (
591
+ str(param_id).upper().lstrip("0") or "0"
592
+ ) # Handle case where param_id is "0"
593
+
594
+ param_value = self.get_2411_param(param_id)
595
+ if param_value is not None:
596
+ return param_value
597
+ else:
598
+ _LOGGER.debug("Parameter %s not found for %s", param_id, self.id)
599
+ return None
600
+
550
601
  def _handle_2411_message(self, msg: Message) -> None:
551
602
  """Handle incoming 2411 parameter messages.
552
603
 
@@ -555,7 +606,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
555
606
  It handles parameter value normalization and validation.
556
607
 
557
608
  :param msg: The incoming 2411 message
558
- :type msg: Message
609
+ :type msg: Message to process
559
610
  """
560
611
  if not hasattr(msg, "payload") or not isinstance(msg.payload, dict):
561
612
  _LOGGER.debug("Invalid 2411 message format: %s", msg)
@@ -568,33 +619,30 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
568
619
  _LOGGER.debug("Missing parameter ID or value in 2411 message: %s", msg)
569
620
  return
570
621
 
571
- # Create a composite key for this parameter using the normalized ID
572
- key = f"{Code._2411}_{param_id}"
622
+ # Mark that we support 2411 parameters
623
+ if not self._supports_2411:
624
+ self._supports_2411 = True
625
+ _LOGGER.debug("Device %s supports 2411 parameters", self.id)
573
626
 
574
- # Store the message in the device's message store
575
- old_value = self._msgs.get(Code._2411)
576
- # Use direct assignment for Code._2411 key
577
- self._msgs[Code._2411] = msg
578
- # For the composite key, we need to bypass type checking
579
- self._msgs[key] = msg # type: ignore[index]
627
+ # Normalize the value if needed
628
+ if param_id == "75" and isinstance(param_value, (int, float)):
629
+ param_value = round(float(param_value), 1)
630
+ elif param_id in ("52", "95"): # Percentage parameters
631
+ param_value = round(float(param_value), 3) # Keep precision for percentages
580
632
 
633
+ # Store in params
634
+ old_value = self.get_2411_param(param_id)
635
+ self.set_2411_param(param_id, param_value)
636
+
637
+ # Log the update
581
638
  _LOGGER.debug(
582
- "Updated 2411 parameter %s = %s (was: %s) for %s",
639
+ "Updated 2411 parameter %s: %s (was: %s) for %s",
583
640
  param_id,
584
641
  param_value,
585
- old_value.payload if old_value else None,
642
+ old_value,
586
643
  self.id,
587
644
  )
588
645
 
589
- # Mark that we support 2411 parameters
590
- if not self._supports_2411:
591
- self._supports_2411 = True
592
- _LOGGER.debug("Device %s supports 2411 parameters", self.id)
593
-
594
- # Round parameter 75 values to 1 decimal place
595
- if param_id == "75" and isinstance(param_value, int | float):
596
- param_value = round(float(param_value), 1)
597
-
598
646
  # call the 2411 parameter update callback
599
647
  self._handle_param_update(param_id, param_value)
600
648
 
@@ -605,7 +653,7 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
605
653
  handling for 2411 parameter messages. It updates the device state and
606
654
  triggers any necessary callbacks.
607
655
 
608
- After handling the messages, it calls the initialized callback if set to notify that
656
+ After handling the messages, it calls the initialized callback - if set - to notify that
609
657
  the device was fully initialized.
610
658
 
611
659
  :param msg: The incoming message to process
@@ -749,72 +797,6 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
749
797
  _LOGGER.debug("No bound REM or DIS devices found for FAN %s", self.id)
750
798
  return None
751
799
 
752
- def get_fan_param(self, param_id: str) -> Any | None:
753
- """Retrieve a fan parameter value from the device's message store.
754
-
755
- This method attempts to fetch a specific parameter value for a FAN device from the
756
- stored messages. It first looks for the parameter using a composite key (e.g., '2411_3F')
757
- and falls back to checking the general 2411 message if needed.
758
-
759
- :param param_id: The parameter ID to retrieve.
760
- :type param_id: str
761
- :return: The parameter value if found, None otherwise
762
- :rtype: Any | None
763
- """
764
- # Ensure param_id is uppercase and strip leading zeros for consistency
765
- param_id = (
766
- str(param_id).upper().lstrip("0") or "0"
767
- ) # Handle case where param_id is "0"
768
- # we need some extra workarounds to please mypy
769
- # Create a composite key for this parameter using the normalized ID
770
- key = f"{Code._2411}_{param_id}"
771
-
772
- # Get the message using the composite key first, fall back to just the code
773
- msg = None
774
-
775
- # First try to get the specific parameter message
776
- try:
777
- # Try to access the message directly using the key
778
- msg = self._msgs[key] # type: ignore[index]
779
- except (KeyError, TypeError):
780
- # If that fails, try to find the message by iterating through the dictionary
781
- msg = next((v for k, v in self._msgs.items() if str(k) == key), None)
782
-
783
- # If not found, try to get the general 2411 message
784
- if msg is None:
785
- msg = self._msgs.get(Code._2411)
786
-
787
- if not msg or not hasattr(msg, "payload"):
788
- if not self.supports_2411:
789
- _LOGGER.debug(
790
- "Cannot get parameter %s from %s: 2411 parameters not supported",
791
- param_id,
792
- self.id,
793
- )
794
- else:
795
- _LOGGER.debug(
796
- "No payload found for parameter %s on %s", param_id, self.id
797
- )
798
- return None
799
-
800
- # If we have a message but not the specific parameter, try to get it from the payload
801
- if param_id and hasattr(msg.payload, "get"):
802
- value = msg.payload.get("value")
803
- if value is not None:
804
- return value
805
-
806
- # If we get here, the parameter wasn't found in the message
807
- if not self.supports_2411:
808
- _LOGGER.debug(
809
- "Parameter %s not found for %s: 2411 parameters not supported",
810
- param_id,
811
- self.id,
812
- )
813
- else:
814
- _LOGGER.debug("Parameter %s not found in payload for %s", param_id, self.id)
815
-
816
- return None
817
-
818
800
  @property
819
801
  def air_quality(self) -> float | None:
820
802
  """Return the current air quality measurement.
@@ -929,14 +911,38 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
929
911
  @property
930
912
  def fan_info(self) -> str | None:
931
913
  """
932
- Extract fan info description from _31D9 or _31DA message payload, e.g. "speed 2, medium".
914
+ Extract fan info description from MessageIndex _31D9 or _31DA payload,
915
+ e.g. "speed 2, medium".
933
916
  By its name, the result is picked up by a sensor in HA Climate UI.
934
917
  Some manufacturers (Orcon, Vasco) include the fan mode (auto, manual), others don't (Itho).
935
918
 
936
- :return: a string describing fan mode, speed
937
- """
919
+ :return: string describing fan mode, speed
920
+ """
921
+ if self._gwy.msg_db:
922
+ # Use SQLite query on MessageIndex. res_rate/res_mode not exposed yet
923
+ sql = f"""
924
+ SELECT code from messages WHERE verb in (' I', 'RP')
925
+ AND (src = ? OR dst = ?)
926
+ AND (plk LIKE '%{SZ_FAN_MODE}%')
927
+ """
928
+ res_mode: list = self._msg_qry(sql)
929
+ # SQLite query on MessageIndex
930
+ _LOGGER.info(f"{res_mode} # FAN_MODE FETCHED from MessageIndex")
931
+
932
+ sql = f"""
933
+ SELECT code from messages WHERE verb in (' I', 'RP')
934
+ AND (src = ? OR dst = ?)
935
+ AND (plk LIKE '%{SZ_FAN_RATE}%')
936
+ """
937
+ res_rate: list = self._msg_qry(sql)
938
+ # SQLite query on MessageIndex
939
+ _LOGGER.info(
940
+ f"{res_rate} # FAN_RATE FETCHED from MessageIndex"
941
+ ) # DEBUG always empty?
942
+
938
943
  if Code._31D9 in self._msgs:
939
- # Itho, Vasco D60 and ClimaRad (MiniBox fan) send mode/speed in _31D9
944
+ # was a dict by Code
945
+ # Itho, Vasco D60 and ClimaRad MiniBox fan send mode/speed in _31D9
940
946
  v: str
941
947
  for k, v in self._msgs[Code._31D9].payload.items():
942
948
  if k == SZ_FAN_MODE and len(v) > 2: # prevent non-lookups to pass
ramses_rf/dispatcher.py CHANGED
@@ -70,12 +70,12 @@ def _create_devices_from_addrs(gwy: Gateway, this: Message) -> None:
70
70
  # Devices need to know their controller, ?and their location ('parent' domain)
71
71
  # NB: only addrs processed here, packet metadata is processed elsewhere
72
72
 
73
- # Determinging bindings to a controller:
73
+ # Determining bindings to a controller:
74
74
  # - configury; As per any schema # codespell:ignore configury
75
75
  # - discovery: If in 000C pkt, or pkt *to* device where src is a controller
76
76
  # - eavesdrop: If pkt *from* device where dst is a controller
77
77
 
78
- # Determinging location in a schema (domain/DHW/zone):
78
+ # Determining location in a schema (domain/DHW/zone):
79
79
  # - configury; As per any schema # codespell:ignore configury
80
80
  # - discovery: If in 000C pkt - unable for 10: & 00: (TRVs)
81
81
  # - discovery: from packet fingerprint, excl. payloads (only for 10:)
@@ -99,7 +99,7 @@ def _create_devices_from_addrs(gwy: Gateway, this: Message) -> None:
99
99
  def _check_msg_addrs(msg: Message) -> None: # TODO
100
100
  """Validate the packet's address set.
101
101
 
102
- Raise InvalidAddrSetError if the meta data is invalid, otherwise simply return.
102
+ Raise InvalidAddrSetError if the metadata is invalid, otherwise simply return.
103
103
  """
104
104
 
105
105
  # TODO: needs work: doesn't take into account device's (non-HVAC) class
@@ -218,7 +218,7 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
218
218
  and msg.dst != msg.src
219
219
  ):
220
220
  # HGI80 can do what it likes
221
- # receiving an I isn't currently in the schema & so can't yet be tested
221
+ # receiving an I_ isn't currently in the schema & so can't yet be tested
222
222
  _check_dst_slug(msg) # ? raise exc.PacketInvalid
223
223
 
224
224
  if gwy.config.reduce_processing >= DONT_UPDATE_ENTITIES:
@@ -268,8 +268,10 @@ def process_msg(gwy: Gateway, msg: Message) -> None:
268
268
 
269
269
  else:
270
270
  logger_xxxx(msg)
271
- if gwy.msg_db:
272
- gwy.msg_db.add(msg)
271
+ # if gwy.msg_db:
272
+ # gwy.msg_db.add(
273
+ # msg
274
+ # ) # why add it? passes all tests without
273
275
 
274
276
 
275
277
  # TODO: this needs cleaning up (e.g. handle intervening packet)