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.
- {proconip-2.2.1 → proconip-2.3.0}/CHANGELOG.md +7 -0
- {proconip-2.2.1 → proconip-2.3.0}/PKG-INFO +1 -1
- {proconip-2.2.1 → proconip-2.3.0}/src/proconip/_version.py +2 -2
- {proconip-2.2.1 → proconip-2.3.0}/src/proconip/definitions.py +75 -11
- {proconip-2.2.1 → proconip-2.3.0}/tests/conftest.py +17 -0
- proconip-2.3.0/tests/fixtures/get_state_electrolysis.csv +6 -0
- {proconip-2.2.1 → proconip-2.3.0}/tests/test_definitions.py +45 -0
- {proconip-2.2.1 → proconip-2.3.0}/.gitignore +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/LICENSE +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/README.md +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/pyproject.toml +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/src/proconip/__init__.py +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/src/proconip/api.py +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/src/proconip/py.typed +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/tests/fixtures/get_dmx.csv +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/tests/fixtures/get_state.csv +0 -0
- {proconip-2.2.1 → proconip-2.3.0}/tests/test_api.py +0 -0
|
@@ -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.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|