mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-07-19 00:54:53 +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