eiannone / tesla-cmd-api

MIT License
7 stars 1 forks source link

Questions about implementation #1

Closed RonnyWinkler closed 10 months ago

RonnyWinkler commented 10 months ago

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

Looks like: grafik

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.

grafik

Do you have an idea how the result can checked? I tried wird your example to send a charge limit with message body: grafik

But it seems it is not executed. I assume, the res.signedMessageStatus has these properties: grafik Is this a message status/error code?

Thanks for your help. Ronny

eiannone commented 10 months ago

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"

RonnyWinkler commented 10 months ago

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).

grafik

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?

RonnyWinkler commented 10 months ago

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 timeZeroend 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 expiresInparameter (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.

RonnyWinkler commented 10 months ago

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.

eiannone commented 10 months ago

Great thanks, I will update the code ASAP. I think you can increase the expiresIn argument. And yes, it represents seconds

RonnyWinkler commented 10 months ago

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");
        }
RonnyWinkler commented 10 months ago

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 )?

eiannone commented 10 months ago

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.

RonnyWinkler commented 10 months ago

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.

https://github.com/teslamotors/vehicle-command/blob/bd191bbf33d01615da2c98314b87bdccf142c484/examples/unlock/unlock.go#L87C52-L87C52

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.

https://github.com/teslamotors/vehicle-command/blob/bd191bbf33d01615da2c98314b87bdccf142c484/internal/dispatcher/dispatcher.go#L66

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...

RonnyWinkler commented 10 months ago

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

eiannone commented 10 months ago

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: https://github.com/timdorr/tesla-api/discussions/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.

RonnyWinkler commented 10 months ago

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.

RonnyWinkler commented 9 months ago

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:

I'm not sure if I use the correct commands for.

RonnyWinkler commented 9 months ago

My adapted version https://github.com/RonnyWinkler/homey.tesla/blob/1.1.8/lib/CarServer.js