1+ import enum
12import json
23import logging
34import time
4- from typing import Optional
5+ from typing import Literal , Optional
56
67import requests
78import websocket
1112from py_incharge .utils import find_element_through_shadow
1213
1314
15+ class Command (enum .Enum ):
16+ unlock_connector = "UnlockConnector"
17+ start_transaction = "Remote start transaction"
18+ stop_transaction = "Remote Stop Transaction"
19+ set_light_intensity = "Set Light intensity"
20+ change_availability = "Change availability"
21+ reset = "Reset"
22+ trigger_status_notification = "TriggerMessage StatusNotificat"
23+
24+
25+ class WebsocketMessageType (enum .Enum ):
26+ ticket_auth = "TICKET_AUTH"
27+ custom_command = "CUSTOM_COMMAND"
28+ response = "RESPONSE"
29+ sent = "SENT"
30+ ping = "PING"
31+ error = "ERROR"
32+
33+
1434class InCharge :
1535 AZURE_BASE_URL = "https://businessspecificapimanglobal.azure-api.net"
1636
@@ -92,16 +112,168 @@ def login(self):
92112 driver .quit ()
93113 logging .info ("Login successful, bearer token obtained" )
94114
95- def get_ticket (self ) -> str :
115+ @staticmethod
116+ def requires_login (method ):
117+ """Decorator to ensure the user is logged in before executing a method."""
118+
119+ def wrapper (self , * args , ** kwargs ):
120+ if not self .bearer_token :
121+ raise ValueError ("Must login first (call 'client.login()')" )
122+ return method (self , * args , ** kwargs )
123+
124+ return wrapper
125+
126+ @requires_login
127+ def unlock_connector (self , station_name , connector_id : int = 1 ):
128+ """This will unlock your EV charger."""
129+ return self ._send_command_via_websocket (
130+ self ._get_command_id (station_name , Command .unlock_connector ),
131+ station_name ,
132+ {"example-number-parameter" : connector_id },
133+ expected_status = "Unlocked" ,
134+ )
135+
136+ @requires_login
137+ def start_transaction (self , station_name : str , rfid : str , connector_id : int = 1 ):
138+ """This will turn on your EV charger."""
139+ return self ._send_command_via_websocket (
140+ self ._get_command_id (station_name , Command .start_transaction ),
141+ station_name ,
142+ {"connectorId" : connector_id , "idTag" : rfid },
143+ expected_status = "Accepted" ,
144+ )
145+
146+ @requires_login
147+ def set_light_intensity (
148+ self ,
149+ station_name : str ,
150+ intensity : Literal ["0" , "10" , "25" , "50" , "75" , "90" , "100" ],
151+ ):
152+ """This will set the light intensity of the EV charger."""
153+ return self ._send_command_via_websocket (
154+ self ._get_command_id (station_name , Command .set_light_intensity ),
155+ station_name ,
156+ {"example-enum-parameter" : intensity },
157+ expected_status = "Accepted" ,
158+ )
159+
160+ @requires_login
161+ def stop_transaction (self , station_name : str , transaction_id : int = 1 ):
162+ """This will turn off your EV charger."""
163+ return self ._send_command_via_websocket (
164+ self ._get_command_id (station_name , Command .stop_transaction ),
165+ station_name ,
166+ {"transactionId" : transaction_id },
167+ expected_status = "Accepted" ,
168+ )
169+
170+ @requires_login
171+ def change_availability (
172+ self ,
173+ station_name : str ,
174+ availability : Literal ["Operative" , "Inoperative" ],
175+ connector_id : int = 1 ,
176+ ):
177+ """This will change the availability of the EV charger."""
178+ return self ._send_command_via_websocket (
179+ self ._get_command_id (station_name , Command .change_availability ),
180+ station_name ,
181+ {"connectorId" : connector_id , "availability" : availability },
182+ expected_status = "Accepted" ,
183+ )
184+
185+ @requires_login
186+ def reset (self , station_name : str , mode : Literal ["Soft" , "Hard" ] = "Soft" ):
187+ """This will reset the EV charger."""
188+ return self ._send_command_via_websocket (
189+ self ._get_command_id (station_name , Command .reset ),
190+ station_name ,
191+ {"typeOfReset" : mode },
192+ expected_status = "Accepted" ,
193+ )
194+
195+ @requires_login
196+ def trigger_status_notification (self , station_name : str , connector_id : int = 1 ):
197+ """This will trigger a status notification for the EV charger."""
198+ return self ._send_command_via_websocket (
199+ self ._get_command_id (station_name , Command .trigger_status_notification ),
200+ station_name ,
201+ {"connectorId" : connector_id },
202+ expected_status = "Accepted" ,
203+ )
204+
205+ @requires_login
206+ def _send_command_via_websocket (
207+ self , command_id : str , station_name : str , parameters : dict , expected_status : str
208+ ) -> bool :
209+ """
210+ Sends a command via websocket to the InCharge API and waits for a response.
211+ This method is used for various commands like unlocking a connector, starting a transaction, etc.
212+ It also checks the response status to determine if the command was accepted or rejected.
213+
214+ It does the following:
215+ 1. Connects to the InCharge websocket server.
216+ 2. Requests a new ticket id.
217+ 3. Sends a ticket authentication message to the websocket.
218+ 4. Sends a custom command message with the specified command ID, station name, and parameters.
219+ 5. Waits for a response from the websocket and checks the status of the response.
220+ """
221+
222+ logging .info ("Connecting to websocket" )
223+ ws = websocket .create_connection (self .WEBSOCKET_URL )
224+
225+ try :
226+ ticket_auth_msg = {
227+ "type" : WebsocketMessageType .ticket_auth .value ,
228+ "id" : self ._get_new_ticket_id (),
229+ }
230+ ws .send (json .dumps (ticket_auth_msg ))
231+
232+ logging .info (f"Sent ticket authentication to websocket: { ticket_auth_msg } " )
233+ logging .info (f"Received message from websocket: { ws .recv ()} " )
234+ time .sleep (1 )
235+
236+ custom_command_msg = {
237+ "type" : WebsocketMessageType .custom_command .value ,
238+ "commandId" : command_id ,
239+ "stations" : [station_name ],
240+ "parameters" : parameters ,
241+ }
242+ ws .send (json .dumps (custom_command_msg ))
243+
244+ logging .info (f"Sent command to websocket: { custom_command_msg } " )
245+
246+ while True :
247+ msg = ws .recv ()
248+ logging .info (f"Received message from websocket: { msg } " )
249+
250+ msg_json = json .loads (msg )
251+ if msg_json .get ("type" ) == WebsocketMessageType .response .value :
252+ payload = json .loads (msg_json .get ("payload" , "{}" ))
253+
254+ status = payload .get ("status" )
255+ if status == expected_status :
256+ logging .info (f"Command accepted: { status } " )
257+ return True
258+ elif status == "Rejected" :
259+ logging .error (f"Command rejected: { status } " )
260+ return False
261+ elif msg_json .get ("type" ) == WebsocketMessageType .error .value :
262+ logging .error (
263+ f"Error received from websocket: { msg_json .get ('payload' )} "
264+ )
265+ return False
266+ finally :
267+ ws .close ()
268+
269+ @requires_login
270+ def _get_new_ticket_id (self ) -> str :
96271 """
97272 Before starting a remote transaction, a ticket ID must be obtained.
98273 This ticket is used to authenticate the websocket connection and is required
99274 to start a remote transaction. This function sends a POST request to the
100275 InCharge API to obtain a ticket id.
101276 """
102- if not self .bearer_token :
103- raise ValueError ("Must login first before getting ticket" )
104-
105277 response = requests .post (
106278 InCharge .TICKET_URL ,
107279 headers = {
@@ -112,21 +284,19 @@ def get_ticket(self) -> str:
112284 json = {},
113285 )
114286 if response .status_code == 200 :
115- print ("Ticket request failed:" , response .status_code , response .text )
287+ logging . error ("Ticket request failed:" , response .status_code , response .text )
116288 raise ValueError ("Failed to get ticket from API" )
117289
118290 return response .text .strip ().strip ('"' )
119291
120- def get_remote_start_command_id (self , station_name : str ) -> str :
292+ @requires_login
293+ def _get_command_id (self , station_name : str , command : Command ) -> str :
121294 """
122295 Nobody knows why, but the command ID is not static and must be fetched
123296 from the InCharge API every time before starting a remote transaction.
124297 This function sends a GET request to the InCharge API to retrieve the command id
125298 for the remote start transaction command for the specified station.
126299 """
127- if not self .bearer_token :
128- raise ValueError ("Must login first before getting command ID" )
129-
130300 response = requests .get (
131301 InCharge .COMMAND_ID_URL .format (station_name = station_name ),
132302 headers = {
@@ -139,58 +309,8 @@ def get_remote_start_command_id(self, station_name: str) -> str:
139309 print ("Command ID request failed:" , response .status_code , response .text )
140310 raise ValueError ("Failed to get command ID from API" )
141311
142- for command in response .json ():
143- if command ["details" ]["name" ] == "Remote start transaction" :
144- return command ["commandId" ]
312+ for command_info in response .json ():
313+ if command_info ["details" ]["name" ] == command . value :
314+ return command_info ["commandId" ]
145315
146- raise ValueError ("Remote start transaction command not found" )
147-
148- def start_remote_transaction (
149- self , station_name : str , rfid : str , connector_id : int = 1
150- ):
151- """
152- In short: this method will turn on your EV charger.
153-
154- This method starts a remote transaction on the specified station using the provided RFID and connector ID.
155- It first retrieves the command ID for the remote start transaction, then obtains a ticket ID,
156- and finally establishes a websocket connection to send the remote start command.
157- """
158- if not self .bearer_token :
159- raise ValueError ("Must login first before starting transaction" )
160-
161- logging .info ("Starting remote transaction..." )
162- command_id = self .get_remote_start_command_id (station_name )
163- ticket_id = self .get_ticket ()
164-
165- ws = websocket .create_connection (InCharge .WEBSOCKET_URL )
166-
167- ticket_auth_msg = {"type" : "TICKET_AUTH" , "id" : ticket_id }
168- ws .send (json .dumps (ticket_auth_msg ))
169- logging .info (f"Sent TICKET_AUTH: { ticket_auth_msg } " )
170-
171- response = ws .recv ()
172- logging .info (f"Received: { response } " )
173-
174- time .sleep (1 )
175-
176- custom_command_msg = {
177- "type" : "CUSTOM_COMMAND" ,
178- "commandId" : command_id ,
179- "stations" : [station_name ],
180- "parameters" : {"connectorId" : connector_id , "idTag" : rfid },
181- }
182- ws .send (json .dumps (custom_command_msg ))
183- logging .info (f"Sent CUSTOM_COMMAND: { custom_command_msg } " )
184-
185- while True :
186- msg = ws .recv ()
187- logging .info (f"Received: { msg } " )
188- msg_json = json .loads (msg )
189- if msg_json .get ("type" ) == "RESPONSE" :
190- payload = json .loads (msg_json .get ("payload" , "{}" ))
191- if payload .get ("status" ) == "Accepted" :
192- logging .info ("Remote start accepted." )
193- return True
194- elif payload .get ("status" ) == "Rejected" :
195- logging .error ("Remote start rejected." )
196- return False
316+ raise ValueError (f"Command { command .name } not found" )
0 commit comments