proconip 2.2.1__tar.gz → 2.3.0__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.
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.0](https://github.com/ylabonte/proconip-pypi/compare/v2.2.1...v2.3.0) (2026-10-12)
4
+
5
+
6
+ ### Features
7
+
8
+ * flag the chlorine canister columns as cell data under electrolysis ([#100](https://github.com/ylabonte/proconip-pypi/issues/100)) ([27ca22a](https://github.com/ylabonte/proconip-pypi/commit/27ca22acb2e42e47647318713670ddd9f3938daf))
9
+
3
10
  ## [2.2.1](https://github.com/ylabonte/proconip-pypi/compare/v2.2.0...v2.2.1) (2026-10-08)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: proconip
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: Async Python library for interacting with the ProCon.IP pool controller.
5
5
  Project-URL: Homepage, https://github.com/ylabonte/proconip-pypi
6
6
  Project-URL: Documentation, https://ylabonte.github.io/proconip-pypi/
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.2.1'
22
- __version_tuple__ = version_tuple = (2, 2, 1)
21
+ __version__ = version = '2.3.0'
22
+ __version_tuple__ = version_tuple = (2, 3, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -187,7 +187,10 @@ class DataObject:
187
187
  temperature, ``16–23`` → relay, ``24–27`` → digital input,
188
188
  ``28–35`` → external relay, ``36–38`` → canister, ``39–41`` →
189
189
  consumption. Anything outside this range falls into a sentinel
190
- "uncategorized" bucket.
190
+ "uncategorized" bucket. The category is purely positional: with
191
+ a salt-electrolysis cell, columns 36 and 39 still land in the
192
+ canister/consumption categories although they describe the
193
+ cell (see `GetStateData.is_electrolysis_object`).
191
194
  name: Sensor name as reported by the controller (e.g. ``"Redox"``).
192
195
  unit: Unit string (``"mV"``, ``"°C"``, ``"%"``, ``"--"``, …).
193
196
  offset: Calibration offset applied to ``value``.
@@ -620,7 +623,24 @@ class GetStateData:
620
623
  return self._dosage_control & 1 == 1
621
624
 
622
625
  def is_electrolysis_enabled(self) -> bool:
623
- """True if electrolysis (saltwater) chlorination is enabled (bit 4)."""
626
+ """True if the chlorine supply is a salt-electrolysis cell (bit 4).
627
+
628
+ When set, the chlorine channel is not fed from a canister: the
629
+ chlorine "canister" reading (column 36) is the cell's remaining
630
+ lifetime / maintenance interval in percent, and the chlorine
631
+ "consumption" reading (column 39) is today's *calculated* chlorine
632
+ production. Use `is_electrolysis_object` to tell those two objects
633
+ apart from real canister readings.
634
+
635
+ Polarity evidence: the TRM labels bit 4 "Cl liquid || Elektrolysis",
636
+ naming both options without saying which value is which. A live
637
+ firmware 1.7.6 controller configured for liquid chlorine
638
+ (``rdxcntrl.ini`` ``DTYPE=0``, chlorine dosing on) reports
639
+ DOSAGE_CNTRL 257 (bits 0 and 8, bit 4 clear), as does the real capture
640
+ in the test fixtures. So bit 4 is the chlorine device-type selector,
641
+ and set = electrolysis is inferred, not observed; a real electrolysis
642
+ capture is still pending.
643
+ """
624
644
  return self._dosage_control & 16 == 16
625
645
 
626
646
  def is_ph_minus_dosage_enabled(self) -> bool:
@@ -651,6 +671,33 @@ class GetStateData:
651
671
  return self.is_ph_plus_dosage_enabled()
652
672
  return False
653
673
 
674
+ def is_electrolysis_object(self, data_entity: DataObject) -> bool:
675
+ """Does this canister/consumption object describe a salt-electrolysis cell?
676
+
677
+ The controller reuses the chlorine canister columns for a salt
678
+ electrolysis setup instead of adding new ones. With electrolysis
679
+ enabled (see `is_electrolysis_enabled`):
680
+
681
+ - column 36 (`chlorine_canister`) is the cell's **remaining lifetime**
682
+ (or maintenance interval) in percent — the share of the configured
683
+ operating hours that is left, not a canister fill level;
684
+ - column 39 (`chlorine_consumption`) is **today's chlorine production**
685
+ calculated from the configured production rate, not a liquid
686
+ volume drawn from a canister.
687
+
688
+ Callers should label and present these objects accordingly (e.g. no
689
+ canister/volume wording). The pH- and pH+ columns are always real
690
+ canisters.
691
+
692
+ Args:
693
+ data_entity: Any `DataObject` from this state.
694
+
695
+ Returns:
696
+ True only for the chlorine canister (column 36) or chlorine
697
+ consumption (column 39) object while electrolysis is enabled.
698
+ """
699
+ return data_entity.column in (36, 39) and self.is_electrolysis_enabled()
700
+
654
701
  def get_dosage_relay(self, data_entity: DataObject) -> int | None:
655
702
  """Aggregated relay ID that handles the dosing for this canister/consumption entity.
656
703
 
@@ -912,20 +959,28 @@ class GetStateData:
912
959
 
913
960
  @property
914
961
  def canister_objects(self) -> list[DataObject]:
915
- """The three canister fill-level readings (columns 36–38).
962
+ """The three canister remaining-content readings (columns 36–38), in percent.
916
963
 
917
964
  Order: chlorine, pH-, pH+. Convenience properties `chlorine_canister`,
918
965
  `ph_minus_canister`, and `ph_plus_canister` return individual entries.
966
+ The values are estimates the controller derives from pump runtime and
967
+ configured delivery rate. With a salt-electrolysis cell the chlorine
968
+ entry is the cell's remaining lifetime instead — check
969
+ `is_electrolysis_object`.
919
970
  """
920
971
  return self._canister_objects
921
972
 
922
973
  @property
923
974
  def consumption_objects(self) -> list[DataObject]:
924
- """The three dosage consumption counters (columns 39–41).
975
+ """The three daily dosage amounts (columns 39–41).
925
976
 
926
977
  Order: chlorine, pH-, pH+. Convenience properties
927
978
  `chlorine_consumption`, `ph_minus_consumption`, and
928
- `ph_plus_consumption` return individual entries.
979
+ `ph_plus_consumption` return individual entries. Each is the amount
980
+ dosed *today* (the controller resets it daily), in the unit the
981
+ controller reports (``ml``; the chlorine column may report ``g``). With a
982
+ salt-electrolysis cell the chlorine entry is today's calculated
983
+ chlorine production — check `is_electrolysis_object`.
929
984
  """
930
985
  return self._consumption_objects
931
986
 
@@ -941,32 +996,41 @@ class GetStateData:
941
996
 
942
997
  @property
943
998
  def chlorine_canister(self) -> DataObject:
944
- """Chlorine canister fill level (column 36)."""
999
+ """Chlorine canister remaining content in percent (column 36).
1000
+
1001
+ With a salt-electrolysis cell (`is_electrolysis_enabled`) this is the
1002
+ cell's remaining lifetime / maintenance interval in percent instead.
1003
+ """
945
1004
  return self._canister_objects[0]
946
1005
 
947
1006
  @property
948
1007
  def ph_minus_canister(self) -> DataObject:
949
- """pH- canister fill level (column 37)."""
1008
+ """pH- canister remaining content in percent (column 37)."""
950
1009
  return self._canister_objects[1]
951
1010
 
952
1011
  @property
953
1012
  def ph_plus_canister(self) -> DataObject:
954
- """pH+ canister fill level (column 38)."""
1013
+ """pH+ canister remaining content in percent (column 38)."""
955
1014
  return self._canister_objects[2]
956
1015
 
957
1016
  @property
958
1017
  def chlorine_consumption(self) -> DataObject:
959
- """Cumulative chlorine consumption counter (column 39)."""
1018
+ """Chlorine dosed today (column 39), in ``ml`` or ``g``.
1019
+
1020
+ With a salt-electrolysis cell (`is_electrolysis_enabled`) this is
1021
+ today's chlorine production calculated from the configured
1022
+ production rate instead.
1023
+ """
960
1024
  return self._consumption_objects[0]
961
1025
 
962
1026
  @property
963
1027
  def ph_minus_consumption(self) -> DataObject:
964
- """Cumulative pH- consumption counter (column 40)."""
1028
+ """pH- dosed today (column 40), in ``ml``."""
965
1029
  return self._consumption_objects[1]
966
1030
 
967
1031
  @property
968
1032
  def ph_plus_consumption(self) -> DataObject:
969
- """Cumulative pH+ consumption counter (column 41)."""
1033
+ """pH+ dosed today (column 41), in ``ml``."""
970
1034
  return self._consumption_objects[2]
971
1035
 
972
1036
  @property
@@ -36,3 +36,20 @@ def get_state_data(get_state_csv: str) -> GetStateData:
36
36
  @pytest.fixture
37
37
  def get_dmx_data(get_dmx_csv: str) -> GetDmxData:
38
38
  return GetDmxData(get_dmx_csv)
39
+
40
+
41
+ @pytest.fixture
42
+ def get_state_electrolysis_csv() -> str:
43
+ """`get_state.csv` reconfigured for a salt-electrolysis chlorine supply.
44
+
45
+ Derived from the real capture: DOSAGE_CNTRL 273 (Cl enabled + bit 4
46
+ electrolysis + pH- enabled), chlorine daily amount in ``g``, and non-zero
47
+ values in the chlorine "canister" (cell lifetime %) and "consumption"
48
+ (calculated production) columns, per the vendor's TRM column table.
49
+ """
50
+ return (FIXTURES_DIR / "get_state_electrolysis.csv").read_text()
51
+
52
+
53
+ @pytest.fixture
54
+ def get_state_electrolysis_data(get_state_electrolysis_csv: str) -> GetStateData:
55
+ return GetStateData(get_state_electrolysis_csv)
@@ -0,0 +1,6 @@
1
+ SYSINFO,1.7.3,9559698,1,3,0,273,4,4,5
2
+ Time,n.a.,n.a.,n.a.,n.a.,CPU Temp,Redox,pH,Pumpe,n.a.,n.a.,n.a.,n.a.,n.a.,n.a.,n.a.,Terassenlicht,n.a.,n.a.,Gartenlicht,pH minus,Chlor,Bachlauf,Poolpumpe,TASTER1,TASTER2,TASTER3,TASTER4,n.a.,n.a.,n.a.,n.a.,n.a.,n.a.,n.a.,n.a.,Cl Rest,pH- Rest,pH+ Rest,Cl consumption,pH- consumption,pH+ consumption
3
+ h,mV,mV,Bar,ppm,C,mV,pH,C,C,C,C,C,C,C,C,--,--,--,--,--,--,--,--,cm/s,--,--,--,--,--,--,--,--,--,--,--,%,%,%,g,ml,ml
4
+ 0,0,0,-0.400,6.00000,147.5,0.0,0.0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
5
+ 1,0.0625,0.0625,0.0000125,0.000156,-0.00468750,0.0625,0.0078125,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0.1,0.1,0.1,1,1,1
6
+ 529,-44,-44,-44,-44,24878,14000,932,111,0,0,0,0,0,0,0,2,0,0,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,873,615,1000,412,0,0
@@ -266,6 +266,51 @@ def test_get_dosage_relay(get_state_data: GetStateData) -> None:
266
266
  assert get_state_data.get_dosage_relay(get_state_data.analog_objects[0]) is None
267
267
 
268
268
 
269
+ def test_is_electrolysis_object_false_for_liquid_chlorine(get_state_data: GetStateData) -> None:
270
+ assert not get_state_data.is_electrolysis_enabled()
271
+ for obj in get_state_data.canister_objects + get_state_data.consumption_objects:
272
+ assert not get_state_data.is_electrolysis_object(obj)
273
+
274
+
275
+ def test_is_electrolysis_object_marks_chlorine_columns_only(
276
+ get_state_electrolysis_data: GetStateData,
277
+ ) -> None:
278
+ state = get_state_electrolysis_data
279
+ assert state.is_electrolysis_enabled()
280
+ assert state.is_chlorine_dosage_enabled()
281
+ assert state.is_electrolysis_object(state.chlorine_canister)
282
+ assert state.is_electrolysis_object(state.chlorine_consumption)
283
+ # pH canisters stay real canisters
284
+ for obj in (
285
+ state.ph_minus_canister,
286
+ state.ph_plus_canister,
287
+ state.ph_minus_consumption,
288
+ state.ph_plus_consumption,
289
+ ):
290
+ assert not state.is_electrolysis_object(obj)
291
+ # unrelated columns never qualify
292
+ assert not state.is_electrolysis_object(state.redox_electrode)
293
+ assert not state.is_electrolysis_object(state.analog_objects[0])
294
+
295
+
296
+ def test_electrolysis_keeps_chlorine_columns_positional(
297
+ get_state_electrolysis_data: GetStateData,
298
+ ) -> None:
299
+ """Under electrolysis, columns 36/39 stay in the canister/consumption categories.
300
+
301
+ The ``%``/``g`` units and values below come from the synthesised
302
+ electrolysis fixture, not from observed controller output; the category
303
+ asserts are what this test pins.
304
+ """
305
+ state = get_state_electrolysis_data
306
+ assert state.chlorine_canister.unit == "%"
307
+ assert state.chlorine_canister.value == pytest.approx(87.3)
308
+ assert state.chlorine_canister.category == CATEGORY_CANISTER
309
+ assert state.chlorine_consumption.unit == "g"
310
+ assert state.chlorine_consumption.value == pytest.approx(412)
311
+ assert state.chlorine_consumption.category == CATEGORY_CONSUMPTION
312
+
313
+
269
314
  def test_is_dosage_relay_by_relay_object(get_state_data: GetStateData) -> None:
270
315
  dosage_relay = get_state_data.get_relay(5) # chlorine dosage relay_id
271
316
  assert get_state_data.is_dosage_relay(relay_object=dosage_relay)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes