zoffline / zwift-offline

Use Zwift offline
GNU Affero General Public License v3.0
641 stars 104 forks source link

Easy way to see if somebody is actively riding? #344

Closed peteh closed 4 weeks ago

peteh commented 1 month ago

Hi

I'm trying to use this server to see if Zwift is running and connected.

Use case: When I start the app, I want to start the fan too.

Idea: use one of the zoffline server's api endpoints to see if somebody is connected.

Does such an api endpoint exist that could be used?

fatsbrown commented 1 month ago

Edit: actually there are routes in the Zwift API, https://us-or-rly101.zwift.com/relay/dropin returns DropInWorldList and https://us-or-rly101.zwift.com/relay/worlds/1/players/<player_id> returns PlayerState.

Hi. There isn't an API but since it's open source you can implement something directly in zoffline. The length of the online dictionary tells you how many players are currently connected to the UDP server and online[player_id] contains the last PlayerState received from that player. I suppose you can check speed to know if the player is moving or heartrate if you want to set the fan speed accordingly.

If you want to control the fan from another app, a possibility would be adding something like this in CDNHandler do_GET() to expose the values through HTTP

        if self.path == '/players':
            self.send_response(200)
            self.send_header('Content-type', 'application/json')
            self.end_headers()
            output = json.dumps(list(online.keys()))
            self.wfile.write(output.encode())
            return
        if self.path.startswith('/player/'):
            try:
                player_id = int(self.path.split('/')[-1])
                if player_id in online:
                    self.send_response(200)
                    self.send_header('Content-type', 'application/json')
                    self.end_headers()
                    from google.protobuf.json_format import MessageToDict
                    output = json.dumps(MessageToDict(online[player_id]))
                    self.wfile.write(output.encode())
                    return
            except:
                pass

GET http://127.0.0.1/players [1]

GET http://127.0.0.1/player/1 {"id": "1", "worldTime": "307330679646", "distance": 0, "roadTime": 991632, "laps": 0, "speed": 0, "roadPosition": 10484187, "cadenceUHz": 0, "psF10": 0, "heartrate": 0, "power": 0, "heading": "1384499", "lean": "1000000", "climbing": 0, "time": 0, "f19": 983060, "aux3": 33554447, "progress": 0, "justWatching": true, "calories": 0, "x": 4377.258, "yAltitude": 18546.525, "z": -9741.832, "watchingRiderId": "1", "groupId": "0", "sport": "CYCLING", "distLat": 129.20082, "world": 15, "psF36": 0, "psF37": 0, "canSteer": false, "route": -930393161}

peteh commented 1 month ago

Ah that would be perfect. I could even adjust light settings based on current power

peteh commented 1 month ago

Small update, for me the online variable is always empty. I tried both in docker and standalone on a linux machine but it seems to never be update. Even when I ride.

fatsbrown commented 1 month ago

Make sure server-ip.txt is correct.

You should see this in Zwift log.txt (Documents/Zwift/Logs) with the server IP

[INFO] TCP host 192.168.0.109:3025 (secure)
[INFO] Connecting to TCP server...
[INFO] Saying hello to TCP server (largest wa is 0)
[INFO] UDP host 192.168.0.109:3024 (secure)
[INFO] Connecting to UDP server with relay id 1...

The online dict is populated when the client connects to the UDP server.

peteh commented 1 month ago

Ah thank you very much, that works.

Does it make any difference if I add the api to the cdn or to the swift api server? The latter can specify routes easier within the flask app.

I would add something like /api_ext/players/[player_id]

Another question on the side: When switching from single to multiplayer, is there anything to be done with the profile? It looks like I cannot log into the single player account when switching to multiplayer via multiplayer.txt

fatsbrown commented 1 month ago

Does it make any difference if I add the api to the cdn or to the swift api server? The latter can specify routes easier within the flask app.

Not much, but flask already has it (I edited my first reply), /relay/dropin contains a list of online riders and /relay/worlds/1/players/<player_id> contains the last player state.

When switching from single to multiplayer, is there anything to be done with the profile? It looks like I cannot log into the single player account when switching to multiplayer via multiplayer.txt

When multiplayer is enabled you need to sign up (/signup). The first account will be profile 1 which is the default in single player mode. Also notice that if multiplayer is enabled you need to login (POST username and password to /login) to have access to /relay/dropin.

peteh commented 1 month ago

API Looks like the existing points are not json but Protobuf. So either I add a ?encode=json parameter or add new endpoints. But this seems doable.

Users I have a single player profile which I cannot log into when switching to multiplayer. Can I create a new profile and then upload the profile.bin to make it work?

fatsbrown commented 1 month ago

Looks like the existing points are not json but Protobuf. So either I add a ?encode=json parameter or add new endpoints. But this seems doable.

Dropin already has a json response if "Accept" header is "application/json" (https://github.com/zoffline/zwift-offline/blob/master/zwift_offline.py#L2993). The other route only returns protobuf but you can decode in your script. If there isn't protobuf library for the language you are using, you can run command line protoc, for instance:

curl -k https://us-or-rly101.zwift.com/relay/worlds/1/players/1 -o 1.bin
protoc --decode PlayerState udp-node-msgs.proto < 1.bin

Can I create a new profile and then upload the profile.bin to make it work?

Yes, or you can rename your current profile folder in storage to 1.

fatsbrown commented 1 month ago

Dropin example for multiplayer:

curl -k -c cookie.txt -F username=user@email.com -F password=xxx https://us-or-rly101.zwift.com/login/
curl -k -b cookie.txt https://us-or-rly101.zwift.com/relay/dropin -o dropin.bin
protoc --decode DropInWorldList world.proto < dropin.bin

or

curl -k -b cookie.txt https://us-or-rly101.zwift.com/relay/dropin | protoc --decode DropInWorldList world.proto
curl -k -b cookie.txt https://us-or-rly101.zwift.com/relay/worlds/1/players/1 | protoc --decode PlayerState udp-node-msgs.proto