ips-api-client 0.1.1__tar.gz → 0.2.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.
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/CHANGELOG.md +8 -0
- {ips_api_client-0.1.1/ips_api_client.egg-info → ips_api_client-0.2.0}/PKG-INFO +12 -9
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/models.py +6 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/parser.py +29 -1
- {ips_api_client-0.1.1 → ips_api_client-0.2.0/ips_api_client.egg-info}/PKG-INFO +12 -9
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/SOURCES.txt +3 -1
- ips_api_client-0.2.0/pyproject.toml +41 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/setup.py +1 -1
- ips_api_client-0.2.0/tests/test_parser.py +70 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/LICENSE +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/MANIFEST.in +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/README.md +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/__init__.py +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/client.py +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/const.py +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/exceptions.py +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/dependency_links.txt +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/requires.txt +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/top_level.txt +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/requirements.txt +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/setup.cfg +0 -0
- {ips_api_client-0.1.1 → ips_api_client-0.2.0}/tests/test_client.py +0 -0
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-07-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Parse the ORP live state string (`lblorpState`) into `PoolReading.orp_state`.
|
|
12
|
+
- Detect the live overfeed lockout condition per channel via
|
|
13
|
+
`PoolReading.ph_overfeed` / `orp_overfeed` (tri-state: True/False/None) and the
|
|
14
|
+
`state_is_overfeed()` helper.
|
|
15
|
+
|
|
8
16
|
## [0.1.0] - 2025-10-21
|
|
9
17
|
|
|
10
18
|
### Added
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ips-api-client
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async Python client for IPS Controllers pool monitoring system
|
|
5
5
|
Home-page: https://github.com/stgarrity/ips-api-client
|
|
6
6
|
Author: Steve Garrity
|
|
7
|
-
Author-email: sgarrity@gmail.com
|
|
7
|
+
Author-email: Steve Garrity <sgarrity@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/stgarrity/ips-api-client
|
|
10
|
+
Project-URL: Documentation, https://github.com/stgarrity/ips-api-client#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/stgarrity/ips-api-client
|
|
12
|
+
Project-URL: Issues, https://github.com/stgarrity/ips-api-client/issues
|
|
13
|
+
Keywords: ips,pool,chemistry,monitoring,home-assistant
|
|
8
14
|
Classifier: Development Status :: 3 - Alpha
|
|
9
15
|
Classifier: Intended Audience :: Developers
|
|
10
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -13,21 +19,18 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Home Automation
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
25
|
Requires-Python: >=3.9
|
|
17
26
|
Description-Content-Type: text/markdown
|
|
18
27
|
License-File: LICENSE
|
|
19
28
|
Requires-Dist: aiohttp>=3.8.0
|
|
20
29
|
Requires-Dist: beautifulsoup4>=4.11.0
|
|
21
30
|
Dynamic: author
|
|
22
|
-
Dynamic: author-email
|
|
23
|
-
Dynamic: classifier
|
|
24
|
-
Dynamic: description
|
|
25
|
-
Dynamic: description-content-type
|
|
26
31
|
Dynamic: home-page
|
|
27
32
|
Dynamic: license-file
|
|
28
|
-
Dynamic: requires-dist
|
|
29
33
|
Dynamic: requires-python
|
|
30
|
-
Dynamic: summary
|
|
31
34
|
|
|
32
35
|
# IPS Controllers API Client
|
|
33
36
|
|
|
@@ -17,9 +17,15 @@ class PoolReading:
|
|
|
17
17
|
|
|
18
18
|
# Additional details (from detail page)
|
|
19
19
|
ph_state: Optional[str] = None
|
|
20
|
+
orp_state: Optional[str] = None
|
|
20
21
|
ph_setpoint: Optional[float] = None
|
|
21
22
|
orp_setpoint: Optional[int] = None
|
|
22
23
|
|
|
24
|
+
# Live overfeed lockout condition per channel.
|
|
25
|
+
# True = in overfeed, False = known not in overfeed, None = state unknown.
|
|
26
|
+
ph_overfeed: Optional[bool] = None
|
|
27
|
+
orp_overfeed: Optional[bool] = None
|
|
28
|
+
|
|
23
29
|
|
|
24
30
|
@dataclass
|
|
25
31
|
class PoolController:
|
|
@@ -124,6 +124,27 @@ def extract_first_number(value: str) -> Optional[str]:
|
|
|
124
124
|
return match.group(1)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
+
def state_is_overfeed(state: Optional[str]) -> Optional[bool]:
|
|
128
|
+
"""Determine if a live pH/ORP state string reports an overfeed lockout.
|
|
129
|
+
|
|
130
|
+
The controller state is a compound, dash/comma separated string (e.g.
|
|
131
|
+
"Normal - No Flow" or "Overfeed - No Flow"). When the unit exceeds its feed
|
|
132
|
+
limit it locks out dosing and adds an "Overfeed" token to this string.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
state: Live state string from the detail page, or None.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if the state reports overfeed, False if the state is known and does
|
|
139
|
+
not report overfeed, or None if the state is missing/unknown.
|
|
140
|
+
"""
|
|
141
|
+
if state is None or not state.strip():
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
normalized = re.sub(r'\s+', '', state).lower()
|
|
145
|
+
return 'overfeed' in normalized
|
|
146
|
+
|
|
147
|
+
|
|
127
148
|
def extract_status_from_icon(img_src: str) -> str:
|
|
128
149
|
"""Extract status from icon filename.
|
|
129
150
|
|
|
@@ -256,6 +277,7 @@ def parse_device_detail(html: str) -> PoolReading:
|
|
|
256
277
|
orp_elem = soup.find('span', id='lblOrpNum')
|
|
257
278
|
temp_elem = soup.find('span', id='lblTempValue')
|
|
258
279
|
ph_state_elem = soup.find('span', id='lblpHState')
|
|
280
|
+
orp_state_elem = soup.find('span', id='lblorpState')
|
|
259
281
|
ph_setpoint_elem = soup.find('span', id='lblphSetpoint')
|
|
260
282
|
orp_setpoint_elem = soup.find('span', id='lblorpSetpoint')
|
|
261
283
|
timestamp_elem = soup.find('span', id='lblLastReading')
|
|
@@ -269,13 +291,19 @@ def parse_device_detail(html: str) -> PoolReading:
|
|
|
269
291
|
or ''
|
|
270
292
|
)
|
|
271
293
|
|
|
294
|
+
ph_state = ph_state_elem.get_text(strip=True) if ph_state_elem else None
|
|
295
|
+
orp_state = orp_state_elem.get_text(strip=True) if orp_state_elem else None
|
|
296
|
+
|
|
272
297
|
reading = PoolReading(
|
|
273
298
|
ph=parse_float(ph_elem.get_text() if ph_elem else ''),
|
|
274
299
|
orp=parse_int(orp_elem.get_text() if orp_elem else ''),
|
|
275
300
|
temperature=parse_float(temp_elem.get_text() if temp_elem else ''),
|
|
276
|
-
ph_state=
|
|
301
|
+
ph_state=ph_state,
|
|
302
|
+
orp_state=orp_state,
|
|
277
303
|
ph_setpoint=ph_setpoint,
|
|
278
304
|
orp_setpoint=orp_setpoint,
|
|
305
|
+
ph_overfeed=state_is_overfeed(ph_state),
|
|
306
|
+
orp_overfeed=state_is_overfeed(orp_state),
|
|
279
307
|
timestamp=parse_datetime(timestamp_elem.get_text() if timestamp_elem else ''),
|
|
280
308
|
)
|
|
281
309
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ips-api-client
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async Python client for IPS Controllers pool monitoring system
|
|
5
5
|
Home-page: https://github.com/stgarrity/ips-api-client
|
|
6
6
|
Author: Steve Garrity
|
|
7
|
-
Author-email: sgarrity@gmail.com
|
|
7
|
+
Author-email: Steve Garrity <sgarrity@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/stgarrity/ips-api-client
|
|
10
|
+
Project-URL: Documentation, https://github.com/stgarrity/ips-api-client#readme
|
|
11
|
+
Project-URL: Repository, https://github.com/stgarrity/ips-api-client
|
|
12
|
+
Project-URL: Issues, https://github.com/stgarrity/ips-api-client/issues
|
|
13
|
+
Keywords: ips,pool,chemistry,monitoring,home-assistant
|
|
8
14
|
Classifier: Development Status :: 3 - Alpha
|
|
9
15
|
Classifier: Intended Audience :: Developers
|
|
10
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -13,21 +19,18 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
19
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Home Automation
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
25
|
Requires-Python: >=3.9
|
|
17
26
|
Description-Content-Type: text/markdown
|
|
18
27
|
License-File: LICENSE
|
|
19
28
|
Requires-Dist: aiohttp>=3.8.0
|
|
20
29
|
Requires-Dist: beautifulsoup4>=4.11.0
|
|
21
30
|
Dynamic: author
|
|
22
|
-
Dynamic: author-email
|
|
23
|
-
Dynamic: classifier
|
|
24
|
-
Dynamic: description
|
|
25
|
-
Dynamic: description-content-type
|
|
26
31
|
Dynamic: home-page
|
|
27
32
|
Dynamic: license-file
|
|
28
|
-
Dynamic: requires-dist
|
|
29
33
|
Dynamic: requires-python
|
|
30
|
-
Dynamic: summary
|
|
31
34
|
|
|
32
35
|
# IPS Controllers API Client
|
|
33
36
|
|
|
@@ -2,6 +2,7 @@ CHANGELOG.md
|
|
|
2
2
|
LICENSE
|
|
3
3
|
MANIFEST.in
|
|
4
4
|
README.md
|
|
5
|
+
pyproject.toml
|
|
5
6
|
requirements.txt
|
|
6
7
|
setup.py
|
|
7
8
|
ips_api/__init__.py
|
|
@@ -15,4 +16,5 @@ ips_api_client.egg-info/SOURCES.txt
|
|
|
15
16
|
ips_api_client.egg-info/dependency_links.txt
|
|
16
17
|
ips_api_client.egg-info/requires.txt
|
|
17
18
|
ips_api_client.egg-info/top_level.txt
|
|
18
|
-
tests/test_client.py
|
|
19
|
+
tests/test_client.py
|
|
20
|
+
tests/test_parser.py
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ips-api-client"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Steve Garrity", email = "sgarrity@gmail.com"}
|
|
10
|
+
]
|
|
11
|
+
description = "Async Python client for IPS Controllers pool monitoring system"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = {text = "MIT"}
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Home Automation",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
]
|
|
28
|
+
keywords = ["ips", "pool", "chemistry", "monitoring", "home-assistant"]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"aiohttp>=3.8.0",
|
|
31
|
+
"beautifulsoup4>=4.11.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/stgarrity/ips-api-client"
|
|
36
|
+
Documentation = "https://github.com/stgarrity/ips-api-client#readme"
|
|
37
|
+
Repository = "https://github.com/stgarrity/ips-api-client"
|
|
38
|
+
Issues = "https://github.com/stgarrity/ips-api-client/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["ips_api*"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Unit tests for DeviceDetail parsing and overfeed detection."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
9
|
+
|
|
10
|
+
from ips_api.parser import parse_device_detail, state_is_overfeed
|
|
11
|
+
|
|
12
|
+
FIXTURES = os.path.join(os.path.dirname(__file__), 'fixtures')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load(name: str) -> str:
|
|
16
|
+
with open(os.path.join(FIXTURES, name)) as f:
|
|
17
|
+
return f.read()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
"state, expected",
|
|
22
|
+
[
|
|
23
|
+
("Overfeed - No Flow", True),
|
|
24
|
+
("Demand, Overfeed - No Flow", True),
|
|
25
|
+
("over feed", True), # spacing/case insensitive
|
|
26
|
+
("OVERFEED", True),
|
|
27
|
+
("Normal - No Flow", False),
|
|
28
|
+
("Low Alert - No Demand - No Flow", False),
|
|
29
|
+
("", None),
|
|
30
|
+
(" ", None),
|
|
31
|
+
(None, None),
|
|
32
|
+
],
|
|
33
|
+
)
|
|
34
|
+
def test_state_is_overfeed(state, expected):
|
|
35
|
+
assert state_is_overfeed(state) is expected
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_parse_device_detail_normal():
|
|
39
|
+
reading = parse_device_detail(_load('device_detail_normal.html'))
|
|
40
|
+
|
|
41
|
+
assert reading.ph == 7.20
|
|
42
|
+
assert reading.orp == 725
|
|
43
|
+
assert reading.temperature == 82
|
|
44
|
+
assert reading.ph_setpoint == 7.3
|
|
45
|
+
assert reading.orp_setpoint == 740
|
|
46
|
+
assert reading.ph_state == "Normal - No Flow"
|
|
47
|
+
assert reading.orp_state == "Demand, In Delay - No Flow"
|
|
48
|
+
# No overfeed, but state is known -> False (not None)
|
|
49
|
+
assert reading.ph_overfeed is False
|
|
50
|
+
assert reading.orp_overfeed is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_parse_device_detail_overfeed():
|
|
54
|
+
reading = parse_device_detail(_load('device_detail_overfeed.html'))
|
|
55
|
+
|
|
56
|
+
assert reading.ph_state == "Overfeed - No Flow"
|
|
57
|
+
assert reading.orp_state == "Demand, Overfeed - No Flow"
|
|
58
|
+
assert reading.ph_overfeed is True
|
|
59
|
+
assert reading.orp_overfeed is True
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_parse_device_detail_missing_state():
|
|
63
|
+
# No state spans at all -> overfeed flags are None (unknown), not False
|
|
64
|
+
html = '<html><body><span id="lblphNum">7.2</span></body></html>'
|
|
65
|
+
reading = parse_device_detail(html)
|
|
66
|
+
|
|
67
|
+
assert reading.ph_state is None
|
|
68
|
+
assert reading.orp_state is None
|
|
69
|
+
assert reading.ph_overfeed is None
|
|
70
|
+
assert reading.orp_overfeed is None
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|