22import requests
33import time
44import xmltodict
5+ import base64
6+ import json
57
68defaultHeaders = {
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
0 commit comments