-
Notifications
You must be signed in to change notification settings - Fork 26
WIP: New plugin - smartcar #451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
30d5315
5bbc364
7c33225
d7fb145
3cabc63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| smartcar/integrationpluginsmartcar.json usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/smartcar/ | ||
| smartcar/integrationpluginsmartcar.py usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/smartcar/ | ||
| smartcar/requirements.txt usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/smartcar/ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here, the .qm files need to be added |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| { | ||
| "name": "smartcar", | ||
| "displayName": "Smartcar", | ||
| "id": "f33bb38d-640e-4b15-a39e-f0e7b2cc6993", | ||
| "paramTypes": [ | ||
| { | ||
| "id": "70e2c7a1-55b4-47fd-83b5-549de30b1391", | ||
| "name": "testMode", | ||
| "displayName": "Test Mode", | ||
| "type": "bool", | ||
| "defaultValue": false | ||
| }, | ||
| { | ||
| "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"], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an "electricvehicle" interface exists nowadays which should be implemented in this one. |
||
| "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" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| 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" | ||
|
|
||
| testMode = configValue(smartcarPluginTestModeParamTypeId) | ||
| logger.log("Test mode enabled: ", testMode) | ||
|
|
||
| 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=testMode | ||
| ) | ||
| 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): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nymea's python api gained support for |
||
| 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'] | ||
|
|
||
|
|
||
| def configValueChanged(paramTypeId, value): | ||
| if paramTypeId == smartcarPluginTestModeParamTypeId: | ||
| logger.log("Test mode enabled changed to: ", value) | ||
| #should retrigger a setup somehow here | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest nymea-plugins doesn't have a single translations package any more but every plugin will ship its own translations within the pluign package.