Skip to main content

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:

Subsystem nameRelay closed →Relay open →
gates, garages, liftsopenclosed
doors, locksunlockedlocked
shadesopenclosed
lights, fans, fire, power, wateronoff
securityarmeddisarmed

The default is chosen by the subsystem's name, with a plural fallback: GEM looks up the name as-is, then with an s appended. So a subsystem named gate resolves to the gates row, light to lights, and so on — singular or plural both work.

A subsystem whose name is not one of the rows above (for example climate, cameras, or any custom name you create) has no built-in relay mapping. For relay-driven hardware placed under such a subsystem the raw contact state passes through unchanged (closed/open), so you must supply an explicit relay_states override (below) for the zone to read as on/off, open/closed, etc.

Override precedence​

A site can override the default per zone or per subsystem. Precedence, most specific first:

  1. Zone relay_states attribute (JSON)
  2. Subsystem relay_states attribute
  3. Built-in subsystem defaults (above)

The relay_states attribute is a JSON object keyed by the two physical contact states, with the semantic zone state you want each to produce as the value. Enter it in the Attribute editor on the zone (or subsystem) exactly like this:

{ "closed": "open", "open": "closed" }

For example, to make a gate wired normally-closed read correctly — contact closed means the gate is closed — set the zone's relay_states to { "closed": "closed", "open": "open" }.

The normally_closed shortcut​

A normally_closed boolean zone attribute is also honored as a quick inversion that stacks on top of whichever mapping won (zone override, subsystem override, or default): when true it flips the relay-closed and relay-open results. normally_open is the default and a no-op — only normally_closed inverts the contact interpretation. Set normally_closed = true on a zone instead of hand-writing a relay_states JSON when all you need is to invert a recognized subsystem's default.

Driving relays from commands​

The inverse helper zoneToRelayCommand turns a zone command into the physical relay action — it returns open or close. It understands the verbs open, close, on, off, lock, unlock, arm, and disarm, normalizing each to the matching zone state before looking it up in the same mapping. A command that doesn't correspond to either mapped state is passed through unchanged.

Pulse relays: why a wrong mapping looks like a broken button​

A pulse relay has no separate open and close input — a single momentary closure tells the opener to do the other thing. So Open and Close cannot be idempotent, and a control surface that offers both has to consult the door's sensed state to avoid pulsing an already-open door closed.

That makes the relay mapping load-bearing for control, not just for display. A garage door whose mapping is inverted relative to its wiring reports the opposite of where it is, and the symptom is not a wrong label — it is that one of the two buttons appears to do nothing, while the other works normally. A door reporting closed when it is physically open takes no action on Close.

The Garage control page suppresses only a pulse it can prove is unnecessary: the door must be reporting one of its two known resting states, and that state must already match the request. A door reporting nothing, reporting a state the page doesn't recognize, or carrying no state attribute at all gets the pulse — an unknown position is not a reason to refuse a deliberate press. When a pulse is suppressed the page says so and names the override.

Press and hold Open or Close to pulse regardless of sensed state. That is the escape hatch for a door whose sensor is missing, miswired, or mapped backwards — but treat it as a diagnosis, not a fix: correct the mapping with relay_states or normally_closed so the state reads true.

Doors driven by a discrete opener rather than a pulse relay — anything carrying open_action / close_action zone attributes — are never gated this way. Those verbs are idempotent on the wire, so the sensed state doesn't enter into it.

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.