Questions about implementation
Closed this issue · 14 comments
Hi,
this is not an issue, but a try to get in contact to you :-)
I have some questions about your code adaption and hope for hints.
In #sendRequest(), the seaaionInfo is passed back as result:
https://github.com/eiannone/tesla-cmd-api/blob/main/src/CarServer.js#L52C23-L52C23
But in #requestAction(), this result is parsed:
https://github.com/eiannone/tesla-cmd-api/blob/main/src/CarServer.js#L90
Attributes like actionStatus and actionStatus.result are checked.
These attributes are not present in my result.
But similar attributes are present in "res" object has some kind of result state.
Do you have an idea how the result can checked?
I tried wird your example to send a charge limit with message body:
But it seems it is not executed. I assume, the res.signedMessageStatus has these properties:
Is this a message status/error code?
Thanks for your help.
Ronny
Hi @RonnyWinkler , before sending any request with an action to CarServer, you must establish an authenticated session, calling the startSession()
method. This is a special request, to which the server responds with a "sessionInfo" response, which is automatically parsed by the CarServer class.
Please check that you are calling await api.startSession()
before calling api.chargingSetLimit()
.
Anyway, the signedMessageStatus you're receiving in the response indicates an error, and code 17 is "Time expired"
Hi, thanks for your answer :-)
I changed the startSession( ) to be sent as first action in requestAction( ).
The reason is, that in this case the car is online (checked before, waked up if it was asleep).
So as first try, both requests (startSession and the request itsel) is sent. For further requests, only the request is sent.
But for the request I get the response shown above.
"Time expired" is irritating. The sessionInfo is ok - no Error is thrown. The car is awake, so no http timeout.
Have you a hint what could be the cause for the error code?
I found a place where a timestamp is used:
class Signer {
constructor(key, verifierName, verifierSessionInfo) {
this.session = new AuthSession(key, verifierSessionInfo.publicKey);
this.verifierName = verifierName;
this.timeZero = Math.floor(Date.now() / 1000) - verifierSessionInfo.clockTime;
this.epoch = Buffer.from(verifierSessionInfo.epoch, 0, 16);
this.counter = verifierSessionInfo.counter;
}
In my environment, the Date.now()
returns the UTC time, not the local time (1 hour lower that local time).
Can this cause the issue?
Do you know what's the meaning of timeZero
end epoch
?
Using local time makes do difference...
...short Update:
I think I run into a expired error while using the debugger.
I saw now the expiresIn
parameter (5 sec?) in this.signer.generateSignature(encodedPayload, this.Domain.DOMAIN_INFOTAINMENT, 5);
When running the app withput debugger, it worked now :-)
Can I set a free expiresIn value or is there a maximum limit? Just to know if the http request would take a while.
Thanks again for your good work. Very helpful to get into the command api syntax.
Just FYI:
To inform the caller about a reqtest timeout error, I added an error check:
CarServer.#sendRequest:
// Update session
if (res.hasOwnProperty('sessionInfo') && res.hasOwnProperty('signatureData')) {
if (!res.signatureData.hasOwnProperty('sessionInfoTag') || !res.signatureData.sessionInfoTag.hasOwnProperty('tag'))
throw new Error('Missing sessionInfo tag');
const sessionInfo = this.sessionInfoProto.decode(res.sessionInfo);
this.signer = await new Signer(this.privateKey, this.vin, sessionInfo);
if (!this.signer.validateSessionInfo(res.sessionInfo, res.requestUuid, res.signatureData.sessionInfoTag.tag)) {
this.signer = null;
throw new Error("Session info hmac invalid");
}
// Return error if frequest timed out
if (res.hasOwnProperty('signedMessageStatus') && res.signedMessageStatus.hasOwnProperty('operationStatus')){
if (res.signedMessageStatus.operationStatus != this.ActionResult.OPERATIONSTATUS_OK){
throw new Error("Signed message error: "+this.MessageFault[res.signedMessageStatus.signedMessageFault]);
}
}
return sessionInfo;
}
I imported these constants (I used instance attributes instead of constants):
this.MessageOperationStatus = this.protoMessage.lookupEnum("UniversalMessage.OperationStatus_E").values;
this.MessageFault = this.protoMessage.lookupEnum("UniversalMessage.MessageFault_E").values;
This way an error is thrown in such an case.
Great thanks, I will update the code ASAP.
I think you can increase the expiresIn argument. And yes, it represents seconds
I added a second check (first "else if"):
This happens e.g. for MESSAGEFAULT_ERROR_INSUFFICIENT_PRIVILEGES. In this case, the sessionInfo is not provided.
if (res.hasOwnProperty('sessionInfo') && res.hasOwnProperty('signatureData')) {
if (!res.signatureData.hasOwnProperty('sessionInfoTag') || !res.signatureData.sessionInfoTag.hasOwnProperty('tag'))
throw new Error('Missing sessionInfo tag');
const sessionInfo = this.sessionInfoProto.decode(res.sessionInfo);
this.signer = await new Signer(this.privateKey, this.vin, sessionInfo);
if (!this.signer.validateSessionInfo(res.sessionInfo, res.requestUuid, res.signatureData.sessionInfoTag.tag)) {
this.signer = null;
throw new Error("Session info hmac invalid");
}
// Return error if request timed out
if (res.hasOwnProperty('signedMessageStatus') && res.signedMessageStatus.hasOwnProperty('operationStatus')){
if (res.signedMessageStatus.operationStatus != this.ActionResult.OPERATIONSTATUS_OK){
throw new Error("Signed message error: "+this.MessageFault[res.signedMessageStatus.signedMessageFault]);
}
}
return sessionInfo;
}
// Return response payload
else if (res.hasOwnProperty('signedMessageStatus') && res.signedMessageStatus.hasOwnProperty('operationStatus')){
// Return error
if (res.signedMessageStatus.operationStatus != this.ActionResult.OPERATIONSTATUS_OK){
throw new Error("Signed message error: "+this.MessageFault[res.signedMessageStatus.signedMessageFault]);
}
}
else if (res.hasOwnProperty('protobufMessageAsBytes')) {
return this.carServerResponseProto.decode(res.protobufMessageAsBytes);
}
else {
throw new Error("Invalid response");
}
Another question - sorry :-)
Simple requests are working (flash lights, charge port).
Other request are blocked with: MESSAGEFAULT_ERROR_INSUFFICIENT_PRIVILEGES
E.g. ION_LOCK/UNLOCK, vehicleControlWindowActionVent/Close, chargingStartStopActionStart/Stop.
Is it needed to request additional scopes? During oAuth, I set all scopes and with REST/Proxy, it's working.
Is it perhaps dependent on the Domain?
await this.#sendRequest({
toDestination: { domain: this.Domain.DOMAIN_INFOTAINMENT },
I found this:
https://github.com/teslamotors/vehicle-command/blob/main/pkg/vehicle/vehicle.go#L129
Is this set like a bitwise OR (DOMAIN_INFOTAINMENT + DOMAIN_INFOTAINMENT )?
Yes, I think we should use a different domain, maybe DOMAIN_VEHICLE_SECURITY.
In the original go code, I see those commands use a different function, i.e. instead of executeCarServerAction
they use executeRKEAction
, which probably has a slightly different request.
Where in the Code can I find both methods`?
I found this example. There they use domain=nil. Based on the description of startSession( ) this should allow both domains.
If domains is nil, then the client will establish connections with all supported vehicle subsystems
I tried to pass null
, but without success.
The proxy seems so store domain based session information. In your code, only one session is stored, right?
So if two different actions for different domains are needed, it's perhaps necessary to store also both sessions.
But I haven't understood the code yet
Update:
Every domain needs its own session. If "nil" is passed, then sessions for all domains are created:
https://github.com/teslamotors/vehicle-command/blob/bd191bbf33d01615da2c98314b87bdccf142c484/internal/dispatcher/dispatcher.go#L98
And I assume the commands are sent with one of the domain. So when sending, the sessions must be loaded based on the session. That's what I interpreted from the code so far...
Hi @eiannone ,
I assume you're still working on the domain bug?
Please send me a short update if I can help with some tests.
I also would like to contribute with some commands (including parameters) I discovered and tested already. If it's ok I will create an issue for to create a small documentation.
Kind Regards
Ronny
Hi @RonnyWinkler, unfortunately I've paused working on this project for now, but I hope to resume it in a few weeks.
I can give you some hints: some commands need to be forwarded to the security domain instead of infotainment. Specifically, the commands listed in file https://github.com/teslamotors/vehicle-command/blob/main/pkg/vehicle/security.go.
If you look at the original go code for the dispatcher (https://github.com/teslamotors/vehicle-command/blob/main/internal/dispatcher/dispatcher.go), you can see that it starts two sessions at the same time (method Dispatcher::StartSessions()
), one for Domain.DOMAIN_INFOTAINMENT
and one for Domain.DOMAIN_VEHICLE_SECURITY
.
Then, each message must be sent to the appropriate domain.
For simplicity, I used only the one for infotainment.
So, in node.js, you should modify CarServer.js
class, updating both startSession()
and #requestAction()
methods, passing, the correct domain. For example we could pass it as an argument to both methods.
Feel free to submit a pull request if you want.
I'm now trying to port the Hermes protocol discovered by Lotharbach from Go to Node.js (see here: timdorr/tesla-api#769), which should allow sending signed commands using the owner api instead of fleet api, bypassing the need to register a business app, and bypassing also the limitations of the fleet api.
I should be able to use my code to sign the messages, but send them through Hermes protocol. Currently I'm stuck with websocket communication, I'm quite new to using it in node.js.
Hi and thanks for your update. I will adapt the domain handling.
I don't know if I can create a PR because I hade to make some changes to be able to include it in my app.
- no await calls outside the class.
- moved the global static variables into an async contructor and stored as instance attributes.
https://github.com/RonnyWinkler/homey.tesla/blob/main/lib/CarServer.js
So perhaps you need to copy&paste the changes.
Hi @eiannone
I added the domains as parameters and the Signer is also stored for every domain like:
this.signer[req.toDestination.domain] = await new Signer(...)
But using the DOMAIN_VEHICLE_SECURITY for action RKE_ACTION_LOCK I get http-400 error.
Using DOMAIN_INFOTAINMENT I stil get MESSAGEFAULT_ERROR_INSUFFICIENT_PRIVILEGES
Very irritating is:
- Sentry mode (vehicleControlSetSentryModeAction) is working with DOMAIN_INFOTAINMENT even if this command is listed in vsec definition.
- Charging (chargingStartStopActionStart) has same result as RKE_ACTION_LOCK, but that's not a security command.
I'm not sure if I use the correct commands for.
My adapted version
https://github.com/RonnyWinkler/homey.tesla/blob/1.1.8/lib/CarServer.js