Relay Semantics
Relay-driven hardware (gates, garage doors, locks, lighting contactors) reports and accepts a raw contact state — open or closed. What that contact means depends on the equipment and the subsystem. GEM centralizes this translation so drivers never hardcode it.
The translation helpers
Two server helpers map between raw relay state and logical zone state:
relayToZoneState(relayState, zone)— turn a contact reading into a zone state (e.g. "this gate is open").zoneToRelayCommand(cmd, zone)— turn a zone command into the relay action that achieves it.
Drivers route relay handling through these helpers rather than reasoning about contacts themselves.
Default mappings by subsystem
The default assumes a normally-open relay (relay closed = the active/energized state). From there the meaning is subsystem-specific:
| Behavior | Subsystems | Mapping (relay closed →) |
|---|---|---|
| Inverted | gates, garages, doors/locks, lifts | open / unlocked |
| Direct | lights, fans, power | on |
| Direct | shades | open |
| Direct | security | armed |
Override precedence
A site can override the default per zone or per subsystem. Precedence, most specific first:
- Zone
relay_statesattribute (JSON) - Subsystem
relay_statesattribute - Built-in subsystem defaults (above)
A legacy normally_closed boolean is also honored. Note that normally_open is the default and a no-op — only normally_closed inverts the contact interpretation.
:::tip Drivers: never hardcode contact meaning
If you're writing a relay driver, always go through relayToZoneState / zoneToRelayCommand. Hardcoding "closed = on" breaks the moment a site wires a gate normally-closed or maps a contact differently. See Driver Development.
:::