proconip 2.2.0__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.0 → proconip-2.3.0}/CHANGELOG.md +14 -0
- {proconip-2.2.0 → proconip-2.3.0}/PKG-INFO +5 -6
- {proconip-2.2.0 → proconip-2.3.0}/README.md +3 -4
- {proconip-2.2.0 → proconip-2.3.0}/pyproject.toml +3 -7
- {proconip-2.2.0 → proconip-2.3.0}/src/proconip/_version.py +2 -2
- {proconip-2.2.0 → proconip-2.3.0}/src/proconip/api.py +18 -6
- {proconip-2.2.0 → proconip-2.3.0}/src/proconip/definitions.py +104 -16
- {proconip-2.2.0 → proconip-2.3.0}/tests/conftest.py +17 -0
- proconip-2.3.0/tests/fixtures/get_state_electrolysis.csv +6 -0
- proconip-2.3.0/tests/test_api.py +548 -0
- {proconip-2.2.0 → proconip-2.3.0}/tests/test_definitions.py +45 -0
- proconip-2.2.0/tests/test_api.py +0 -431
- {proconip-2.2.0 → proconip-2.3.0}/.gitignore +0 -0
- {proconip-2.2.0 → proconip-2.3.0}/LICENSE +0 -0
- {proconip-2.2.0 → proconip-2.3.0}/src/proconip/__init__.py +0 -0
- {proconip-2.2.0 → proconip-2.3.0}/src/proconip/py.typed +0 -0
- {proconip-2.2.0 → proconip-2.3.0}/tests/fixtures/get_dmx.csv +0 -0
- {proconip-2.2.0 → proconip-2.3.0}/tests/fixtures/get_state.csv +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [2.2.1](https://github.com/ylabonte/proconip-pypi/compare/v2.2.0...v2.2.1) (2026-10-08)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* support aiohttp 3.14 without deprecation warnings; test against it ([#94](https://github.com/ylabonte/proconip-pypi/issues/94), [#97](https://github.com/ylabonte/proconip-pypi/issues/97)) ([#98](https://github.com/ylabonte/proconip-pypi/issues/98)) ([87dcc3d](https://github.com/ylabonte/proconip-pypi/commit/87dcc3d01ab2d5cae7fb826638086665f2076c7c))
|
|
16
|
+
|
|
3
17
|
## [2.2.0](https://github.com/ylabonte/proconip-pypi/compare/v2.1.2...v2.2.0) (2026-06-14)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
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/
|
|
@@ -176,8 +176,8 @@ async def manual_dosage_example():
|
|
|
176
176
|
client_session = aiohttp.ClientSession()
|
|
177
177
|
config = ConfigObject("http://192.168.2.3", "admin", "admin")
|
|
178
178
|
dosage_control = DosageControl(client_session, config)
|
|
179
|
-
await dosage_control.async_chlorine_dosage(3600)
|
|
180
|
-
await dosage_control.async_ph_minus_dosage(60)
|
|
179
|
+
await dosage_control.async_chlorine_dosage(3600) # start for 1 hour
|
|
180
|
+
await dosage_control.async_ph_minus_dosage(60) # start for 1 minute
|
|
181
181
|
await client_session.close()
|
|
182
182
|
|
|
183
183
|
|
|
@@ -201,13 +201,12 @@ async def dmx_example():
|
|
|
201
201
|
print(f"{channel.name} before: {channel.value}")
|
|
202
202
|
dmx_data.set(channel.index, (channel.value + 128) % 256)
|
|
203
203
|
print(f"{channel.name} after: {dmx_data.get_value(channel.index)}")
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
await dmx_control.async_set(dmx_data)
|
|
206
206
|
await client_session.close()
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
asyncio.run(dmx_example())
|
|
210
|
-
|
|
211
210
|
```
|
|
212
211
|
|
|
213
212
|
## A brief description of the ProCon.IP pool controller
|
|
@@ -148,8 +148,8 @@ async def manual_dosage_example():
|
|
|
148
148
|
client_session = aiohttp.ClientSession()
|
|
149
149
|
config = ConfigObject("http://192.168.2.3", "admin", "admin")
|
|
150
150
|
dosage_control = DosageControl(client_session, config)
|
|
151
|
-
await dosage_control.async_chlorine_dosage(3600)
|
|
152
|
-
await dosage_control.async_ph_minus_dosage(60)
|
|
151
|
+
await dosage_control.async_chlorine_dosage(3600) # start for 1 hour
|
|
152
|
+
await dosage_control.async_ph_minus_dosage(60) # start for 1 minute
|
|
153
153
|
await client_session.close()
|
|
154
154
|
|
|
155
155
|
|
|
@@ -173,13 +173,12 @@ async def dmx_example():
|
|
|
173
173
|
print(f"{channel.name} before: {channel.value}")
|
|
174
174
|
dmx_data.set(channel.index, (channel.value + 128) % 256)
|
|
175
175
|
print(f"{channel.name} after: {dmx_data.get_value(channel.index)}")
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
await dmx_control.async_set(dmx_data)
|
|
178
178
|
await client_session.close()
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
asyncio.run(dmx_example())
|
|
182
|
-
|
|
183
182
|
```
|
|
184
183
|
|
|
185
184
|
## A brief description of the ProCon.IP pool controller
|
|
@@ -37,15 +37,11 @@ test = [
|
|
|
37
37
|
"pytest",
|
|
38
38
|
"pytest-asyncio",
|
|
39
39
|
"pytest-cov",
|
|
40
|
-
"aioresponses",
|
|
41
|
-
# aioresponses 0.7.8 can't build mocked responses against aiohttp 3.14+
|
|
42
|
-
# (ClientResponse gained a required `stream_writer` kwarg), which breaks
|
|
43
|
-
# the whole test_api suite. Pin tests to the 3.13.x line HA Core ships.
|
|
44
|
-
# TODO: lift this cap once a compatible aioresponses release lands.
|
|
45
|
-
"aiohttp<3.14",
|
|
46
40
|
]
|
|
47
41
|
docs = [
|
|
48
|
-
|
|
42
|
+
# MkDocs 2.0 drops the plugin system and the theming that Material and
|
|
43
|
+
# mkdocstrings rely on, with no migration path — stay on 1.x.
|
|
44
|
+
"mkdocs<2",
|
|
49
45
|
"mkdocs-material",
|
|
50
46
|
"mkdocstrings[python]",
|
|
51
47
|
"mkdocs-include-markdown-plugin",
|
|
@@ -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
|
|
@@ -26,11 +26,11 @@ subclasses so callers can handle them uniformly.
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
import asyncio
|
|
29
|
+
import base64
|
|
29
30
|
import contextlib
|
|
30
31
|
import socket
|
|
31
32
|
|
|
32
33
|
from aiohttp import (
|
|
33
|
-
BasicAuth,
|
|
34
34
|
ClientError,
|
|
35
35
|
ClientResponse,
|
|
36
36
|
ClientSession,
|
|
@@ -84,6 +84,18 @@ class TimeoutException(ProconipApiException):
|
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def _auth_headers(config: ConfigObject) -> dict[str, str]:
|
|
88
|
+
"""Build the HTTP Basic ``Authorization`` header for the controller credentials.
|
|
89
|
+
|
|
90
|
+
Built by hand rather than via ``aiohttp.BasicAuth`` / ``auth=``, which
|
|
91
|
+
aiohttp 3.14 deprecates (removal in 4.0), while its replacement
|
|
92
|
+
``aiohttp.encode_basic_auth`` doesn't exist on older supported versions.
|
|
93
|
+
Credentials are latin-1 encoded, matching ``BasicAuth``'s default.
|
|
94
|
+
"""
|
|
95
|
+
credentials = f"{config.username}:{config.password}".encode("latin1")
|
|
96
|
+
return {"Authorization": "Basic " + base64.b64encode(credentials).decode("ascii")}
|
|
97
|
+
|
|
98
|
+
|
|
87
99
|
async def _handle_response(response: ClientResponse) -> str:
|
|
88
100
|
"""Validate the response and return its body, mapping HTTP errors to typed exceptions."""
|
|
89
101
|
if response.status in (401, 403):
|
|
@@ -130,10 +142,9 @@ async def async_get_raw_data(
|
|
|
130
142
|
ProconipApiException: For DNS failures, connection resets, and other
|
|
131
143
|
network-level errors.
|
|
132
144
|
"""
|
|
133
|
-
auth = BasicAuth(config.username, config.password)
|
|
134
145
|
try:
|
|
135
146
|
async with asyncio.timeout(timeout):
|
|
136
|
-
async with client_session.get(url,
|
|
147
|
+
async with client_session.get(url, headers=_auth_headers(config)) as response:
|
|
137
148
|
return await _handle_response(response)
|
|
138
149
|
except TimeoutError as exc:
|
|
139
150
|
raise TimeoutException("API request timed out") from exc
|
|
@@ -300,15 +311,16 @@ async def async_post_usrcfg_cgi(
|
|
|
300
311
|
ProconipApiException: For network-level errors.
|
|
301
312
|
"""
|
|
302
313
|
url = URL(config.base_url).with_path(API_PATH_USRCFG)
|
|
303
|
-
|
|
304
|
-
|
|
314
|
+
headers = {
|
|
315
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
316
|
+
**_auth_headers(config),
|
|
317
|
+
}
|
|
305
318
|
try:
|
|
306
319
|
async with asyncio.timeout(timeout):
|
|
307
320
|
async with client_session.post(
|
|
308
321
|
url=url,
|
|
309
322
|
headers=headers,
|
|
310
323
|
data=payload,
|
|
311
|
-
auth=auth,
|
|
312
324
|
) as response:
|
|
313
325
|
return await _handle_response(response)
|
|
314
326
|
except TimeoutError as exc:
|
|
@@ -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``.
|
|
@@ -585,7 +588,13 @@ class GetStateData:
|
|
|
585
588
|
@property
|
|
586
589
|
def config_other_enable(self) -> int:
|
|
587
590
|
"""Misc configuration flags. Use the `is_*_enabled` methods to query
|
|
588
|
-
individual bits (TCP/IP boost, SD card, DMX, …).
|
|
591
|
+
individual bits (TCP/IP boost, SD card, DMX, …).
|
|
592
|
+
|
|
593
|
+
Not every "Sonstiges" checkbox is mirrored here on current firmware:
|
|
594
|
+
on 1.7.6, bits 0 (TCP/IP boost), 1 (SD card) and 5 (high bus load)
|
|
595
|
+
were observed as 0 although those options were enabled, while bit 2
|
|
596
|
+
(DMX) tracked its checkbox exactly. Bits 3, 4, 6, 7 and 8 are
|
|
597
|
+
unverified on hardware."""
|
|
589
598
|
return self._config_other_enable
|
|
590
599
|
|
|
591
600
|
@property
|
|
@@ -614,7 +623,24 @@ class GetStateData:
|
|
|
614
623
|
return self._dosage_control & 1 == 1
|
|
615
624
|
|
|
616
625
|
def is_electrolysis_enabled(self) -> bool:
|
|
617
|
-
"""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
|
+
"""
|
|
618
644
|
return self._dosage_control & 16 == 16
|
|
619
645
|
|
|
620
646
|
def is_ph_minus_dosage_enabled(self) -> bool:
|
|
@@ -645,6 +671,33 @@ class GetStateData:
|
|
|
645
671
|
return self.is_ph_plus_dosage_enabled()
|
|
646
672
|
return False
|
|
647
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
|
+
|
|
648
701
|
def get_dosage_relay(self, data_entity: DataObject) -> int | None:
|
|
649
702
|
"""Aggregated relay ID that handles the dosing for this canister/consumption entity.
|
|
650
703
|
|
|
@@ -749,15 +802,28 @@ class GetStateData:
|
|
|
749
802
|
return NTP_FAULT_STATE[0]
|
|
750
803
|
|
|
751
804
|
def is_tcpip_boost_enabled(self) -> bool:
|
|
752
|
-
"""True if TCP/IP boost
|
|
805
|
+
"""True if bit 0 (TCP/IP boost) of `config_other_enable` is set.
|
|
806
|
+
|
|
807
|
+
Unreliable on current firmware: 1.7.6 was observed reporting this bit
|
|
808
|
+
as 0 while TCP/IP boost was enabled, so ``False`` doesn't prove the
|
|
809
|
+
feature is off.
|
|
810
|
+
"""
|
|
753
811
|
return self._config_other_enable & 1 == 1
|
|
754
812
|
|
|
755
813
|
def is_sd_card_enabled(self) -> bool:
|
|
756
|
-
"""True if SD card
|
|
814
|
+
"""True if bit 1 (SD card) of `config_other_enable` is set.
|
|
815
|
+
|
|
816
|
+
Unreliable on current firmware: 1.7.6 was observed reporting this bit
|
|
817
|
+
as 0 while the SD card was enabled, so ``False`` doesn't prove the
|
|
818
|
+
feature is off.
|
|
819
|
+
"""
|
|
757
820
|
return self._config_other_enable & 2 == 2
|
|
758
821
|
|
|
759
822
|
def is_dmx_enabled(self) -> bool:
|
|
760
|
-
"""True if DMX output is enabled in the controller config (bit 2).
|
|
823
|
+
"""True if DMX output is enabled in the controller config (bit 2).
|
|
824
|
+
|
|
825
|
+
Verified on firmware 1.7.6: the bit follows the "DMX512 aktiv" checkbox.
|
|
826
|
+
"""
|
|
761
827
|
return self._config_other_enable & 4 == 4
|
|
762
828
|
|
|
763
829
|
def is_avatar_enabled(self) -> bool:
|
|
@@ -774,7 +840,12 @@ class GetStateData:
|
|
|
774
840
|
return self._config_other_enable & 16 == 16
|
|
775
841
|
|
|
776
842
|
def is_high_bus_load_enabled(self) -> bool:
|
|
777
|
-
"""True if high bus load
|
|
843
|
+
"""True if bit 5 (high bus load, 1-Wire) of `config_other_enable` is set.
|
|
844
|
+
|
|
845
|
+
Unreliable on current firmware: 1.7.6 was observed reporting this bit
|
|
846
|
+
as 0 while high bus load was enabled, so ``False`` doesn't prove the
|
|
847
|
+
feature is off.
|
|
848
|
+
"""
|
|
778
849
|
return self._config_other_enable & 32 == 32
|
|
779
850
|
|
|
780
851
|
def is_flow_sensor_enabled(self) -> bool:
|
|
@@ -888,20 +959,28 @@ class GetStateData:
|
|
|
888
959
|
|
|
889
960
|
@property
|
|
890
961
|
def canister_objects(self) -> list[DataObject]:
|
|
891
|
-
"""The three canister
|
|
962
|
+
"""The three canister remaining-content readings (columns 36–38), in percent.
|
|
892
963
|
|
|
893
964
|
Order: chlorine, pH-, pH+. Convenience properties `chlorine_canister`,
|
|
894
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`.
|
|
895
970
|
"""
|
|
896
971
|
return self._canister_objects
|
|
897
972
|
|
|
898
973
|
@property
|
|
899
974
|
def consumption_objects(self) -> list[DataObject]:
|
|
900
|
-
"""The three dosage
|
|
975
|
+
"""The three daily dosage amounts (columns 39–41).
|
|
901
976
|
|
|
902
977
|
Order: chlorine, pH-, pH+. Convenience properties
|
|
903
978
|
`chlorine_consumption`, `ph_minus_consumption`, and
|
|
904
|
-
`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`.
|
|
905
984
|
"""
|
|
906
985
|
return self._consumption_objects
|
|
907
986
|
|
|
@@ -917,32 +996,41 @@ class GetStateData:
|
|
|
917
996
|
|
|
918
997
|
@property
|
|
919
998
|
def chlorine_canister(self) -> DataObject:
|
|
920
|
-
"""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
|
+
"""
|
|
921
1004
|
return self._canister_objects[0]
|
|
922
1005
|
|
|
923
1006
|
@property
|
|
924
1007
|
def ph_minus_canister(self) -> DataObject:
|
|
925
|
-
"""pH- canister
|
|
1008
|
+
"""pH- canister remaining content in percent (column 37)."""
|
|
926
1009
|
return self._canister_objects[1]
|
|
927
1010
|
|
|
928
1011
|
@property
|
|
929
1012
|
def ph_plus_canister(self) -> DataObject:
|
|
930
|
-
"""pH+ canister
|
|
1013
|
+
"""pH+ canister remaining content in percent (column 38)."""
|
|
931
1014
|
return self._canister_objects[2]
|
|
932
1015
|
|
|
933
1016
|
@property
|
|
934
1017
|
def chlorine_consumption(self) -> DataObject:
|
|
935
|
-
"""
|
|
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
|
+
"""
|
|
936
1024
|
return self._consumption_objects[0]
|
|
937
1025
|
|
|
938
1026
|
@property
|
|
939
1027
|
def ph_minus_consumption(self) -> DataObject:
|
|
940
|
-
"""
|
|
1028
|
+
"""pH- dosed today (column 40), in ``ml``."""
|
|
941
1029
|
return self._consumption_objects[1]
|
|
942
1030
|
|
|
943
1031
|
@property
|
|
944
1032
|
def ph_plus_consumption(self) -> DataObject:
|
|
945
|
-
"""
|
|
1033
|
+
"""pH+ dosed today (column 41), in ``ml``."""
|
|
946
1034
|
return self._consumption_objects[2]
|
|
947
1035
|
|
|
948
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
|