Python toolkit for the ZWO minilang.
Install from PyPi with your favorite pip
invocation:
$ pip install zwolang
You can confirm proper installation via the zwom
CLI:
$ zwom --help
Usage: zwom [OPTIONS] COMMAND [ARGS]...
+- Options -------------------------------------------------------------------+
| --help Show this message and exit. |
+-----------------------------------------------------------------------------+
+- Commands ------------------------------------------------------------------+
| batch Discover and convert all `*.zwom` files in the given directory. |
| single Convert the specified `*.zwom` file to Zwift's `*.zwo`. |
+-----------------------------------------------------------------------------+
The primary purpose of this package is to provide a simple, human-readable format for constructing Zwift workouts that can be used to generate the actual workout XML. Let's call it a *.zwom
file, or ZWOM.
ZWOM files are parsed using a Parsimonious grammar, as specified below:
workout = ((comment / block) elws*)+ / elws
block = tag ws "{" ((comment / params) / elws)+ "}"
params = (message / value) ","?
value = tag ws (string / range / rangeval)
message = "@" ws duration ws string
range = rangeval ws "->" ws rangeval
rangeval = duration / numeric / zone
duration = number ":" number
percent = number "%"
zone = ("Z" number) / "SS"
numeric = percent / number
elws = ws / emptyline
comment = ~r"\;[^\r\n]*"
tag = ~"[A-Z_]+"
string = ~'"[^\"]+"'
number = ~"\d+"
ws = ~"\s*"
emptyline = ws+
Like Zwift's built-in workout builder, the ZWO minilang is a block-based system. Blocks are specified using a <tag> {<block contents>}
format supporting arbitrary whitespace.
Inline comments are also supported, denoted by a leading ;
.
Each ZWO file must begin with a META
block containing comma-separated parameters:
Keyword | Description | Accepted Inputs | Optional? |
---|---|---|---|
NAME |
Displayed workout name | str |
No |
AUTHOR |
Workout author | str |
No |
DESCRIPTION |
Workout description | str 1 |
No |
FTP |
Rider's FTP | int |
Maybe2 |
TAGS |
Workout tags | String of hashtags3 | Yes |
- Multiline strings are supported
- Zwift's workouts are generated using FTP percentages rather than absolute watts, so your FTP is required if you want to use absolute watts in your ZWOM
- Tags are capped at 31 total characters, including spaces and hashtags. Zwift also provides 4 built-in tags (
#RECOVERY
,#INTERVALS
,#FTP
, and#TT
) that may also be added and do not count against this total.
Following the META
block are your workout blocks:
Keyword | Description |
---|---|
FREE |
Free ride |
COOLDOWN |
Cooldown |
INTERVALS |
Intervals |
RAMP |
Ramp |
SEGMENT |
Steady segment |
WARMUP |
Warmup |
NOTE: While there is no specific Ramp block in the workout building UI, some experimental observations have been made:
- If a Ramp is at the very beginning of the workout, Zwift serializes it as a Warmup block
- If there are multiple blocks in a workout and a Ramp is at the end, there are two paths:
- If the left power is higher than the right power, Zwift serializes it as a Cooldown block
- If the right power is higher than the left power, Zwift serializes it as a Ramp block
- If there are multiple blocks in a workout and a Ramp is not at the beginning nor the end, Zwift serializes it as a Ramp block
When writing your *.zwom
file, these 3 blocks can be used interchangably, and ZWOM will try to match this behavior when outputting its *.zwo
file. Zwift may do its own normalization if edits are made in the workout UI.
Workout blocks can contain the following (optionally) comma-separated parameters:
Keyword | Description | Accepted Inputs | Optional? |
---|---|---|---|
DURATION |
Block duration | MM:SS , Range1 |
No |
CADENCE |
Target cadence | int , Range1,2 |
Yes |
REPEAT |
Number of intervals | int |
Only valid for intervals |
POWER |
Target power | int , int% , Zone3, Range1 |
Mostly no4 |
@ |
Display a message | @ MM:SS str 5 |
Yes |
- For Interval & Ramp segments, the range syntax can be used to set values for the
<left> -> <right>
segments (e.g.65% -> 120%
orZ2 -> Z6
) - Cadence ranges are only valid for Interval segments
- Zones may be specified as
Z1-7
orSS
- Power is ignored for Free segments
- Message timestamps are relative to their containing block
The START_REPEAT
and END_REPEAT
meta blocks are provided to specify an arbitrary chunk of blocks to repeat. The START_REPEAT
block must specify a REPEAT
parameter; END_REPEAT
accepts no parameters. Nested repeats are not currently supported.
For example:
SEGMENT {DURATION 2:00, POWER 65%}
RAMP {
DURATION 2:00,
POWER 120% -> 140%,
@ 0:00 "Here goes the ramp!",
@ 1:50 "10 seconds left!",
}
SEGMENT {DURATION 2:00, POWER 65%}
RAMP {
DURATION 2:00,
POWER 120% -> 140%,
@ 0:00 "Here goes the ramp!",
@ 1:50 "10 seconds left!",
}
Becomes:
START_REPEAT {REPEAT 2}
SEGMENT {DURATION 2:00, POWER 65%}
RAMP {
DURATION 2:00,
POWER 120% -> 140%,
@ 0:00 "Here goes the ramp!",
@ 1:50 "10 seconds left!",
}
END_REPEAT {}
; Here is a workout-level comment!
META {
NAME "Sample Workout",
AUTHOR "sco1",
DESCRIPTION "Here's a description!
Descriptions may be on more than one line too!",
TAGS "#RECOVERY #super #sweet #workout",
FTP 270,
}
FREE {DURATION 10:00}
INTERVALS {
; Here is a block-level comment!
REPEAT 3,
DURATION 1:00 -> 0:30,
POWER 55% -> 78%,
CADENCE 85 -> 110,
}
SEGMENT {DURATION 2:00, POWER 65%}
RAMP {
DURATION 2:00,
POWER 120% -> 140%,
@ 0:00 "Here goes the ramp!",
@ 1:50 "10 seconds left!",
}
FREE {DURATION 10:00}