Skip to content

Commit

Permalink
Merge pull request #46 from dkramarc/master
Browse files Browse the repository at this point in the history
Fixed Apex Classic Support and added Vortech Pumps
  • Loading branch information
itchannel authored May 11, 2024
2 parents 2e66e4f + 19f152b commit 89f38b2
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 48 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ __pycache__/

# Intellij IDEA stores project information in this directory
.idea
apex-ha-venv
249 changes: 224 additions & 25 deletions custom_components/apex/apex.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import requests
import time
import xmltodict
import base64
import json

defaultHeaders = {
"Accept": "*/*",
Expand All @@ -21,82 +23,172 @@ def __init__(
self.deviceip = deviceip
self.sid = None
self.version = "new"
self.did_map = {}

def auth(self):
headers = {**defaultHeaders}
data = {"login": self.username, "password": self.password, "remember_me": False}
# Try logging in 3 times due to controller timeout
login = 0
while login < 3:
login_attempt = 0

while login_attempt < 3:
r = requests.post(f"http://{self.deviceip}/rest/login", headers=headers, json=data)
# _LOGGER.debug(r.request.body)
_LOGGER.debug(r.status_code)
_LOGGER.debug(r.text)

_LOGGER.debug(f"Attempt {login_attempt + 1}: Sending POST request to http://{self.deviceip}/rest/login")
_LOGGER.debug(f"Response status code: {r.status_code}")
# _LOGGER.debug(f"Response body: {r.text}")

if r.status_code == 200:
self.sid = r.json()["connect.sid"]
return True
if r.status_code == 404:
self.sid = r.json().get("connect.sid", None)
if self.sid:
_LOGGER.debug(f"Successfully authenticated with session. Session ID: {self.sid}")
return True
else:
_LOGGER.error("Session ID missing in the response.")
elif r.status_code == 404:
self.version = "old"
_LOGGER.info("Detected old version of the device software.")
return True
elif r.status_code != 401:
_LOGGER.warning(f"Unexpected status code: {r.status_code}")
else:
print("Status code failure")
login += 1

"""Need to test different login speeds due to 401 errors"""

_LOGGER.info(f"Basic Auth attempt because of 401 error")
# Basic Auth fallback
basic_auth_header = base64.b64encode(f"{self.username}:{self.password}".encode()).decode('utf-8')
headers['Authorization'] = f"Basic {basic_auth_header}"
r = requests.post(f"http://{self.deviceip}/", headers=headers)

_LOGGER.debug(f"Basic Auth Response status code: {r.status_code}")
# _LOGGER.debug(f"Basic Auth Response body: {r.text}")

if r.status_code == 200:
self.version = "old"
self.sid = f"Basic {basic_auth_header}"
_LOGGER.info("Successfully authenticated using Basic Auth.")
_LOGGER.debug(f"Basic Auth SID: {self.sid}")
return True
else:
_LOGGER.error("Failed to authenticate using both methods.")

login_attempt += 1
if login_attempt < 3:
_LOGGER.info(f"Retrying authentication... Attempt #{login_attempt + 1}")

_LOGGER.error("Authentication failed after 3 attempts.")
return False


def oldstatus(self):
"""Function for returning information on old controllers (Currently not authenticated)"""
headers = {**defaultHeaders}
headers['Authorization'] = self.sid

r = requests.get(f"http://{self.deviceip}/cgi-bin/status.xml?" + str(round(time.time())), headers=headers)
_LOGGER.debug(f"oldstatus: Response status code: {r.status_code}")
# _LOGGER.debug(f"oldstatus: Response body: {r.text}")

xml = xmltodict.parse(r.text)
# Code to convert old style to new style json
# _LOGGER.debug("oldstatus: XML parsed successfully")

result = {}
system = {}
system["software"] = xml["status"]["@software"]
system["hardware"] = xml["status"]["@hardware"] + " Legacy Version (Status.xml)"

result["system"] = system
# _LOGGER.debug(f"oldstatus: system: {system}")

inputs = []
for value in xml["status"]["probes"]["probe"]:
# Ensure the 'probe' key exists and is a list
probes = xml["status"]["probes"].get("probe", [])
if not isinstance(probes, list):
probes = [probes] # Make it a single-item list if it's not a list

for value in probes:
inputdata = {}
inputdata["did"] = "base_" + value["name"]
inputdata["name"] = value["name"]
inputdata["type"] = value["type"]
inputdata["value"] = value["value"]
# Using get to provide a default value of 'variable' if 'type' is not found
inputdata["type"] = value.get("type", "variable")
inputdata["value"] = value["value"].strip() # Also stripping any whitespace from the value
inputs.append(inputdata)

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

outputs = []
for value in xml["status"]["outlets"]["outlet"]:
_LOGGER.debug(value)
outputdata = {}
outputdata["did"] = value["deviceID"]
outputdata["name"] = value["name"]
outputdata["status"] = [value["state"], "", "OK", ""]
outputdata["id"] = value["outputID"]
outputdata["type"] = "outlet"
outputs.append(outputdata)
self.did_map[value["deviceID"]] = value["name"]

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

_LOGGER.debug(result)
_LOGGER.debug(f"oldstatus result: {result}")
return result

def oldstatus_json(self):
i = 0
while i <= 3:
headers = {**defaultHeaders}
headers['Authorization'] = self.sid

r = requests.get(f"http://{self.deviceip}/cgi-bin/status.json?" + str(round(time.time())), headers=headers)
# _LOGGER.debug(f"oldstatus_json: Response status code: {r.status_code}")
# _LOGGER.debug(f"oldstatus_json: Response body: {r.text}")

if r.status_code == 200:
json_in = r.json()
# _LOGGER.debug(f"oldstatus_json: json_in: {json_in}")

# data comes in istat so move it to root of results
result = json_in["istat"];

# generate system info
system = {}
system["software"] = result["software"]
system["hardware"] = result["hostname"] + " " + result["hardware"] + " " + result["serial"]
result["system"] = system
# _LOGGER.debug(f"oldstatus_json: system: {system}")

# Add Apex type for Feed Calculation
result["feed"]["apex_type"] = "old"

# Parse outputs to get name for map (for toggle)
outputs = result["outputs"]
for output in outputs:
did = output["did"]
name = output["name"]
self.did_map[did] = name
# _LOGGER.debug(f"oldstatus_json: did_map: {self.did_map}")

#_LOGGER.debug(f"oldstatus_json result: {result}")
return result
elif r.status_code == 401:
self.auth()
else:
_LOGGER.debug("oldstatus_json: Unknown error occurred")
return {}
i += 1



def status(self):
_LOGGER.debug(self.sid)

_LOGGER.debug(f"status grab for {self.version}: sid[{self.sid}]")

if self.sid is None:
_LOGGER.debug("We are none")
self.auth()

if self.version == "old":
result = self.oldstatus()
# result = self.oldstatus()
result = self.oldstatus_json()
return result

i = 0
while i <= 3:
headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
Expand All @@ -116,6 +208,7 @@ def config(self):
if self.version == "old":
result = {}
return result

if self.sid is None:
_LOGGER.debug("We are none")
self.auth()
Expand All @@ -127,9 +220,50 @@ def config(self):
if r.status_code == 200:
return r.json()
else:
print("Error occured")
print("Error occurred")

def toggle_output(self, did, state):
# _LOGGER.debug(f"toggle_output [{self.version}]: did[{did}] state[{state}]")

if self.version == "old":
headers = {**defaultHeaders}
headers['Authorization'] = self.sid
headers['Content-Type'] = 'application/x-www-form-urlencoded'

# 1 = OFF, 0 = AUTO, 2 = ON
state_value = 1
ret_state = "OFF"
if state == "ON":
state_value = 2
ret_state = "ON"
if state == "AOF":
state_value = 0
ret_state = "OFF"
if state == "AON":
state_value = 0
ret_state = "ON"

object_name = self.did_map[did]

data = f"{object_name}_state={state_value}&noResponse=1"
_LOGGER.debug(f"toggle_output [old] Out Data: {data}")

headers['Content-Length'] = f"{len(data)}"
_LOGGER.debug(f"toggle_output [old] Headers: {headers}")

try:
url = f"http://{self.deviceip}/cgi-bin/status.cgi"
r = requests.post(url, headers=headers, data=data, proxies={"http": None, "https": None})
_LOGGER.debug(f"toggle_output [old] ({r.status_code}): {r.text}")
except Exception as e:
_LOGGER.debug(f"toggle_output [old] Exception: {e}")

status_data = {
"status": [ret_state],
}
return status_data


headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}

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

def toggle_feed_cycle(self, did, state):
_LOGGER.debug(f"toggle_feed_cycle [{self.version}]: did[{did}] state[{state}]")

if self.version == "old":

# Feed A-D: (0/3)
# FeedCycle=Feed&FeedSel=3&noResponse=1
# Cancel (5)
# FeedCycle=Feed&FeedSel=5&noResponse=1

feed_selection_map = {
"1": "0",
"2": "1",
"3": "2",
"4": "3"
}

# Default to Cancel/OFF
FeedSel = "5"
# ret_state: 1 = ON, 92 = OFF
ret_state = 92
ret_did = 6 # 6 is Off

# If Start Feed then map to FeedSel needed
if state == "ON" and did in feed_selection_map:
FeedSel = feed_selection_map[did]
ret_state = 1
ret_did = did

headers = {**defaultHeaders}
headers['Authorization'] = self.sid
headers['Content-Type'] = 'application/x-www-form-urlencoded'

data = f"FeedCycle=Feed&FeedSel={FeedSel}&noResponse=1"
# _LOGGER.debug(f"toggle_feed_cycle [old] Out Data: {data}")

headers['Content-Length'] = f"{len(data)}"
# _LOGGER.debug(f"toggle_feed_cycle [old] Headers: {headers}")

try:
url = f"http://{self.deviceip}/cgi-bin/status.cgi"
r = requests.post(url, headers=headers, data=data, proxies={"http": None, "https": None})
_LOGGER.debug(f"toggle_feed_cycle [old] ({r.status_code}): {r.text}")
except Exception as e:
_LOGGER.debug(f"toggle_feed_cycle [old] Exception: {e}")

status_data = {
"active": ret_state,
"errorCode": 0,
"errorMessage": "",
"name": ret_did,
"apex_type:": "old"
}
return status_data


headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
if state == "ON":
data = {"active": 1, "errorCode": 0, "errorMessage": "", "name": did}
Expand All @@ -160,6 +349,9 @@ def toggle_feed_cycle(self, did, state):
return data

def set_variable(self, did, code):
if self.version == "old":
return {"error": "Not available on Apex Classic"}

headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
config = self.config()
variable = None
Expand All @@ -186,6 +378,9 @@ def set_variable(self, did, code):
return {"error": ""}

def update_firmware(self):
if self.version == "old":
return {"error": "Not available on Apex Classic"}

headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
config = self.config()

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

def set_dos_rate(self, did, profile_id, rate):

if self.version == "old":
return {"error": "Not available on Apex Classic"}

headers = {**defaultHeaders, "Cookie": "connect.sid=" + self.sid}
config = self.config()

Expand Down
10 changes: 8 additions & 2 deletions custom_components/apex/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
"dos": {"icon": "mdi:test-tube"},
"virtual": {"icon": "mdi:monitor-account"},
"iotaPump|Sicce|Syncra": {"icon" : "mdi:pump"},
"Feed" : {"icon": "mdi:shaker"}
"Feed" : {"icon": "mdi:shaker"},
"gph" : {"icon": "mdi:waves-arrow-right"},
"vortech" : {"icon": "mdi:pump"},
"UNK" : {"icon": "mdi:help"}
}

FEED_CYCLES = [
Expand Down Expand Up @@ -39,7 +42,10 @@
"iotaPump|Sicce|Syncra": {"icon" : "mdi:pump", "measurement": "%"},
"variable" : {"icon" : "mdi:cog-outline"},
"virtual" : {"icon" : "mdi:cog-outline"},
"feed" : {"icon": "mdi:timer", "measurement": "mins"}
"feed" : {"icon": "mdi:timer", "measurement": "mins"},
"gph" : {"icon": "mdi:waves-arrow-right", "measurement": "gph"},
"vortech" : {"icon": "mdi:pump"},
"UNK" : {"icon": "mdi:help"}
}

MANUAL_SENSORS = [
Expand Down
2 changes: 1 addition & 1 deletion custom_components/apex/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"loggers": ["custom_components.apex"],
"requirements": [],
"ssdp": [],
"version": "1.14",
"version": "1.15",
"zeroconf": []
}
Loading

0 comments on commit 89f38b2

Please sign in to comment.