WomoNET Protocol

Version 1.1

The WomoNET network uses MQTT as the primary protocol for communication between various devices. This setup enables efficient and flexible data exchange within the network.

Version History

VersionChanges
1.1Added device Error Feedback: per-fault error/<id>/state topics and one-by-one reset via error/<id>/set/state
1.0Initial protocol: config (c), state (s), auto-discovery, and client availability

Structure

The MQTT topics in the WomoNET network are systematically organized into the following main categories:

c (config)

conf (configuration)

This topic stores configuration settings, including user interface preferences, device parameters, and other important configurations. The key feature of this topic is that settings are persistent, meaning they remain intact even after a system reboot.

  • ui: Contains UI layout from the WomoNET.app. Layouts are written as JSON for every available layout
  • driver: Can be used to store configurations for drivers

dev (virtual devices, auto-discovery)

This topic is reserved for drivers to publish their auto-discovery configuration. The configuration data for device discovery is defined here, as described in detail below.

s (state)

Each MQTT client within the WomoNET network has a dedicated state topic where it publishes messages reflecting its current state or data. All data has to be human-readable UTF-8 data. Numeric values have to be sent as UTF-8 string. Decimal point is . (dot). We parse with Dart’s double.tryParse.

State updates and commands should use QoS 1.

Message Semantics

State messages should be retained by default, ensuring new subscribers immediately receive the current state. Implementations should:

  • Publish state only when the value changes (change detection)
  • Use retain=true for state topics so the MQTT broker caches the latest value
  • Avoid periodic republishing of unchanged values to reduce bandwidth

Structure

The state tree is structured as follows: s/<client_id>/<device_id>/<object_id>/[set/]<state/value/command>, whereas:

  • client_id is the MQTT client ID
  • device_id is the identifier of the virtual device
  • object_id is the object unit of a type’s value
  • set (optional) is a constant which is only provided if this is a command
  • state/value/command is the actual state, value or command

Topic Validity

State topics are only valid while the corresponding auto-discovery configuration exists. When an auto-discovery topic is removed (empty retained message), all associated state topics should be considered invalid.

Client Availability

Each client publishes its availability status to c/dev/<client_id>/alive:

  • online: Published (retained) when the client connects to the broker
  • offline: Published automatically by the broker via Last Will and Testament (LWT) when the client disconnects unexpectedly

Subscribers should monitor the alive topic to determine if a client’s auto-discovery topics and state values are current. When a client goes offline, its retained state values may be stale.

Error Feedback

Drivers report device faults on well-known, retained topics — one topic per active fault — so the app can surface them without any additional auto-discovery configuration. This mirrors the Client Availability convention: the topics are implicit for every virtual device and do not appear in the auto-discovery tree.

Topic: s/<client_id>/<device_id>/error/<id>/state (retained)

  • One topic per active fault. <id> identifies this occurrence of the fault: it is assigned by the driver, unique within the device, and must be a valid MQTT topic segment (no /, +, #). Subscribers discover the active faults of a device by subscribing to the wildcard s/<client_id>/<device_id>/error/+/state.
  • Scoped to the virtual device (device_id), matching how the app groups components into WidgetGroups.
  • Clearing a fault = empty retained message on its topic (the same mechanism used to invalidate auto-discovery topics). A device with no error/<id>/state topics — or all of them cleared — is healthy.
  • When a client goes offline, its retained error state — like all its state — may be stale. Subscribers should weigh the alive topic accordingly.

Payload: a JSON object describing the fault. The identifier is not repeated in the payload — it is the last path segment (<id>). Fields:

FieldRequiredDescription
severityyeserror or warning (see below)
codenoDevice-native fault code as a UTF-8 string (e.g. E12, 31, 0x8A), for display and translation lookups. Omit for faults the driver synthesizes itself (e.g. “device not responding”)
messagenoHuman-readable description in English. Optional; the app may prefer its own translation keyed by code

Severity follows the automotive warning-lamp model:

  • error — the device cannot be used in this state (analogous to a red stop lamp), e.g. gas supply interrupted, electronics locked.
  • warning — the device remains usable, but the user should take care (analogous to an amber warning lamp), e.g. imminent undervoltage.

Only currently active faults have a topic. A continuously-present fault keeps the same <id> (topic) across updates, so the app does not re-notify.

Example — an example heater reporting two concurrent faults publishes two retained topics:

s/womonet-core-123.../heater-a1b2/error/1/state:

{"severity": "error", "code": "E12", "message": "Overtemperature shutdown"}

s/womonet-core-123.../heater-a1b2/error/2/state:

{"severity": "warning", "code": "W07", "message": "Supply voltage low"}
Reset

Users reset faults — one by one — by publishing to the fault’s own command topic:

Topic: s/<client_id>/<device_id>/error/<id>/set/state (command, not retained)

Any payload triggers the reset (e.g. reset). To reset every fault of a device, publish to each active fault’s command topic.

On a reset request the driver always clears the fault (empty retained message on its error/<id>/state topic). It then attempts to resolve the underlying condition on the device:

  • If the condition is already gone, the fault stays cleared.
  • If the condition persists (the fault cannot be resolved), the driver raises it again under a new <id> (new topic). This re-notifies the user instead of leaving a silently-stuck entry, and it distinguishes “still broken after I acknowledged it” from the original report.

Faults whose condition resolves on the device on their own clear without any reset.

Open Questions
  • History: This mechanism reports current state only. A fault log/history, if needed, would be a separate concern.

Auto-discovery

The configuration data needs to be published as MQTT messages in a specific JSON format. Each component requires a unique topic name and a JSON configuration. The topic format is c/dev/<client_id>/<device_id>/<type>/<component>, where:

  • <client_id> is the MQTT client id (globally unique)
  • <device_id> is an identifier for the virtual device (driver level). This id has to be unique for the client to avoid collisions
  • <type> is the type of component. See below for available types
  • <component> is the name of the component. It has to be unique across all components for the given type of this virtual device

When publishing auto discovery configs, the retain flag has to be set for the MQTT message.

Auto discovery is organized different to Home Assistant: We use dev_id and then type instead the other way around. This enables us to use WidgetGroups in the app for each dev_id.

Naming Convention

device_id and component use different casing so the device level and the component level stay visually distinct within a topic:

  • device_id uses kebab-case with dashes:
    • Examples: inverter-d929, solar-mppt-1a2b, heater-a1b2
  • component uses snake_case with underscores:
    • Examples: battery_voltage, inverter_enabled, zone1_temp

Available Types

toggle

A switchable unit which can be turned on or off.

NameDescription
state_tTopic on which the current state is published, only true and false are allowed
cmd_tTopic on which commands for setting the toggle’s state are received. false, 0, off (all case insensitive) are interpreted as false, everything else is interpreted as true. If allow_timed is true, setting this value will cancel any running timer
allow_timedtrue or false, representing whether the value can be triggered in a timed manner by writing a CSV string representing the state and the duration it should be held in milliseconds (e.g., true,5000, setting the output on for 5000 milliseconds). If the timed state is already the requested state (e.g true,5000 is sent, but the state is already true) the command is ignored

actuator

An actuator which can be switched in two directions e.g. push/pull, forward/reverse and also has a neutral stop state. Important: The driver has to make sure to automatically stop the motion, when no more valid messages are received.

NameDescription
state_tTopic on which the numeric value representing the actuator’s state is published. Can be 1 (push/forward), 0 (neutral/stop), or -1 (pull/reverse)
cmd_tTopic on which commands for setting the actuator’s state are received. Can be 1 (push/forward), 0 (neutral/stop), or -1 (pull/reverse). If allow_timed is true, setting this value will cancel any running timer
allow_timedOptional (defaults to false). true or false, representing whether the value can be triggered in a timed manner by writing a CSV string representing the state and the duration it should be held in milliseconds (e.g., -1,5000, setting the direction to pull on for 5000 milliseconds). If the timed state is already the requested state (e.g -1,5000 is sent, but the state is already -1), the command is ignored

binary

A boolean sensor with states on and off.

NameDescription
state_tTopic on which the current state is published, only true and false are allowed

sensor

A sensor publishing a value.

NameDescription
state_tTopic on which the value is published. The value is UTF-8 (e.g 42.1234 or heating)
unit_of_measurementOptional. Specifies the unit of the sensor’s value for display purposes. Examples include °C for temperature, % for humidity, m/s for speed, kWh for energy, etc. If not provided, the value is treated as unitless. Use standardized units (SI) whenever possible to ensure compatibility

mode

Selectable options for a dropdown.

NameDescription
state_tTopic on which the current mode is published. The value is a UTF-8 string representing the current selection (e.g. heat, cool, auto, etc.)
cmd_tTopic on which commands to change the mode are received. The value should be a UTF-8 string corresponding to one of the selectable options (e.g. heat, cool, off, etc.). NOTE: for translations, we could define common english options and provide translations via a lookup table for them.
option_tTopic on which the list of available modes is published, provided as a JSON array of strings (e.g. ["off", "heat", "cool", "auto"]). User interfaces should respect the order of the options. This topic should be published with the retain flag set

slider

A controllable level value (slider in UI), which can be changed between a min and max value with a specified step size, e.g. for controlling heater/cooler temperatures, light dimmers, power consumption, charger output, …

NameDescription
state_tTopic on which the current value is published
cmd_tTopic on which commands for setting the target value are received
min_valueThe minimum value for the slider
max_valueThe maximum value for the slider
step_sizeThe size of a step, e.g. 0.5
unit_of_measurementOptional. Specifies the unit of the slider’s value for display purposes. Examples include °C for temperature, % for percentage, A for current, W for power, etc. If not provided, the value is treated as unitless. Use standardized units (SI) whenever possible to ensure compatibility

button

A stateless command trigger. Unlike the other types it has no state_t: triggering it simply publishes a fixed payload to its command topic, e.g. to reset a counter, acknowledge an error, or start a calibration.

NameDescription
cmd_tTopic on which the command is published when the button is triggered
payload_pressThe payload published to cmd_t when the button is pressed (e.g. PRESS)

Example Structure

c/
├── conf/
│   ├── ui/
│   │   ├── 1 = "{ layout JSON }"
│   │   ├── main = "{ layout JSON }"
│   │   └── heater_only = "{ layout JSON }"
│   └── driver/
│       ├── core-gpio = "{ driver configuration JSON }"
│       └── core-adc = "{ driver configuration JSON }"
└── dev/
    ├── womonet-core-123.../
    │   ├── adc/
    │   │   └── sensor/
    │   │       ├── vin0 = "{'unit_of_measurement': 'V', 'state_t': 's/womonet-core-123.../adc/0/state'}"
    │   │       ├── vin1 = "{'unit_of_measurement': 'V', 'state_t': 's/womonet-core-123.../adc/1/state'}"
    │   │       └── ...
    │   ├── extrelay/
    │   │   └── toggle/
    │   │       ├── relay0 = "{'state_t': 's/womonet-core-123.../extrelay/0/state', 'cmd_t': 's/womonet-core-123.../extrelay/0/set/state', 'allow_timed': true}"
    │   │       ├── relay1 = "{'state_t': 's/womonet-core-123.../extrelay/1/state', 'cmd_t': 's/womonet-core-123.../extrelay/1/set/state', 'allow_timed': true}"
    │   │       └── ...
    │   ├── heater-a1b2/
    │   │   ├── mode/
    │   │   │   └── heat = "{'state_t': 's/womonet-core-123.../heater-a1b2/heat/state', 'cmd_t': 's/womonet-core-123.../heater-a1b2/heat/set/state', 'option_t': 's/womonet-core-123.../heater-a1b2/heat/options'}"
    │   │   ├── sensor/
    │   │   │   ├── current_temp = "{'state_t': 's/womonet-core-123.../heater-a1b2/current_temp/state'}"
    │   │   │   └── boiler_temp = "{'state_t': 's/womonet-core-123.../heater-a1b2/boiler_temp/state'}"
    │   │   └── slider/
    │   │       └── target_temp = "{'state_t': 's/womonet-core-123.../heater-a1b2/target_temp/state', 'cmd_t': 's/womonet-core-123.../heater-a1b2/target_temp/set/state', 'min_value': 16, 'max_value': 36, 'step_size': 0.5}"
    │   └── ...
    └── womonet-power-524.../
        ├── toggle/
        │   └── pump = "{'state_t': 's/womonet-power-524.../pump/state', 'cmd_t': 's/womonet-power-524.../pump/set/state'}"
        ├── actuator/
        │   ├── step = "{'state_t': 's/womonet-power-524.../step/state', 'cmd_t': 's/womonet-power-524.../step/set/state', 'allow_timed': true}"
        │   ├── awning = "{'state_t': 's/womonet-power-524.../awning/state', 'cmd_t': 's/womonet-power-524.../awning/set/state'}"
        │   ├── valve0 = "{'state_t': 's/womonet-power-524.../valve0/state', 'cmd_t': 's/womonet-power-524.../valve0/set/state', 'allow_timed': true}"
        │   └── valve1 = "{'state_t': 's/womonet-power-524.../valve1/state', 'cmd_t': 's/womonet-power-524.../valve1/set/state', 'allow_timed': true}"
        └── ...

s/
├── womonet-core-123.../
│   ├── adc/
│   │   ├── 0/
│   │   │   └── state = "1.33"
│   │   ├── 1/
│   │   │   └── state = "0.452"
│   │   └── ...
│   ├── extrelay/
│   │   ├── 0/
│   │   │   ├── state = "ON"
│   │   │   └── set/
│   │   │       └── state (writable only, no retain)
│   │   ├── 1/
│   │   │   ├── state = "OFF"
│   │   │   └── set/
│   │   │       └── state (writable only, no retain)
│   │   └── ...
│   ├── heater-a1b2/
│   │   ├── heat/
│   │   │   ├── state = "heating"
│   │   │   ├── options = "[\"off\", \"heating\", \"venting\"]"
│   │   │   └── set/
│   │   │       └── state (writable only, no retain)
│   │   ├── current_temp/
│   │   │   └── state = "18.62"
│   │   ├── target_temp/
│   │   │   ├── state = "22.0"
│   │   │   └── set/
│   │   │       └── state (writable only, no retain)
│   │   └── error/
│   │       ├── 1/
│   │       │   ├── state = "{\"severity\": \"error\", \"code\": \"E12\", \"message\": \"Overtemperature shutdown\"}"
│   │       │   └── set/
│   │       │       └── state (writable only, no retain)
│   │       └── 2/
│   │           ├── state = "{\"severity\": \"warning\", \"code\": \"W07\", \"message\": \"Supply voltage low\"}"
│   │           └── set/
│   │               └── state (writable only, no retain)
│   └── ...
└── womonet-power-524.../
    ├── pump/
    │   ├── state = "ON"
    │   └── set/
    │       └── state (writable only, no retain)
    ├── step/
    │   ├── state = "0"
    │   └── set/
    │       └── state (writable only, no retain)
    └── ...

All product and company names, brand names, and trademarks are the property of their respective owners. They are used on this site solely to identify the respective products for compatibility and integration purposes. Their use does not imply any affiliation with, sponsorship by, or endorsement by the respective trademark holders. WomoNET is an independent project and is not affiliated with any of the listed manufacturers.