mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-07-20 01:02:46 +02:00
204 lines
8.0 KiB
Python
204 lines
8.0 KiB
Python
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']
|
|
|