Skip to content

Commit 4b364a0

Browse files
author
Thomas Salm
committed
Merge branch 'nanomad-feture/get-user-timezone' into develop
2 parents 2140153 + 27a2c4c commit 4b364a0

File tree

6 files changed

+153
-37
lines changed

6 files changed

+153
-37
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class SaicApiException(Exception):
2+
def __init__(self, msg: str, return_code: int = None):
3+
if return_code is not None:
4+
self.message = f'return code: {return_code}, message: {msg}'
5+
else:
6+
self.message = msg
7+
8+
def __str__(self):
9+
return self.message

src/saic_ismart_client/rest_v2/__init__.py

Whitespace-only changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from datetime import datetime, timezone
2+
3+
import requests
4+
5+
from saic_ismart_client.exceptions import SaicApiException
6+
from saic_ismart_client.rest_v2.model import TimeZoneResponse
7+
8+
9+
class SaicRestV2Api():
10+
11+
def __init__(self, base_uri: str):
12+
self.__base_uri = base_uri
13+
14+
def get_user_timezone(self, token: str, uid: str):
15+
response = TimeZoneResponse()
16+
self.__execute_get('api.app/v1/user/timezone', token, uid=uid, response_holder=response)
17+
return response
18+
19+
def __execute_get(self, endpoint: str, token: str, uid=None, response_holder=None):
20+
headers = self.__get_headers(token, uid)
21+
try:
22+
response = requests.get(url=f'{self.__base_uri}/{endpoint}', headers=headers)
23+
if response_holder is None:
24+
return response.content.decode()
25+
else:
26+
return response_holder.init_from_dict(response.json())
27+
except requests.exceptions.ConnectionError as ece:
28+
raise SaicApiException(f'Connection error: {ece}')
29+
except requests.exceptions.Timeout as et:
30+
raise SaicApiException(f'Timeout error: {et}')
31+
except requests.exceptions.HTTPError as ehttp:
32+
status_code = ehttp.response.status_code
33+
raise SaicApiException(f'HTTP error. HTTP status: {status_code}, {ehttp}')
34+
except requests.exceptions.RequestException as e:
35+
raise SaicApiException(f'{e}')
36+
37+
def __get_headers(self, token, uid=None):
38+
headers = {
39+
'Content-Type': 'application/json;charset=UTF-8',
40+
'APP-SEND-DATE': str(datetime.now().replace(tzinfo=timezone.utc).timestamp() * 1000),
41+
'APP-CONTENT-ENCRYPTED': '0',
42+
'APP-LANGUAGE-TYPE': 'en',
43+
'APP-LOGIN-TOKEN': token,
44+
}
45+
if uid:
46+
headers['APP-USER-ID'] = uid
47+
return headers
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import abc
2+
import datetime
3+
import re
4+
5+
6+
class BaseData(abc.ABC):
7+
def __init__(self):
8+
self.code: int | None = None
9+
self.message: str | None = None
10+
11+
def init_from_dict(self, data: dict):
12+
self.code = int(data.get('code'))
13+
self.message = data.get('message')
14+
return self
15+
16+
17+
TZ_REGEX = re.compile(r'^(?P<base>GMT|UTC)(?P<sign>[+-])(?P<hour>\d{1,2})(:(?P<minute>\d{2}))?$')
18+
19+
20+
class TimeZoneEntity():
21+
def __init__(self):
22+
self.timezone = None
23+
24+
def init_from_dict(self, data: dict):
25+
self.timezone = data.get('timezone')
26+
return self
27+
28+
def __str__(self):
29+
return f'{{"timezone": "{self.timezone}"}}'
30+
31+
def get_timezone_offset(self):
32+
m = TZ_REGEX.match(self.timezone)
33+
if m is not None:
34+
sign = m.group('sign')
35+
hours = int(m.group('hour'))
36+
minutes = int(m.group('minute')) if m.group('minute') is not None else 0
37+
if sign == '+':
38+
offset = datetime.timedelta(hours=hours, minutes=minutes)
39+
else:
40+
offset = -datetime.timedelta(hours=hours, minutes=minutes)
41+
return datetime.timezone(offset=offset, name=self.timezone)
42+
else:
43+
raise ValueError(f'Invalid timezone: {self.timezone}')
44+
45+
46+
class TimeZoneResponse(BaseData):
47+
def __init__(self):
48+
super().__init__()
49+
self.data: TimeZoneEntity | None = None
50+
51+
def init_from_dict(self, data: dict):
52+
super().init_from_dict(data)
53+
self.data = TimeZoneEntity().init_from_dict(data.get('data'))
54+
return self
55+
56+
def __str__(self):
57+
return f'{{"code": {self.code}, "message": "{self.message}", "data": {self.data}}}'

src/saic_ismart_client/saic_api.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from saic_ismart_client.common_model import AbstractMessage, AbstractMessageBody, Header, MessageBodyV2, MessageV2, \
1313
ScheduledChargingMode, TargetBatteryCode, ChargeCurrentLimitCode
14+
from saic_ismart_client.exceptions import SaicApiException
1415
from saic_ismart_client.ota_v1_1.Message import MessageCoderV11
1516
from saic_ismart_client.ota_v1_1.data_model import AbortSendMessageReq, AlarmSwitch, AlarmSwitchReq, Message, \
1617
MessageBodyV11, MessageListReq, MessageListResp, MessageV11, MpAlarmSettingType, MpUserLoggingInReq, \
@@ -21,6 +22,8 @@
2122
from saic_ismart_client.ota_v3_0.Message import MessageBodyV30, MessageCoderV30, MessageV30
2223
from saic_ismart_client.ota_v3_0.data_model import OtaChrgCtrlReq, OtaChrgCtrlStsResp, OtaChrgHeatReq, \
2324
OtaChrgHeatResp, OtaChrgMangDataResp, OtaChrgRsvanReq, OtaChrgSetngReq, OtaChrgSetngResp, OtaChrgRsvanResp
25+
from saic_ismart_client.rest_v2.api import SaicRestV2Api
26+
from saic_ismart_client.rest_v2.model import TimeZoneEntity
2427

2528
UID_INIT = '0000000000000000000000000000000000000000000000000#'
2629
AVG_SMS_DELIVERY_TIME = 15
@@ -64,19 +67,15 @@ def convert(message: Message) -> SaicMessage:
6467
message.vin)
6568

6669

67-
class SaicApiException(Exception):
68-
def __init__(self, msg: str, return_code: int = None):
69-
if return_code is not None:
70-
self.message = f'return code: {return_code}, message: {msg}'
71-
else:
72-
self.message = msg
73-
74-
def __str__(self):
75-
return self.message
76-
77-
7870
class SaicApi:
79-
def __init__(self, saic_uri: str, saic_user: str, saic_password: str, relogin_delay: int = None):
71+
def __init__(
72+
self,
73+
saic_uri: str,
74+
saic_rest_uri: str,
75+
saic_user: str,
76+
saic_password: str,
77+
relogin_delay: int = None
78+
):
8079
self.saic_uri = saic_uri
8180
self.saic_user = saic_user
8281
self.saic_password = saic_password
@@ -87,6 +86,7 @@ def __init__(self, saic_uri: str, saic_user: str, saic_password: str, relogin_de
8786
self.message_v1_1_coder = MessageCoderV11()
8887
self.message_V2_1_coder = MessageCoderV21()
8988
self.message_V3_0_coder = MessageCoderV30()
89+
self.rest_v2_api = SaicRestV2Api(saic_rest_uri)
9090
self.cookies = None
9191
self.uid = ''
9292
self.token = ''
@@ -845,6 +845,9 @@ def get_token(self):
845845
self.login()
846846
return self.token
847847

848+
def get_user_timezone(self):
849+
return self.rest_v2_api.get_user_timezone(self.get_token(), self.uid)
850+
848851
def handle_error(self, message_body: AbstractMessageBody, iteration: int):
849852
if iteration > 0:
850853
waiting_time = AVG_SMS_DELIVERY_TIME * iteration

tests/test_saic_api.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from saic_ismart_client.ota_v1_1.data_model import MessageV11, MpUserLoggingInRsp, MessageBodyV11, VinInfo, \
1111
MpAlarmSettingType
1212
from saic_ismart_client.ota_v2_1.Message import MessageCoderV21
13-
from saic_ismart_client.ota_v2_1.data_model import OtaRvmVehicleStatusResp25857, RvsPosition, RvsWayPoint,\
13+
from saic_ismart_client.ota_v2_1.data_model import OtaRvmVehicleStatusResp25857, RvsPosition, RvsWayPoint, \
1414
RvsWgs84Point, Timestamp4Short, RvsBasicStatus25857, OtaRvcStatus25857
1515
from saic_ismart_client.ota_v3_0.Message import MessageBodyV30, MessageV30, MessageCoderV30
1616
from saic_ismart_client.ota_v3_0.data_model import OtaChrgMangDataResp, RvsChargingStatus
@@ -48,29 +48,29 @@ def create_vin_info(vin: str) -> VinInfo:
4848
vin_info.brand_name = b'brandName'
4949
vin_info.model_name = b'modelName'
5050
vin_info.active = True
51-
vin_info.model_configuration_json_str = 'name:Tire pressure monitoring system,code:J17,value:1;'\
52-
+ 'name:Regular airbags,code:Q00,value:1;'\
53-
+ 'name:Front-seat airbags,code:Q01,value:1;'\
54-
+ 'name:Airbag switch,code:Q09,value:1;'\
55-
+ 'name:Sun Roof,code:S35,value:0;'\
56-
+ 'name:Remote control,code:S61,value:1;'\
57-
+ 'name:Air conditioning,code:T11,value:1;'\
58-
+ 'name:Electric Power Steering,code:EPS,value:1;'\
59-
+ 'name:Security alert,code:SA64,value:0111110000000000001000000100101000000010100000000000000000000110;'\
60-
+ 'name:Bonnut Status,code:BONNUT,value:1;'\
61-
+ 'name:Door Status,code:DOOR,value:1111;'\
62-
+ 'name:Boot Status,code:BOOT,value:1;'\
63-
+ 'name:Engine Status,code:ENGINE,value:1;'\
64-
+ 'name:Electric Vehicle,code:EV,value:0;'\
65-
+ 'name:HeatedSeat,code:HeatedSeat,value:0;'\
66-
+ 'name:Key Position,code:KEYPOS,value:1;'\
67-
+ 'name:Energy state,code:ENERGY,value:0;'\
68-
+ 'name:Battery Voltage,code:BATTERY,value:1;'\
69-
+ 'name:Interior Temperature,code:INTEMP,value:1;'\
70-
+ 'name:Exterior Temperature,code:EXTEMP,value:1;'\
71-
+ 'name:Window Status,code:WINDOW,value:0000;'\
72-
+ 'name:Left-Right Driving,code:LRD,value:0;'\
73-
+ 'name:Bluetooth Key,code:BTKEY,value:0;'\
51+
vin_info.model_configuration_json_str = 'name:Tire pressure monitoring system,code:J17,value:1;' \
52+
+ 'name:Regular airbags,code:Q00,value:1;' \
53+
+ 'name:Front-seat airbags,code:Q01,value:1;' \
54+
+ 'name:Airbag switch,code:Q09,value:1;' \
55+
+ 'name:Sun Roof,code:S35,value:0;' \
56+
+ 'name:Remote control,code:S61,value:1;' \
57+
+ 'name:Air conditioning,code:T11,value:1;' \
58+
+ 'name:Electric Power Steering,code:EPS,value:1;' \
59+
+ 'name:Security alert,code:SA64,value:0111110000000000001000000100101000000010100000000000000000000110;' \
60+
+ 'name:Bonnut Status,code:BONNUT,value:1;' \
61+
+ 'name:Door Status,code:DOOR,value:1111;' \
62+
+ 'name:Boot Status,code:BOOT,value:1;' \
63+
+ 'name:Engine Status,code:ENGINE,value:1;' \
64+
+ 'name:Electric Vehicle,code:EV,value:0;' \
65+
+ 'name:HeatedSeat,code:HeatedSeat,value:0;' \
66+
+ 'name:Key Position,code:KEYPOS,value:1;' \
67+
+ 'name:Energy state,code:ENERGY,value:0;' \
68+
+ 'name:Battery Voltage,code:BATTERY,value:1;' \
69+
+ 'name:Interior Temperature,code:INTEMP,value:1;' \
70+
+ 'name:Exterior Temperature,code:EXTEMP,value:1;' \
71+
+ 'name:Window Status,code:WINDOW,value:0000;' \
72+
+ 'name:Left-Right Driving,code:LRD,value:0;' \
73+
+ 'name:Bluetooth Key,code:BTKEY,value:0;' \
7474
+ 'name:Battery Type,code:BType,value:2'
7575
return vin_info
7676

@@ -262,7 +262,7 @@ def res():
262262

263263
class TestSaicApi(TestCase):
264264
def setUp(self) -> None:
265-
self.saic_api = SaicApi('https://tap-eu.soimt.com', '[email protected]', 'secret')
265+
self.saic_api = SaicApi('https://tap-eu.soimt.com', 'https://gateway-eu.soimt.com', '[email protected]', 'secret')
266266
self.message_coder_v1_1 = MessageCoderV11()
267267
self.message_coder_v2_1 = MessageCoderV21()
268268
self.message_coder_v3_0 = MessageCoderV30()

0 commit comments

Comments
 (0)