Skip to content

The project file ​

A project is one JSON object in one file. /etc/muxen/deploy.json is a copy of such a file, so everything below describes the deployed configuration too.

This chapter is for someone reading or writing those files directly — building a template, auditing a boat, or implementing a client. Use the API for anything routine: it maintains the timestamps, the generated parameters and the cross-references that hand editing has to get right by itself.

Shape ​

json
{
  "version": 1,
  "metadata":      { … },
  "settings":      [ … ],
  "devices":       [ … ],
  "muxing":        [ … ],
  "sensors":       [ … ],
  "energy":        [ … ],
  "media":         [ … ],
  "buttonMapping": [ … ],
  "views":         [ … ],
  "commissioning": [ … ],
  "synapse":       { … }
}

version is the integer 1 on a project created by this daemon.

Sections are created lazily. A newly created project carries metadata, settings, devices, muxing, sensors, energy, media and buttonMapping; views, commissioning and synapse appear the first time something touches them. A missing section reads as empty, so absence and [] mean the same thing.

Everything except synapse is an array of objects identified by a field, not a map. Order in the array is not meaningful, except in commissioning[].rules, where the index is the addressable identity of a rule.

Timestamps ​

Two keys recur throughout:

KeyWrittenMeaning
ctmwhen the object is createdcreation time
mtmin metadata, on every savelast modification time

Both are UTC in %Y-%m-%dT%H:%M:%SZ — 2026-08-16T09:12:44Z. Only metadata.mtm is refreshed; the ctm on a device, a setting or a switching rule is the moment that object was added and is never touched again.

metadata ​

KeyTypeWritten by
idstringcreate, restore-by-duplication — the short name, and the filename
namestringthe name field of the create or duplicate body; "No name" by default
versionstring"1.0" on creation. A free-text configuration version, unrelated to the top-level version integer
uidstringa UUID v4: generated at creation, and again for each duplicate; kept by a rename
ctmstringcreation time; re-stamped on duplication, kept by a rename
mtmstringevery save through the API
fromTemplatestringduplication, holding the source project's metadata.id
duplicateFromstringduplication, holding the source project's metadata.uid; absent when the source has none
withCommissioningobjectduplication with commissioning, holding the answers that were applied
iconstringnot written by the daemon; reported in the project list when present

The uid names a project, not a file: it follows the project through a rename (PUT /project/<id>/rename/<new-id>) and never through a duplicate. A duplicate is a new project, so it gets a new uid, and duplicateFrom records the uid it was copied from — the way fromTemplate records the short name. The shipped templates carry no uid, so a project duplicated from one has a uid of its own and no duplicateFrom. Projects duplicated before this rule share their source's uid; nothing in the daemon or the interfaces reads it yet.

metadata.id is not rewritten by PUT /project/<id>/restore. After a restore under a different short name, the filename and metadata.id disagree until something rewrites it.

settings ​

Boat-wide values. Each entry is the posted body plus a name and a ctm:

json
{ "name": "PrimaryLanguage", "value": "US", "ctm": "2026-08-16T09:12:44Z" }

A PUT /project/<id>/settings/<name> removes any existing entry with that name and appends the new one, so a setting is unique by name and the body may carry any fields at all.

Three settings exist on a freshly created project:

NameValue
DegradedModeName1"Nominal mode"
PrimaryLanguage"US"
AlternativeLanguages[]

Two names have meaning to the daemon itself:

NameMeaning
passwordits hash field protects the project — see Projects
FeatureCommissioningremoved from the copy when a duplication applies commissioning

devices ​

One entry per MUXEN device fitted.

json
{
  "ctm": "2026-08-16T09:12:44Z",
  "function": 1,
  "functionName": "Bloc 8",
  "instance": 0,
  "deviceId": 64,
  "parameters": [ … ]
}
KeyTypeMeaning
functionintegerthe MUXEN function code, 0–63
functionNamestringthe human name of that function code
instanceintegerthe instance of that function, 0–63
deviceIdintegerfunction × 64 + instance
parametersarraythe device's configuration

The three identity fields are redundant on purpose: deviceId is what URLs use, function and instance are what lookups compare. A device is found by matching both function and instance, so an entry whose deviceId disagrees with them is unreachable through the API.

functionName is filled from the function code at creation and is not re-derived afterwards — it is a label, not an identity.

parameters ​

json
{ "name": "Name", "value": "Saloon lights", "ctm": "…" }
{ "name": "CabineType", "value": "B2", "extra": true, "ctm": "…" }
KeyTypeMeaning
namestringparameter name, unique within the device
valuestringalways stored as a string, whatever JSON type was posted
extrabooleanpresent only when true: the parameter has no firmware counterpart and exists for the interface's benefit
ctmstringwhen it was set

Writing a parameter that already exists removes the old entry and appends the new one, so the array order follows the order of writes.

Parameters created automatically ​

Adding a device through PUT /project/<id>/device/<deviceId> generates:

ParameterValue
Name"<functionName> #<instance + 1>", e.g. Bloc 8 #1

and, for an SFSP interrupter (function 23) only:

ParameterValue
VirtualDevice"1"
NameLed1"Bottom Left"
NameLed2"Top Left"
NameLed3"Bottom Right"
NameLed4"Top Right"
Model"simple"

muxing ​

The switching rules: what drives what. Each entry is the posted body plus name and ctm, and the body's config array is the part the daemon acts on.

json
{
  "name": "OPT-saloon-lights",
  "ctm": "…",
  "config": [
    { "deviceId": 64, "name": "EqnOut2", "value": "B1_1=O1_2" },
    { "deviceId": 64, "name": "ButtonMomentary2", "value": "1", "extra": true }
  ]
}

config entries are device parameters with the target device named inline:

KeyTypeRequired
deviceIdintegeryes
namestringyes
valuestringyes
extrabooleanno

Writing a muxing entry applies every config entry to its device before the rule is stored. If any of them cannot be applied — a deviceId not present in the project, a missing name or value — the whole rule is rejected with 400 and the muxing array is left untouched.

Deleting a muxing entry reverses all of that:

  1. every parameter named in config is removed from its device,
  2. every commissioning rule of type muxing whose target is this entry's name is removed,
  3. every commissioning group left with no rules is removed.

So muxing is the one section with side effects on two others. A hand edit that removes an entry without unwinding its config leaves orphan parameters on the devices.

sensors ​

Analogue sensor definitions: which device channel, which unit, and how to scale between them. Each entry is the posted body plus name and ctm.

json
{
  "name": "FreshWaterPortside",
  "input":  { "deviceType": 1, "deviceInstance": 1,
              "deviceChannel": "analogInput0", "min": 0.0, "max": 5.0 },
  "output": { "unit": "obix:units/liter", "min": 0, "max": 250 },
  "scaleType": "polynomial",
  "polynomial": [ 0, 50 ],
  "ctm": "…"
}

The daemon stores the body without validating it: input, output, scaleType and the scaling coefficients are interpreted by the services that consume the deployment, not here.

energy ​

The electrical zones, one entry per zone, each the posted body plus name and ctm. Stored, not interpreted. What the energy daemon actually reads from a deployment is documented with that daemon.

buttonMapping ​

Maps a physical button to a named function. Each entry is the posted body plus name — the function identifier, which is the URL segment — and ctm.

KeyTypeMeaning
deviceIdintegerthe MUXEN device id of the button panel
indexintegerthe button channel index on that device

The REST path is /button-mapping; the JSON key is buttonMapping.

media ​

Images and other blobs, carried inside the project.

json
{
  "name": "logo",
  "ctm": "…",
  "data": "iVBORw0KGgo…",
  "metadata": { "size": 20416, "etag": "\"3241589\"", "mime": "image/png" }
}
KeyMeaning
datathe bytes, base64-encoded
metadata.sizethe length of the original body in bytes
metadata.mimethe Content-Type of the upload, or application/octet-stream if it had none
metadata.etaga quoted hash of the encoded data, used as the HTTP validator

data is what makes projects large, and it is the field stripped from GET /project/<id> and GET /deploy. GET /project/<id>/media/<name> decodes and returns the bytes with the stored MIME type.

views ​

The screen pages. A view is mostly opaque to the daemon — it stores what the editor posts — but three of its fields are treated specially and five more have a dedicated endpoint.

KeyMeaning
namethe page name, unique, [a-zA-Z0-9-]+ because it is a URL segment
elementsthe page content: every drawable element
conditionGroupsvisibility conditions
imagethe page's own embedded image
tagsreplaced wholesale by PUT …/view/<name>/tags
enabledboolean, replaced by PUT …/view/<name>/enabled

elements, conditionGroups and image are the heavy fields: they are removed from GET /project/<id>, from GET /project/<id>/view and from GET /deploy. Fetch a whole page with GET /project/<id>/view/<name>.

PUT …/view/<name>/settings accepts exactly these keys and ignores any other:

label, description, background, arrowSpacing, defaultIconRadius, defaultArrowStrokeWidth, defaultTextFontSize.

PUT …/view/<name>/texts takes an array of patches { "id": …, "content": …, "translations": … } and applies each to the element of elements[] whose type is "text" and whose id matches. A patch that matches nothing is ignored; content and translations are each replaced only if the patch carries them.

PUT …/view/<name>/name renames a page. It refuses an empty name, a name outside [a-zA-Z0-9-], and a name already used by another view (409).

commissioning ​

The option groups a template offers. Present on templates, and removed from a project the moment commissioning is applied to it.

json
{
  "id": "watermaker",
  "name": "Watermaker",
  "ctm": "…",
  "rules": [
    { "type": "device", "target": 1280 },
    { "type": "muxing", "target": "OPT-watermaker" },
    { "type": "device-parameter",
      "target": [ { "deviceId": 64, "name": "EqnOut3", "value": "…" } ] }
  ]
}
KeyTypeMeaning
idstringrequired — the key the duplication body answers with a boolean
namestringrequired — display name. A group missing it aborts the apply
rulesarrayrequired, and must be an array

Rule objects:

KeyTypeMeaning
typestringmuxing, sensor, view, device or device-parameter. Any other value aborts the apply
targetdepends on typea name for muxing / sensor / view, an integer device id for device, an array of {deviceId, name, value, extra?} for device-parameter
reversebooleaninverts the condition — see Projects

Rules are addressed by index: DELETE /project/<id>/commissioning/<groupId>/rules/<index>. Removing the last rule of a group removes the group.

synapse ​

The one section that is a JSON object, not an array, keyed by automation instance id:

json
"synapse": {
  "3": { … },
  "7": { … }
}

A PUT /project/<id>/synapse/<instanceId> replaces the value at that key outright. The daemon stores the body without interpreting it.

Writing a project file by hand ​

It works — the daemon parses whatever is on disk — but four rules keep it from producing a file that reads back wrong:

  1. Keep deviceId, function and instance consistent. Lookups match on function and instance; URLs use deviceId.
  2. Keep parameter value a string. The API coerces; a hand-written number stays a number and consumers that expect a string will not read it.
  3. Do not remove a muxing entry without removing the parameters its config wrote, and the commissioning rules that point at it.
  4. Fix the ownership afterwards. A file written as root under /etc/muxen stops the service from saving over it. Run systemctl start muxen-config-perms.

A file that does not parse is not an error anywhere visible: it is skipped by the project list, answers 404 on every endpoint, and — if it is deploy.json — makes GET /deploy answer 404 while the nginx fallback happily serves the broken bytes.

Integration of multiplexed solutions
MUXEN and the MUXEN logo are trademarks of MUXEN SAS.