1+ import nymea
2+ import smartcar
3+ import urllib .parse as urlparse
4+ from urllib .parse import parse_qs
5+ import threading
6+ import time
7+ import json
8+ import datetime
9+
10+ smartcarClient = None
11+ defaultPollTimerInterval = 20 # will be read from settings
12+ pollTimers = {}
13+ oauthSessions = {}
14+
15+ def init ():
16+ logger .log ("Smartcar init" )
17+
18+ def deinit ():
19+ # If we started any poll timers, cancel at shutdown
20+ for timerId in pollTimers :
21+ if timerId in pollTimers and pollTimers [timerId ] is not None :
22+ pollTimers [timerId ].cancel ()
23+
24+ def startPairing (info ):
25+ logger .log ("Start pairing" )
26+
27+ global smartcarClient
28+ global oauthSessions
29+
30+ if info .thingClassId == smartcarAccountThingClassId :
31+ logger .log ("Starting pairing: " , smartcarAccountThingClassId )
32+ smartcarClient = createSmartcarClient ()
33+ logger .log ("Auth URL: " , smartcarClient .get_auth_url ())
34+ oauthSessions [info .transactionId ] = smartcarClient
35+ info .oAuthUrl = smartcarClient .get_auth_url ()
36+ info .finish (nymea .ThingErrorNoError )
37+ else :
38+ logger .log ("Unhandled pairing method" )
39+ info .finish (nymea .ThingErrorCreationMethodNotSupported )
40+
41+ def confirmPairing (info , user , secret ):
42+ logger .log ("Confirm pairing..." )
43+ if info .thingClassId == smartcarAccountThingClassId :
44+ parsed = urlparse .urlparse (secret )
45+ code = parse_qs (parsed .query )['code' ][0 ]
46+ accessToken = smartcarClient .exchange_code (code )
47+
48+ saveToken (info .thingId , accessToken )
49+ del oauthSessions [info .transactionId ]
50+ info .finish (nymea .ThingErrorNoError )
51+ else :
52+ info .finish (nymea .ThingErrorCreationMethodNotSupported )
53+
54+ def setupThing (info ):
55+ logger .log ("Setting up thing:" , info .thing .name )
56+
57+ # setup the account (login and discover vehicles)
58+ if info .thing .thingClassId == smartcarAccountThingClassId :
59+ logger .log ("Setting up the account" )
60+ # deal with the connection / auth
61+ try :
62+ client = createSmartcarClient ()
63+ accessToken = getAccessToken (client , info .thing .id )
64+ info .finish (nymea .ThingErrorNoError )
65+ except Exception as e :
66+ logger .error ("Error setting up smartcar account: " , str (e ))
67+ info .finish (nymea .ThingErrorAuthenticationFailure , str (e ))
68+ return
69+
70+ info .thing .setStateValue (smartcarAccountLoggedInStateTypeId , True )
71+ info .thing .setStateValue (smartcarAccountConnectedStateTypeId , True )
72+ vehicle_ids = smartcar .get_vehicle_ids (accessToken )['vehicles' ]
73+
74+ thingDescriptors = []
75+ for raw_vehicle in vehicle_ids :
76+ found = False
77+ vehicle = smartcar .Vehicle (raw_vehicle , accessToken )
78+ logger .log (vehicle .info ())
79+ logger .log ("-----------------" )
80+ for thing in myThings ():
81+ if thing .thingClassId == vehicleThingClassId and thing .paramValue (vehicleThingVehicleidParamTypeId ) == vehicle .info ()['id' ]:
82+ logger .log ("Vehicle already added: " , vehicle .info ()['id' ])
83+ found = True
84+ break
85+ if found :
86+ continue
87+
88+ logger .log ("Adding new vehicle to the system: " , vehicle .info ()['id' ], " parent id: " , info .thing .id )
89+ thingDescriptor = nymea .ThingDescriptor (vehicleThingClassId , vehicle .info ()['model' ], parentId = info .thing .id )
90+ thingDescriptor .params = [
91+ nymea .Param (vehicleThingVehicleidParamTypeId , vehicle .info ()['id' ]),
92+ nymea .Param (vehicleThingMakeParamTypeId , vehicle .info ()['make' ])
93+ ]
94+ thingDescriptors .append (thingDescriptor )
95+
96+ logger .log ("New vehicles appeared" )
97+ autoThingsAppeared (thingDescriptors )
98+ createPollTimer (info .thing )
99+ info .thing .settingChangedHandler = socRefreshRateChanged
100+ return
101+
102+ # setup individual vehicles
103+ if info .thing .thingClassId == vehicleThingClassId :
104+ logger .log ("Should setup vehicle here: " , info .thing .name )
105+ info .finish (nymea .ThingErrorNoError )
106+
107+
108+ def postSetupThing (thing ):
109+ logger .log ("postSetupThing" )
110+
111+ def thingRemoved (thing ):
112+ logger .log ("thingRemoved:" , thing .name )
113+
114+ def createSmartcarClient ():
115+ apiKey = apiKeyStorage ().requestKey ("smartcar" )
116+ clientId = apiKey .data ("clientId" )
117+ clientSecret = apiKey .data ("clientSecret" )
118+ redirectUri = "https://127.0.0.1:8888"
119+
120+ testMode = configValue (smartcarPluginTestModeParamTypeId )
121+ logger .log ("Test mode enabled: " , testMode )
122+
123+ smartcarClient = smartcar .AuthClient (
124+ client_id = clientId ,
125+ client_secret = clientSecret ,
126+ redirect_uri = redirectUri ,
127+ scope = ['required:read_vehicle_info' , 'required:read_battery' , 'required:read_charge' ],
128+ test_mode = testMode
129+ )
130+ return smartcarClient
131+
132+
133+ def socRefreshRateChanged (thing , paramTypeId , value ):
134+ if paramTypeId == smartcarAccountSettingsSocRefreshPeriodParamTypeId :
135+ logger .log ("Refresh rate changed for " , thing .id , " , new value: " , value )
136+
137+ if (thing .id in pollTimers ) and (pollTimers [thing .id ] is not None ):
138+ logger .log ("Timer already exists, cancelling it first" )
139+ pollTimers [thing .id ].cancel ()
140+
141+ pollTimers [thing .id ] = threading .Timer (value , pollService , [thing .id ])
142+ pollTimers [thing .id ].start ()
143+
144+ def createPollTimer (thing ):
145+ timerId = thing .id
146+ if (timerId not in pollTimers ) or (pollTimers [timerId ] is None ):
147+ pollTimers [timerId ] = threading .Timer (5 , pollService , [thing .id ])
148+ pollTimers [timerId ].start ()
149+
150+ def pollService (parentThingId ):
151+ socRefreshSettingValue = defaultPollTimerInterval
152+ for thing in myThings ():
153+ if thing .parentId == parentThingId and thing .thingClassId == vehicleThingClassId :
154+ try :
155+ refreshVehicleSOC (createSmartcarClient (), thing )
156+ except :
157+ logger .error ("Error refreshing vehicle SOC" )
158+ if thing .id == parentThingId :
159+ socRefreshSettingValue = thing .setting (smartcarAccountSettingsSocRefreshPeriodParamTypeId )
160+
161+ # pollTimerInterval can be modified in settings to refresh the SoC faster
162+ # when needed for a demo. When not needed, this value should be higher (e.g. 120 sec)
163+ # because the demo account has an API call quota whose limit can be easily reahed
164+ # when using high refresh rates.
165+ pollTimerInterval = socRefreshSettingValue or defaultPollTimerInterval
166+ pollTimers [parentThingId ] = threading .Timer (pollTimerInterval , pollService , [parentThingId ])
167+ pollTimers [parentThingId ].start ()
168+
169+ def refreshVehicleSOC (client , thing ):
170+ vid = thing .paramValue (vehicleThingVehicleidParamTypeId )
171+ logger .log ("Refreshing SOC for vehicle: " , vid )
172+ vehicle = smartcar .Vehicle (vid , getAccessToken (client , thing .parentId ))
173+
174+ # update the state of charge (battery level)
175+ soc = vehicle .battery ()
176+ logger .log (soc )
177+ percentage = soc ["data" ]["percentRemaining" ]
178+ thing .setStateValue (vehicleBatteryLevelStateTypeId , percentage * 100 )
179+ if percentage * 100 < 10 :
180+ thing .setStateValue (vehicleBatteryCriticalStateTypeId , True )
181+ else :
182+ thing .setStateValue (vehicleBatteryCriticalStateTypeId , False )
183+
184+ # update charing status
185+ chargingState = vehicle .charge ()
186+ logger .log (chargingState )
187+ thing .setStateValue (vehiclePluggedInStateTypeId , chargingState ["data" ]["isPluggedIn" ])
188+ thing .setStateValue (vehicleChargingStateTypeId , chargingState ["data" ]["state" ] == "CHARGING" )
189+
190+
191+ def saveToken (thingId , token ):
192+ pluginStorage ().beginGroup (thingId )
193+ pluginStorage ().setValue ("token" , json .dumps (token , default = str ))
194+ pluginStorage ().endGroup ()
195+
196+ def getAccessToken (client , thingId ):
197+ pluginStorage ().beginGroup (thingId )
198+ token = json .loads (pluginStorage ().value ("token" ))
199+ pluginStorage ().endGroup ()
200+
201+ logger .log ("Retrieved token from pluginStorage: " , token )
202+ expiration_date_time_obj = datetime .datetime .strptime (token ['expiration' ], '%Y-%m-%d %H:%M:%S.%f' )
203+ if smartcar .is_expired (expiration_date_time_obj ):
204+ token = client .exchange_refresh_token (token ['refresh_token' ])
205+ saveToken (thingId , token )
206+ return token ['access_token' ]
207+
208+
209+ def configValueChanged (paramTypeId , value ):
210+ if paramTypeId == smartcarPluginTestModeParamTypeId :
211+ logger .log ("Test mode enabled changed to: " , value )
212+ #should retrigger a setup somehow here
213+
0 commit comments