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.
Files changed (22) hide show
  1. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/CHANGELOG.md +8 -0
  2. {ips_api_client-0.1.1/ips_api_client.egg-info → ips_api_client-0.2.0}/PKG-INFO +12 -9
  3. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/models.py +6 -0
  4. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/parser.py +29 -1
  5. {ips_api_client-0.1.1 → ips_api_client-0.2.0/ips_api_client.egg-info}/PKG-INFO +12 -9
  6. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/SOURCES.txt +3 -1
  7. ips_api_client-0.2.0/pyproject.toml +41 -0
  8. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/setup.py +1 -1
  9. ips_api_client-0.2.0/tests/test_parser.py +70 -0
  10. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/LICENSE +0 -0
  11. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/MANIFEST.in +0 -0
  12. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/README.md +0 -0
  13. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/__init__.py +0 -0
  14. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/client.py +0 -0
  15. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/const.py +0 -0
  16. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api/exceptions.py +0 -0
  17. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/dependency_links.txt +0 -0
  18. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/requires.txt +0 -0
  19. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/ips_api_client.egg-info/top_level.txt +0 -0
  20. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/requirements.txt +0 -0
  21. {ips_api_client-0.1.1 → ips_api_client-0.2.0}/setup.cfg +0 -0
  22. {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.1.1
4
- Summary: API client for IPS Controllers pool monitoring system
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=ph_state_elem.get_text(strip=True) if ph_state_elem else None,
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.1.1
4
- Summary: API client for IPS Controllers pool monitoring system
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*"]
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="ips-api-client",
8
- version="0.1.1",
8
+ version="0.2.0",
9
9
  author="Steve Garrity",
10
10
  author_email="sgarrity@gmail.com",
11
11
  description="API client for IPS Controllers pool monitoring system",
@@ -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