Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,23 @@ Description: nymea.io plugin for Solar-Log
This package will install the nymea.io plugin for Solar-Log.


Package: nymea-plugin-smartcar
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Copy link
Contributor

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.

python3-setuptools,
python3-pip,
Description: nymea.io plugin for smartcar
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install the nymea.io plugin for Smartcar (see https://smartcar.com/).


Package: nymea-plugin-tasmota
Architecture: any
Depends: ${shlibs:Depends},
Expand Down
3 changes: 3 additions & 0 deletions debian/nymea-plugin-smartcar.install.in
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/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here, the .qm files need to be added

142 changes: 142 additions & 0 deletions smartcar/integrationpluginsmartcar.json
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"],
Copy link
Contributor

Choose a reason for hiding this comment

The 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"
}
]
}
]
}
]
}
213 changes: 213 additions & 0 deletions smartcar/integrationpluginsmartcar.py
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nymea's python api gained support for nymea.PluginTimer. No need to start own threads any more.

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

Loading