mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-10-21 14:23:24 +02:00
Added new plugin for smartcar.
This commit is contained in:
parent
eff66ea2f4
commit
30d5315d1f
135
smartcar/integrationpluginsmartcar.json
Normal file
135
smartcar/integrationpluginsmartcar.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
204
smartcar/integrationpluginsmartcar.py
Normal file
204
smartcar/integrationpluginsmartcar.py
Normal 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']
|
||||||
|
|
||||||
4
smartcar/requirements.txt
Normal file
4
smartcar/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Flask==1.0.2
|
||||||
|
flask-cors==3.0.9
|
||||||
|
requests==2.20.1
|
||||||
|
smartcar==5.2.1
|
||||||
Loading…
x
Reference in New Issue
Block a user