Added new plugin for smartcar.

This commit is contained in:
Paul Bot 2021-07-01 15:05:03 +03:00 committed by Michael Zanetti
parent eff66ea2f4
commit 30d5315d1f
3 changed files with 343 additions and 0 deletions

View File

@ -0,0 +1,135 @@
{
"name": "smartcar",
"displayName": "Smartcar",
"id": "f33bb38d-640e-4b15-a39e-f0e7b2cc6993",
"paramTypes": [
{
"id": "bb5ee037-5034-4921-b1d8-3936bc515fa6",
"name": "customClientId",
"displayName": "Client ID",
"defaultValue": "",
"type": "QString"
},
{
"id": "b60c7f7b-8175-437c-8056-8258cc8a3bd5",
"name": "customClientSecret",
"displayName": "Client secret",
"defaultValue": "",
"type": "QString"
}
],
"apiKeys": ["smartcar"],
"vendors": [
{
"name": "smartcar",
"displayName": "Smartcar",
"id": "e52d223f-910d-47a0-a0e4-8f0718d453cd",
"thingClasses": [
{
"name": "smartcarAccount",
"displayName": "Smartcar",
"id": "da78acf9-a64c-4976-8344-26b8e68dcb5f",
"setupMethod": "oauth",
"createMethods": ["user"],
"interfaces": ["account"],
"paramTypes": [
],
"settingsTypes": [
{
"id": "ca4f36ac-b202-4a87-8c17-d59a6e4cf07b",
"name": "socRefreshPeriod",
"displayName": "SOC Refresh Period",
"type": "uint",
"unit": "Seconds",
"defaultValue": 120,
"minValue": 10
}
],
"stateTypes":[
{
"id": "4b7f54bc-460c-4bfa-a4e5-032d5ff3c984",
"name": "connected",
"displayName": "Connected",
"displayNameEvent": "Connected changed",
"defaultValue": true,
"cached": false,
"type": "bool"
},
{
"id": "d03ce680-ebf4-45f1-950a-586b3f026119",
"name": "loggedIn",
"displayName": "Logged in",
"displayNameEvent": "Logged in changed",
"defaultValue": true,
"type": "bool"
}
],
"actionTypes":[
],
"eventTypes":[
]
},
{
"name": "vehicle",
"displayName": "Vehicle",
"id": "77ddd9f7-7c16-4bf3-bff0-c3a2208c8c66",
"createMethods": ["auto"],
"interfaces": ["battery"],
"paramTypes": [
{
"id": "e78e6157-e884-43d0-9f50-6e433ce25ed4",
"name": "make",
"displayName": "Make",
"defaultValue": "-",
"type": "QString"
},
{
"id": "33d63299-f3c0-4c07-a423-85233f2e11a0",
"name": "vehicleid",
"displayName": "Vehicle ID",
"defaultValue": "-",
"type": "QString"
}
],
"stateTypes":[
{
"id": "b772b264-5267-41b0-8806-ecc4351bc94a",
"name": "batteryLevel",
"displayName": "State of Charge",
"displayNameEvent": "Battery changed",
"type": "int",
"unit": "Percentage",
"defaultValue": 0,
"minValue": 0,
"maxValue": 100
},
{
"id": "297aed0b-72ce-4d04-a280-22d00e0389d8",
"name": "batteryCritical",
"displayName": "State of Charge Critical",
"displayNameEvent": "Battery critical changed",
"type": "bool",
"defaultValue": false
},
{
"id": "fbf040a8-90aa-4d4f-8885-39f4bfcf0b00",
"name": "pluggedIn",
"displayName": "Plugged In",
"displayNameEvent": "Plugged In state changed",
"type": "bool",
"defaultValue": "false"
},
{
"id": "056ac150-c31b-4c3b-8a8c-4a009e503eb9",
"name": "charging",
"displayName": "Charging",
"displayNameEvent": "Charging state changed",
"type": "bool",
"defaultValue": "false"
}
]
}
]
}
]
}

View File

@ -0,0 +1,204 @@
import nymea
import smartcar
import urllib.parse as urlparse
from urllib.parse import parse_qs
import threading
import time
import json
import datetime
smartcarClient = None
defaultPollTimerInterval = 20 # will be read from settings
pollTimers = {}
oauthSessions = {}
def init():
logger.log("Smartcar init")
def deinit():
# If we started any poll timers, cancel at shutdown
for timerId in pollTimers:
if timerId in pollTimers and pollTimers[timerId] is not None:
pollTimers[timerId].cancel()
def startPairing(info):
logger.log("Start pairing")
global smartcarClient
global oauthSessions
if info.thingClassId == smartcarAccountThingClassId:
logger.log("Starting pairing: ", smartcarAccountThingClassId)
smartcarClient = createSmartcarClient()
logger.log("Auth URL: ", smartcarClient.get_auth_url())
oauthSessions[info.transactionId] = smartcarClient
info.oAuthUrl = smartcarClient.get_auth_url()
info.finish(nymea.ThingErrorNoError)
else:
logger.log("Unhandled pairing method")
info.finish(nymea.ThingErrorCreationMethodNotSupported)
def confirmPairing(info, user, secret):
logger.log("Confirm pairing...")
if info.thingClassId == smartcarAccountThingClassId:
parsed = urlparse.urlparse(secret)
code = parse_qs(parsed.query)['code'][0]
accessToken = smartcarClient.exchange_code(code)
saveToken(info.thingId, accessToken)
del oauthSessions[info.transactionId]
info.finish(nymea.ThingErrorNoError)
else:
info.finish(nymea.ThingErrorCreationMethodNotSupported)
def setupThing(info):
logger.log("Setting up thing:", info.thing.name)
# setup the account (login and discover vehicles)
if info.thing.thingClassId == smartcarAccountThingClassId:
logger.log("Setting up the account")
# deal with the connection / auth
try:
client = createSmartcarClient()
accessToken = getAccessToken(client, info.thing.id)
info.finish(nymea.ThingErrorNoError)
except Exception as e:
logger.error("Error setting up smartcar account: ", str(e))
info.finish(nymea.ThingErrorAuthenticationFailure, str(e))
return
info.thing.setStateValue(smartcarAccountLoggedInStateTypeId, True)
info.thing.setStateValue(smartcarAccountConnectedStateTypeId, True)
vehicle_ids = smartcar.get_vehicle_ids(accessToken)['vehicles']
thingDescriptors = []
for raw_vehicle in vehicle_ids:
found = False
vehicle = smartcar.Vehicle(raw_vehicle, accessToken)
logger.log(vehicle.info())
logger.log("-----------------")
for thing in myThings():
if thing.thingClassId == vehicleThingClassId and thing.paramValue(vehicleThingVehicleidParamTypeId) == vehicle.info()['id']:
logger.log("Vehicle already added: ", vehicle.info()['id'])
found = True
break
if found:
continue
logger.log("Adding new vehicle to the system: ", vehicle.info()['id'], " parent id: ", info.thing.id)
thingDescriptor = nymea.ThingDescriptor(vehicleThingClassId, vehicle.info()['model'], parentId=info.thing.id)
thingDescriptor.params = [
nymea.Param(vehicleThingVehicleidParamTypeId, vehicle.info()['id']),
nymea.Param(vehicleThingMakeParamTypeId, vehicle.info()['make'])
]
thingDescriptors.append(thingDescriptor)
logger.log("New vehicles appeared")
autoThingsAppeared(thingDescriptors)
createPollTimer(info.thing)
info.thing.settingChangedHandler = socRefreshRateChanged
return
# setup individual vehicles
if info.thing.thingClassId == vehicleThingClassId:
logger.log("Should setup vehicle here: ", info.thing.name)
info.finish(nymea.ThingErrorNoError)
def postSetupThing(thing):
logger.log("postSetupThing")
def thingRemoved(thing):
logger.log("thingRemoved:", thing.name)
def createSmartcarClient():
apiKey = apiKeyStorage().requestKey("smartcar")
clientId = apiKey.data("clientId")
clientSecret = apiKey.data("clientSecret")
redirectUri = "https://127.0.0.1:8888"
smartcarClient = smartcar.AuthClient(
client_id=clientId,
client_secret=clientSecret,
redirect_uri=redirectUri,
scope=['required:read_vehicle_info', 'required:read_battery', 'required:read_charge'],
test_mode=True
)
return smartcarClient
def socRefreshRateChanged(thing, paramTypeId, value):
if paramTypeId == smartcarAccountSettingsSocRefreshPeriodParamTypeId:
logger.log("Refresh rate changed for ", thing.id, " , new value: ", value)
if (thing.id in pollTimers) and (pollTimers[thing.id] is not None):
logger.log("Timer already exists, cancelling it first")
pollTimers[thing.id].cancel()
pollTimers[thing.id] = threading.Timer(value, pollService, [thing.id])
pollTimers[thing.id].start()
def createPollTimer(thing):
timerId = thing.id
if (timerId not in pollTimers) or (pollTimers[timerId] is None):
pollTimers[timerId] = threading.Timer(5, pollService, [thing.id])
pollTimers[timerId].start()
def pollService(parentThingId):
socRefreshSettingValue = defaultPollTimerInterval
for thing in myThings():
if thing.parentId == parentThingId and thing.thingClassId == vehicleThingClassId:
try:
refreshVehicleSOC(createSmartcarClient(), thing)
except:
logger.error("Error refreshing vehicle SOC")
if thing.id == parentThingId:
socRefreshSettingValue = thing.setting(smartcarAccountSettingsSocRefreshPeriodParamTypeId)
# pollTimerInterval can be modified in settings to refresh the SoC faster
# when needed for a demo. When not needed, this value should be higher (e.g. 120 sec)
# because the demo account has an API call quota whose limit can be easily reahed
# when using high refresh rates.
pollTimerInterval = socRefreshSettingValue or defaultPollTimerInterval
pollTimers[parentThingId] = threading.Timer(pollTimerInterval, pollService, [parentThingId])
pollTimers[parentThingId].start()
def refreshVehicleSOC(client, thing):
vid = thing.paramValue(vehicleThingVehicleidParamTypeId)
logger.log("Refreshing SOC for vehicle: ", vid)
vehicle = smartcar.Vehicle(vid, getAccessToken(client, thing.parentId))
# update the state of charge (battery level)
soc = vehicle.battery()
logger.log(soc)
percentage = soc["data"]["percentRemaining"]
thing.setStateValue(vehicleBatteryLevelStateTypeId, percentage * 100)
if percentage * 100 < 10:
thing.setStateValue(vehicleBatteryCriticalStateTypeId, True)
else:
thing.setStateValue(vehicleBatteryCriticalStateTypeId, False)
# update charing status
chargingState = vehicle.charge()
logger.log(chargingState)
thing.setStateValue(vehiclePluggedInStateTypeId, chargingState["data"]["isPluggedIn"])
thing.setStateValue(vehicleChargingStateTypeId, chargingState["data"]["state"] == "CHARGING")
def saveToken(thingId, token):
pluginStorage().beginGroup(thingId)
pluginStorage().setValue("token", json.dumps(token, default=str))
pluginStorage().endGroup()
def getAccessToken(client, thingId):
pluginStorage().beginGroup(thingId)
token = json.loads(pluginStorage().value("token"))
pluginStorage().endGroup()
logger.log("Retrieved token from pluginStorage: ", token)
expiration_date_time_obj = datetime.datetime.strptime(token['expiration'], '%Y-%m-%d %H:%M:%S.%f')
if smartcar.is_expired(expiration_date_time_obj):
token = client.exchange_refresh_token(token['refresh_token'])
saveToken(thingId, token)
return token['access_token']

View File

@ -0,0 +1,4 @@
Flask==1.0.2
flask-cors==3.0.9
requests==2.20.1
smartcar==5.2.1