Hi everyone,
I am currently developing a custom TransitApp Binding for openHAB 5.x that fetches public transit departure schedules for specific stops via the Transit API v4.
A transit stop (stop thing) depends on a central API bridge (bridge thing) and needs to provide multiple departure channels (e.g., routeLongName#1, departureTime#1, isCancelled#1, up to #maxNumDepartures).
During development, I initially tried creating all channels purely at runtime in Java using ChannelBuilder and ThingBuilder. However, this caused issues with textual .things file definitions and UI visibility (channels remained empty/invisible or showed NULL until manual re-initialization).
I have now reworked the implementation into what I believe is the standard openHAB approach, but I would love to get feedback from experienced binding developers to see if this is considered the “clean way” to do it.
My Current Implementation
1. Metadata (OH-INF/thing/thing-types.xml)
Instead of purely dynamic code generation without XML references, I defined base <channel-type> elements and explicitly declared the indexed channel instances inside the <thing-type> up to a reasonable maximum (e.g., 3 departures):
XML
<thing-type id="stop">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>Transit Stop</label>
<description>Transit Stop Thing</description>
<channels>
<channel id="jsonResponse" typeId="jsonResponse"/>
<channel id="routeLongName#1" typeId="routeLongName"/>
<channel id="routeShortName#1" typeId="routeShortName"/>
<channel id="departureTime#1" typeId="departureTime"/>
<channel id="isCancelled#1" typeId="isCancelled"/>
<!-- Repeated up to #3 (or #10) -->
</channels>
</thing-type>
<channel-type id="routeLongName">
<item-type>String</item-type>
<label>Route Long Name</label>
<description>Long name of the route</description>
<state readOnly="true"/>
</channel-type>
<!-- Other base channel types... -->
2. Handler Lifecycle & Bridge Sync (TransitAppStopHandler.java)
To prevent the stop thing from failing on startup when the bridge is still initializing (UNKNOWN status), I implemented bridgeStatusChanged(ThingStatusInfo) to asynchronously trigger the data fetch only when the bridge comes ONLINE:
Java
@Override
public void initialize() {
// ... validate configuration parameters (globalStopId, stopName, etc.) ...
Bridge bridge = getBridge();
if (bridge == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is missing");
return;
}
if (bridge.getStatus() != ThingStatus.ONLINE) {
logger.warn("Bridge {} is not online yet. Stop thing {} will wait for bridge.", bridge.getUID(), thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is not online");
return;
}
fetchStopData();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
super.bridgeStatusChanged(bridgeStatusInfo);
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
fetchStopData();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is not online");
}
}
3. State Updating (updateDepartureChannels)
Once the JSON payload is retrieved, the states of the pre-defined channels are updated based on the maxNumDepartures config parameter:
Java
private void updateDepartureChannels(String jsonBody, int maxNumDepartures) {
try {
// Reset states first
for (int i = 1; i <= maxNumDepartures; i++) {
updateState("routeLongName#" + i, new StringType("-"));
updateState("departureTime#" + i, new StringType("-"));
updateState("isCancelled#" + i, OnOffType.OFF);
}
// Parse JSON and update active indexed channels...
int deptIndex = 1;
while (deptIndex <= maxNumDepartures && /* more departures in JSON */) {
// ... parsing logic ...
updateState("routeLongName#" + deptIndex, new StringType(parsedLongName));
updateState("departureTime#" + deptIndex, new StringType(parsedTime));
updateState("isCancelled#" + deptIndex, isCancelled ? OnOffType.ON : OnOffType.OFF);
deptIndex++;
}
} catch (Exception e) {
logger.error("Failed to parse departure JSON: {}", e.getMessage(), e);
}
}
My Questions to the Community:
-
Static vs. Runtime Dynamic Channels: Is pre-defining indexed channels (like
#1,#2,#3) inthing-types.xmlthe recommended openHAB architecture for this kind of indexed data, or should I go back to usingChannelBuilder/ThingBuilderdynamically in Java? If dynamic building in Java is preferred, how do you handle UI visibility and textual.thingsfile compatibility cleanly? -
Bridge Lifecycle: Is overriding
bridgeStatusChanged(ThingStatusInfo)the cleanest way to defer child initialization until the bridge is authenticated and ready, or is there a built-in framework mechanism I missed? -
General Feedback: Do you see any architectural flaws or violations of openHAB coding guidelines in this snippet?
Thanks in advance for any insights or suggestions!
Best regards