Skip to content

Commit 89f38b2

Browse files
authored
Merge pull request #46 from dkramarc/master
Fixed Apex Classic Support and added Vortech Pumps
2 parents 2e66e4f + 19f152b commit 89f38b2

File tree

8 files changed

+331
-48
lines changed

8 files changed

+331
-48
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ __pycache__/
99

1010
# Intellij IDEA stores project information in this directory
1111
.idea
12+
apex-ha-venv

custom_components/apex/apex.py

Lines changed: 224 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import requests
33
import time
44
import xmltodict
5+
import base64
6+
import json
57

68
defaultHeaders = {
79
"Accept": "*/*",
@@ -21,82 +23,172 @@ def __init__(
2123
self.deviceip = deviceip
2224
self.sid = None
2325
self.version = "new"
26+
self.did_map = {}
2427

2528
def auth(self):
2629
headers = {**defaultHeaders}
2730
data = {"login": self.username, "password": self.password, "remember_me": False}
28-
# Try logging in 3 times due to controller timeout
29-
login = 0
30-
while login < 3:
31+
login_attempt = 0
32+
33+
while login_attempt < 3:
3134
r = requests.post(f"http://{self.deviceip}/rest/login", headers=headers, json=data)
32-
# _LOGGER.debug(r.request.body)
33-
_LOGGER.debug(r.status_code)
34-
_LOGGER.debug(r.text)
35+
36+
_LOGGER.debug(f"Attempt {login_attempt + 1}: Sending POST request to http://{self.deviceip}/rest/login")
37+
_LOGGER.debug(f"Response status code: {r.status_code}")
38+
# _LOGGER.debug(f"Response body: {r.text}")
3539

3640
if r.status_code == 200:
37-
self.sid = r.json()["connect.sid"]
38-
return True
39-
if r.status_code == 404:
41+
self.sid = r.json().get("connect.sid", None)
42+
if self.sid:
43+
_LOGGER.debug(f"Successfully authenticated with session. Session ID: {self.sid}")
44+
return True
45+
else:
46+
_LOGGER.error("Session ID missing in the response.")
47+
elif r.status_code == 404:
4048
self.version = "old"
49+
_LOGGER.info("Detected old version of the device software.")
4150
return True
51+
elif r.status_code != 401:
52+
_LOGGER.warning(f"Unexpected status code: {r.status_code}")
4253
else:
43-
print("Status code failure")
44-
login += 1
45-
46-
"""Need to test different login speeds due to 401 errors"""
47-
54+
_LOGGER.info(f"Basic Auth attempt because of 401 error")
55+
# Basic Auth fallback
56+
basic_auth_header = base64.b64encode(f"{self.username}:{self.password}".encode()).decode('utf-8')
57+
headers['Authorization'] = f"Basic {basic_auth_header}"
58+
r = requests.post(f"http://{self.deviceip}/", headers=headers)
59+
60+
_LOGGER.debug(f"Basic Auth Response status code: {r.status_code}")
61+
# _LOGGER.debug(f"Basic Auth Response body: {r.text}")
62+
63+
if r.status_code == 200:
64+
self.version = "old"
65+
self.sid = f"Basic {basic_auth_header}"
66+
_LOGGER.info("Successfully authenticated using Basic Auth.")
67+
_LOGGER.debug(f"Basic Auth SID: {self.sid}")
68+
return True
69+
else:
70+
_LOGGER.error("Failed to authenticate using both methods.")
71+
72+
login_attempt += 1
73+
if login_attempt < 3:
74+
_LOGGER.info(f"Retrying authentication... Attempt #{login_attempt + 1}")
75+
76+
_LOGGER.error("Authentication failed after 3 attempts.")
4877
return False
4978

79+
5080
def oldstatus(self):
51-
"""Function for returning information on old controllers (Currently not authenticated)"""
5281
headers = {**defaultHeaders}
82+
headers['Authorization'] = self.sid
5383

5484
r = requests.get(f"http://{self.deviceip}/cgi-bin/status.xml?" + str(round(time.time())), headers=headers)
85+
_LOGGER.debug(f"oldstatus: Response status code: {r.status_code}")
86+
# _LOGGER.debug(f"oldstatus: Response body: {r.text}")
87+
5588
xml = xmltodict.parse(r.text)
56-
# Code to convert old style to new style json
89+
# _LOGGER.debug("oldstatus: XML parsed successfully")
90+
5791
result = {}
5892
system = {}
5993
system["software"] = xml["status"]["@software"]
6094
system["hardware"] = xml["status"]["@hardware"] + " Legacy Version (Status.xml)"
61-
6295
result["system"] = system
96+
# _LOGGER.debug(f"oldstatus: system: {system}")
6397

6498
inputs = []
65-
for value in xml["status"]["probes"]["probe"]:
99+
# Ensure the 'probe' key exists and is a list
100+
probes = xml["status"]["probes"].get("probe", [])
101+
if not isinstance(probes, list):
102+
probes = [probes] # Make it a single-item list if it's not a list
103+
104+
for value in probes:
66105
inputdata = {}
67106
inputdata["did"] = "base_" + value["name"]
68107
inputdata["name"] = value["name"]
69-
inputdata["type"] = value["type"]
70-
inputdata["value"] = value["value"]
108+
# Using get to provide a default value of 'variable' if 'type' is not found
109+
inputdata["type"] = value.get("type", "variable")
110+
inputdata["value"] = value["value"].strip() # Also stripping any whitespace from the value
71111
inputs.append(inputdata)
72112

73113
result["inputs"] = inputs
114+
# _LOGGER.debug(f"oldstatus: inputs: {inputs}")
74115

75116
outputs = []
76117
for value in xml["status"]["outlets"]["outlet"]:
77-
_LOGGER.debug(value)
78118
outputdata = {}
79119
outputdata["did"] = value["deviceID"]
80120
outputdata["name"] = value["name"]
81121
outputdata["status"] = [value["state"], "", "OK", ""]
82122
outputdata["id"] = value["outputID"]
83123
outputdata["type"] = "outlet"
84124
outputs.append(outputdata)
125+
self.did_map[value["deviceID"]] = value["name"]
85126

86127
result["outputs"] = outputs
128+
# _LOGGER.debug(f"oldstatus: outputs: {outputs}")
87129

88-
_LOGGER.debug(result)
130+
_LOGGER.debug(f"oldstatus result: {result}")
89131
return result
90132

133+
def oldstatus_json(self):
134+
i = 0
135+
while i <= 3:
136+
headers = {**defaultHeaders}
137+
headers['Authorization'] = self.sid
138+
139+
r = requests.get(f"http://{self.deviceip}/cgi-bin/status.json?" + str(round(time.time())), headers=headers)
140+
# _LOGGER.debug(f"oldstatus_json: Response status code: {r.status_code}")
141+
# _LOGGER.debug(f"oldstatus_json: Response body: {r.text}")
142+
143+
if r.status_code == 200:
144+
json_in = r.json()
145+
# _LOGGER.debug(f"oldstatus_json: json_in: {json_in}")
146+
147+
# data comes in istat so move it to root of results
148+
result = json_in["istat"];
149+
150+
# generate system info
151+
system = {}
152+
system["software"] = result["software"]
153+
system["hardware"] = result["hostname"] + " " + result["hardware"] + " " + result["serial"]
154+
result["system"] = system
155+
# _LOGGER.debug(f"oldstatus_json: system: {system}")
156+
157+
# Add Apex type for Feed Calculation
158+
result["feed"]["apex_type"] = "old"
159+
160+
# Parse outputs to get name for map (for toggle)
161+
outputs = result["outputs"]
162+
for output in outputs:
163+
did = output["did"]
164+
name = output["name"]
165+
self.did_map[did] = name
166+
# _LOGGER.debug(f"oldstatus_json: did_map: {self.did_map}")
167+
168+
#_LOGGER.debug(f"oldstatus_json result: {result}")
169+
return result
170+
elif r.status_code == 401:
171+
self.auth()
172+
else:
173+
_LOGGER.debug("oldstatus_json: Unknown error occurred")
174+
return {}
175+
i += 1
176+
177+
178+
91179
def status(self):
92-
_LOGGER.debug(self.sid)
180+
181+
_LOGGER.debug(f"status grab for {self.version}: sid[{self.sid}]")
182+
93183
if self.sid is None:
94184
_LOGGER.debug("We are none")
95185
self.auth()
96186

97187
if self.version == "old":
98-
result = self.oldstatus()
188+
# result = self.oldstatus()
189+
result = self.oldstatus_json()
99190
return result
191+
100192
i = 0
101193
while i <= 3:
102194
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
@@ -116,6 +208,7 @@ def config(self):
116208
if self.version == "old":
117209
result = {}
118210
return result
211+
119212
if self.sid is None:
120213
_LOGGER.debug("We are none")
121214
self.auth()
@@ -127,9 +220,50 @@ def config(self):
127220
if r.status_code == 200:
128221
return r.json()
129222
else:
130-
print("Error occured")
223+
print("Error occurred")
131224

132225
def toggle_output(self, did, state):
226+
# _LOGGER.debug(f"toggle_output [{self.version}]: did[{did}] state[{state}]")
227+
228+
if self.version == "old":
229+
headers = {**defaultHeaders}
230+
headers['Authorization'] = self.sid
231+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
232+
233+
# 1 = OFF, 0 = AUTO, 2 = ON
234+
state_value = 1
235+
ret_state = "OFF"
236+
if state == "ON":
237+
state_value = 2
238+
ret_state = "ON"
239+
if state == "AOF":
240+
state_value = 0
241+
ret_state = "OFF"
242+
if state == "AON":
243+
state_value = 0
244+
ret_state = "ON"
245+
246+
object_name = self.did_map[did]
247+
248+
data = f"{object_name}_state={state_value}&noResponse=1"
249+
_LOGGER.debug(f"toggle_output [old] Out Data: {data}")
250+
251+
headers['Content-Length'] = f"{len(data)}"
252+
_LOGGER.debug(f"toggle_output [old] Headers: {headers}")
253+
254+
try:
255+
url = f"http://{self.deviceip}/cgi-bin/status.cgi"
256+
r = requests.post(url, headers=headers, data=data, proxies={"http": None, "https": None})
257+
_LOGGER.debug(f"toggle_output [old] ({r.status_code}): {r.text}")
258+
except Exception as e:
259+
_LOGGER.debug(f"toggle_output [old] Exception: {e}")
260+
261+
status_data = {
262+
"status": [ret_state],
263+
}
264+
return status_data
265+
266+
133267
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
134268

135269
# I gave this "type": "outlet" a bit of side-eye, but it seems to be fine even if the
@@ -143,6 +277,61 @@ def toggle_output(self, did, state):
143277
return data
144278

145279
def toggle_feed_cycle(self, did, state):
280+
_LOGGER.debug(f"toggle_feed_cycle [{self.version}]: did[{did}] state[{state}]")
281+
282+
if self.version == "old":
283+
284+
# Feed A-D: (0/3)
285+
# FeedCycle=Feed&FeedSel=3&noResponse=1
286+
# Cancel (5)
287+
# FeedCycle=Feed&FeedSel=5&noResponse=1
288+
289+
feed_selection_map = {
290+
"1": "0",
291+
"2": "1",
292+
"3": "2",
293+
"4": "3"
294+
}
295+
296+
# Default to Cancel/OFF
297+
FeedSel = "5"
298+
# ret_state: 1 = ON, 92 = OFF
299+
ret_state = 92
300+
ret_did = 6 # 6 is Off
301+
302+
# If Start Feed then map to FeedSel needed
303+
if state == "ON" and did in feed_selection_map:
304+
FeedSel = feed_selection_map[did]
305+
ret_state = 1
306+
ret_did = did
307+
308+
headers = {**defaultHeaders}
309+
headers['Authorization'] = self.sid
310+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
311+
312+
data = f"FeedCycle=Feed&FeedSel={FeedSel}&noResponse=1"
313+
# _LOGGER.debug(f"toggle_feed_cycle [old] Out Data: {data}")
314+
315+
headers['Content-Length'] = f"{len(data)}"
316+
# _LOGGER.debug(f"toggle_feed_cycle [old] Headers: {headers}")
317+
318+
try:
319+
url = f"http://{self.deviceip}/cgi-bin/status.cgi"
320+
r = requests.post(url, headers=headers, data=data, proxies={"http": None, "https": None})
321+
_LOGGER.debug(f"toggle_feed_cycle [old] ({r.status_code}): {r.text}")
322+
except Exception as e:
323+
_LOGGER.debug(f"toggle_feed_cycle [old] Exception: {e}")
324+
325+
status_data = {
326+
"active": ret_state,
327+
"errorCode": 0,
328+
"errorMessage": "",
329+
"name": ret_did,
330+
"apex_type:": "old"
331+
}
332+
return status_data
333+
334+
146335
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
147336
if state == "ON":
148337
data = {"active": 1, "errorCode": 0, "errorMessage": "", "name": did}
@@ -160,6 +349,9 @@ def toggle_feed_cycle(self, did, state):
160349
return data
161350

162351
def set_variable(self, did, code):
352+
if self.version == "old":
353+
return {"error": "Not available on Apex Classic"}
354+
163355
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
164356
config = self.config()
165357
variable = None
@@ -186,6 +378,9 @@ def set_variable(self, did, code):
186378
return {"error": ""}
187379

188380
def update_firmware(self):
381+
if self.version == "old":
382+
return {"error": "Not available on Apex Classic"}
383+
189384
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
190385
config = self.config()
191386

@@ -202,6 +397,10 @@ def update_firmware(self):
202397
return False
203398

204399
def set_dos_rate(self, did, profile_id, rate):
400+
401+
if self.version == "old":
402+
return {"error": "Not available on Apex Classic"}
403+
205404
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
206405
config = self.config()
207406

custom_components/apex/const.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
"dos": {"icon": "mdi:test-tube"},
1212
"virtual": {"icon": "mdi:monitor-account"},
1313
"iotaPump|Sicce|Syncra": {"icon" : "mdi:pump"},
14-
"Feed" : {"icon": "mdi:shaker"}
14+
"Feed" : {"icon": "mdi:shaker"},
15+
"gph" : {"icon": "mdi:waves-arrow-right"},
16+
"vortech" : {"icon": "mdi:pump"},
17+
"UNK" : {"icon": "mdi:help"}
1518
}
1619

1720
FEED_CYCLES = [
@@ -39,7 +42,10 @@
3942
"iotaPump|Sicce|Syncra": {"icon" : "mdi:pump", "measurement": "%"},
4043
"variable" : {"icon" : "mdi:cog-outline"},
4144
"virtual" : {"icon" : "mdi:cog-outline"},
42-
"feed" : {"icon": "mdi:timer", "measurement": "mins"}
45+
"feed" : {"icon": "mdi:timer", "measurement": "mins"},
46+
"gph" : {"icon": "mdi:waves-arrow-right", "measurement": "gph"},
47+
"vortech" : {"icon": "mdi:pump"},
48+
"UNK" : {"icon": "mdi:help"}
4349
}
4450

4551
MANUAL_SENSORS = [

custom_components/apex/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"loggers": ["custom_components.apex"],
1313
"requirements": [],
1414
"ssdp": [],
15-
"version": "1.14",
15+
"version": "1.15",
1616
"zeroconf": []
1717
}

0 commit comments

Comments
 (0)