Main UI JavaScript rule reload causing /rest/items failure

@florian-h05 I’m no (real) developer, so I don’t know where to look or check, but according to your knowledge should this been fixed in OpenHAB 5.0.2?

I tried today to change my personal module and got a lot of errors in the log:

Script execution of rule with UID 'motion_lights_statemachine' failed: TypeError: invokeMember (setTimeout) on org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers failed due to: no applicable overload found (overloads: [Method[public long org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.setTimeout(java.lang.Runnable,long)], Method[public long org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.setTimeout(java.lang.Runnable,double)]], arguments: [com.oracle.truffle.js.runtime.builtins.JSFunctionObject$Unbound@2131ba6c (Unbound), JSUndefined (Nullish)]) in @jsscripting-globals.js at line number 184 at column number 12

Then I tried my the workaround I used in OpenHAB 4.3 by reinitializing all the rules (just disable and enable them again), but in OpenHAB 5 this doesn’t work anymore and I receive even more errors:

java.lang.IllegalStateException: The Context is already closed.
        at com.oracle.truffle.polyglot.PolyglotEngineException.closedException(PolyglotEngineException.java:137) ~[?:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.checkClosedOrDisposing(PolyglotContextImpl.java:1667) ~[?:?]
        at com.oracle.truffle.polyglot.PolyglotContextImpl.enterThreadChanged(PolyglotContextImpl.java:974) ~[?:?]
        at com.oracle.truffle.polyglot.PolyglotEngineImpl.enterCached(PolyglotEngineImpl.java:2155) ~[?:?]
        at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:109) ~[?:?]
        at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:118) ~[?:?]
        at com.oracle.truffle.polyglot.PolyglotMap.entrySet(PolyglotMap.java:131) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:220) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:154) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:73) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:222) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:154) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:73) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.write(ReflectiveTypeAdapterFactory.java:247) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:490) ~[?:?]
        at com.google.gson.internal.bind.ObjectTypeAdapter.write(ObjectTypeAdapter.java:184) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:73) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:222) ~[?:?]
        at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:154) ~[?:?]
        at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:73) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$2.write(ReflectiveTypeAdapterFactory.java:247) ~[?:?]
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:490) ~[?:?]
        at com.google.gson.Gson.toJson(Gson.java:944) ~[?:?]
        at com.google.gson.Gson.toJson(Gson.java:899) ~[?:?]
        at com.google.gson.Gson.toJson(Gson.java:848) ~[?:?]
        at com.google.gson.Gson.toJson(Gson.java:825) ~[?:?]
        at org.openhab.core.io.rest.Stream2JSONInputStream.lambda$0(Stream2JSONInputStream.java:54) ~[?:?]
        at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
        at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
        at java.util.stream.ReferencePipeline$15$1.accept(ReferencePipeline.java:541) ~[?:?]
        at java.util.stream.ReferencePipeline$15$1.accept(ReferencePipeline.java:541) ~[?:?]
        at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
        at java.util.HashMap$KeySpliterator.tryAdvance(HashMap.java:1736) ~[?:?]
        at java.util.stream.StreamSpliterators$WrappingSpliterator.lambda$initPartialTraversalState$0(StreamSpliterators.java:292) ~[?:?]
        at java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.fillBuffer(StreamSpliterators.java:206) ~[?:?]
        at java.util.stream.StreamSpliterators$AbstractWrappingSpliterator.doAdvance(StreamSpliterators.java:169) ~[?:?]
        at java.util.stream.StreamSpliterators$WrappingSpliterator.tryAdvance(StreamSpliterators.java:298) ~[?:?]
        at java.util.Spliterators$1Adapter.hasNext(Spliterators.java:681) ~[?:?]
        at org.openhab.core.io.rest.Stream2JSONInputStream$1.nextElement(Stream2JSONInputStream.java:82) ~[?:?]
        at org.openhab.core.io.rest.Stream2JSONInputStream$1.nextElement(Stream2JSONInputStream.java:1) ~[?:?]
        at java.io.SequenceInputStream.peekNextStream(SequenceInputStream.java:98) ~[?:?]
        at java.io.SequenceInputStream.nextStream(SequenceInputStream.java:93) ~[?:?]
        at java.io.SequenceInputStream.transferTo(SequenceInputStream.java:255) ~[?:?]
        at org.openhab.core.io.rest.Stream2JSONInputStream.transferTo(Stream2JSONInputStream.java:111) ~[?:?]
        at org.openhab.core.io.rest.core.internal.GsonMessageBodyWriter.writeTo(GsonMessageBodyWriter.java:84) ~[?:?]
        at org.openhab.core.io.rest.core.internal.MediaTypeExtension.writeTo(MediaTypeExtension.java:85) ~[?:?]
        at org.apache.cxf.jaxrs.utils.JAXRSUtils.writeMessageBody(JAXRSUtils.java:1651) ~[bundleFile:3.6.5]
        at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.serializeMessage(JAXRSOutInterceptor.java:249) ~[bundleFile:3.6.5]
        at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.processResponse(JAXRSOutInterceptor.java:122) ~[bundleFile:3.6.5]
        at org.apache.cxf.jaxrs.interceptor.JAXRSOutInterceptor.handleMessage(JAXRSOutInterceptor.java:84) ~[bundleFile:3.6.5]
        at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) ~[bundleFile:3.6.5]
        at org.apache.cxf.interceptor.OutgoingChainInterceptor.handleMessage(OutgoingChainInterceptor.java:90) ~[bundleFile:3.6.5]
        at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:307) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:265) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:234) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:208) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:160) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:225) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:304) ~[bundleFile:3.6.5]
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doGet(AbstractHTTPServlet.java:222) ~[bundleFile:3.6.5]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:497) ~[bundleFile:4.0.4]
        at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:279) ~[bundleFile:3.6.5]
        at org.ops4j.pax.web.service.spi.servlet.OsgiInitializedServlet.service(OsgiInitializedServlet.java:102) ~[bundleFile:?]
        at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1656) ~[bundleFile:9.4.57.v20241219]
        at org.ops4j.pax.web.service.spi.servlet.OsgiFilterChain.doFilter(OsgiFilterChain.java:113) ~[bundleFile:?]
        at org.ops4j.pax.web.service.jetty.internal.PaxWebServletHandler.doHandle(PaxWebServletHandler.java:334) ~[bundleFile:?]
        at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:600) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:235) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1624) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:505) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1594) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:234) ~[bundleFile:9.4.57.v20241219]
        at org.ops4j.pax.web.service.jetty.internal.PrioritizedHandlerCollection.handle(PrioritizedHandlerCollection.java:96) ~[bundleFile:?]
        at org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:772) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:732) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:479) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.runTask(EatWhatYouKill.java:338) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:315) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.tryProduce(EatWhatYouKill.java:173) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:409) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883) ~[bundleFile:9.4.57.v20241219]
        at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034) ~[bundleFile:9.4.57.v20241219]
        at java.lang.Thread.run(Thread.java:1583) [?:?]
        Suppressed: com.oracle.truffle.api.TruffleStackTrace$LazyStackTrace

The only thing that works for me is restarting the openHAB service, but this is kind of a pain in the *ss when debugging some JS code :wink:

@florian-h05 I’m no (real) developer, so I don’t know where to look or check, but according to your knowledge should this been fixed in OpenHAB 5.0.2?

I’ll first answer my own question :slight_smile: The merge was in the milestone build. I’ve updated today. For most of my personal modules this works quit ok, but I still have a weird issue with no clue where to look for an answer.

If I change and save a personal module, all the details of my openHAB semantic locations are gone. And the rules that depend on those fail to execute obviously. Than I have to restart the openhab.service and the locations are ok and the rules continue to run.

I’m totally lost now…

My testing is only performed against the current snapshot, but IIRC this fix was backported to 5.0.2x.

Where do the details come from? Are they added via script?

I’ll try to eleborate a bit. I have a personal module that implements a statemachine for lighting. It uses a second personal module that has some general logging functions. When I start openHab, the location items show as this (example):

The MetaData item “MotionLights” is set by the personal module.

Than I change my module on disk (just adding a space at the first position and save the file), the log shows me this message when the script that uses the personal module runs:

2025-11-09 22:40:37.513 \[ERROR\] \[rg.apache.cxf.jaxrs.utils.JAXRSUtils\] - Problem with writing the data, class org.openhab.core.io.rest.Stream2JSONInputStream, ContentType: application/json
2025-11-09 22:40:37.515 \[ERROR\] \[internal.JSONResponseExceptionMapper\] - Unexpected exception occurred while processing REST request.
java.lang.IllegalStateException: The Context is already closed.

and

2025-11-09 22:41:14.969 \[ERROR\] \[.handler.AbstractScriptModuleHandler\] - Script execution of rule with UID ‘motion_lights_statemachine’ failed: TypeError: invokeMember (setTimeout) on org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers failed due to: no applicable overload found (overloads: \[Method\[public long org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.setTimeout(java.lang.Runnable,double)\], Method\[public long org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers.setTimeout(java.lang.Runnable,long)\]\], arguments: \[com.oracle.truffle.js.runtime.builtins.JSFunctionObject$Unbound@7c6132c2 (Unbound), JSUndefined (Nullish)\]) in @jsscripting-globals.js at line number 184 at column number 12

I removed the entire stacktrace from this post. Lot’s of debugging led me to the conclusion that my code cannot retrieve the metadata from the location object anymore. But the weird thing is that all my location objects are ‘empty’. Even the location objects that aren’t touched by the rule/script. Like this:

Opening the location item page shows the same error in the log:

2025-11-09 22:49:27.465 \[ERROR\] \[rg.apache.cxf.jaxrs.utils.JAXRSUtils\] - Problem with writing the data, class org.openhab.core.io.rest.JSONResponse$$Lambda/0x00000007c15d0b18, ContentType: application/json
2025-11-09 22:49:27.469 \[ERROR\] \[internal.JSONResponseExceptionMapper\] - Unexpected exception occurred while processing REST request.
java.lang.IllegalStateException: The Context is already closed.

I have to restart the openhab service to get my location objects back. I’ve checked all the logfiles I’m aware of, but cannot find any clues how to debug this. I tried to reinstall the JSBinding, and cleaning the openhab cache after stopping the service. But to no avail.

Sharing your code would help with trying to understand what’s going on.
Based on your logs, the issue seems to be not only reloading, but some more serious data corruption caused in that whole scenario.

This indicates you are calling setTimeout without or with null value for the delay argument.
Just checked the web docs, this should be supported. I will fix that bug in our setTimeout implementation.

I think we should split the discussion to a separate thread.

Thanks for splitting the discussion, I think this is a better thing to do.

No problem uploading the code, although it would be finding a needle in a haystack probably due to it’s size. I’ll refactor a bit (for readability and cleaning up the mess) and make it available somehow.

I think the setTimeout value with a null value is a secondary problem. The location items contain some metadata where the timeouts are configured. After saving my module, the location items become unavailable and thus the timeout value is null and thus the error is thrown. I’ll first adapt the code to test for this null condition and let it error out gracefully.

I think the main issue is that when saving the code somehow the location items aren’t available anymore. (The context closed error).

Could it be that there is something weird happening in the combination metadata / cache (for timers) that isn’t reloaded correctly after saving?

1 Like

The most interesting part of the code is the one about writing to the metadata, so what is written and how. This way I can start reproducing and try to understand what’s happening.

I think you are right about the metadata, thanks for the hint. I think somewhere a reference to an object is stored and that the object is lost after saving/reloading the module. If I remove the namespace-metadata from all locations, the location is visible again and the module functions correctly. Probably there is a reference to a non-existing object somewhere?

Now to find how this is possible…I’ve tried to refactor my code in such a way that only copies of objects are stored, this helps a little bit (ie. not all locations are affected but just one) but still I get the error “Script execution of rule with UID ‘motion_lights_statemachine’ failed: java.lang.IllegalStateException: The Context is already closed.”

I’m probably overlooking something…

The code that is responsible for manipulating the location metadata is:


// ----------------- CONSTANTS/DEFAULTS
const OH_NAMESPACE_MOTIONLIGHTS = "MotionLights"; // The Namespace used on the location item to store the parameters and the machine state.
const DEFAULT_IDLE_TIMEOUT = "PT10M"; // Default idle timeout specified in ISO8601 duration format.
const DEFAULT_ANTIFLAP_TIMEOUT = "PT06S"; // Default antiflap timeout specified in ISO8601 duration format.

/// Default metadata format for MotionLights on location
const OBJ_DEFAULT_METADATA_MOTIONLIGHTS = {
  value: "",
  configuration: {
    idleTimeout: DEFAULT_IDLE_TIMEOUT,
    antiflapTimeout: DEFAULT_ANTIFLAP_TIMEOUT,
    mlmstate: { status: "undef" },
  },
};


/***!
 *
 * Note: mlmstate is an object that contains the persisted state of the motionlights state machine, the state is retrieved from the xstate statemachine.
 *
 * The metadata structure is as follows:
 * "mlmstate":{"status":"active","value":"Room Empty","historyValue":{},"context":{"idleTimeout":"PT10M","antiflapTimeout":"PT06S","oh_location_name":"zlocOffice"},"children":{}}
 *
 */

// ---------------------------------------------------------------------------------------------------
//   Private functions
// ---------------------------------------------------------------------------------------------------
const objExist = (obj) => {
  return (obj && obj !== 'null' && obj !== 'undefined');
}

/**
 * Creates the 'value' field of the location metadata from the current md.configuration.
 * The 'value' field is a human readable summary of the configuration and the current state of the machine.
 *
 * @private
 * @param   {}  configuration   The configuration object from the location metadata
 * @return  {string}        The metadata value. The value will include the configured parameters (idleTimeout/antiFlaptimeout) and the current state of the statemachine, separated by a | symbol
 */
function _buildMetadataValue(configuration: any): string {

  // Return empty string if no metadata configuration object is provided
  if (!objExist(configuration)) {
    return "";
  }

  let values = [];

  // Defaults
  const regularkeys = ["idleTimeout", "antiflapTimeout"];
  for (const key in configuration) {
    const val = configuration[key];
    if (regularkeys.includes(key) && typeof val == "string" && val.length > 0) {
      values.push(key + ": " + val);
    }
  }

  // Machinestate & OFF timer
  if (
    objExist(configuration["mlmstate"]) &&
    Object.keys(configuration["mlmstate"]).length !== 0
  ) {
    values.push("State: " + configuration["mlmstate"].value);
  }

  return values.join(" | ");
}

// ---------------------------------------------------------------------------------------------------
//   Public functions
// ---------------------------------------------------------------------------------------------------

/**
 * Checks if the location item has motionlights metadata set.
 *
 * @param   {Item}      item   openHAB item containing a location
 * @return  {boolean}   true if the item has metadata in the namespace OH_NAMESPACE_MOTIONLIGHTS
 */
function hasLocationMetadata(item): boolean {
  return objExist(item.getMetadata(OH_NAMESPACE_MOTIONLIGHTS));
}

/**
 * Stores the xstate machine snapshot in the metadata and updates the metadata value.
 *
 * @param {Item} locationItem openHAB item containing a location
 * @param {*} snapshot
 */
function updateLocationMetadata(locationItem, snapshot) {

  // Retrieve the existing metadata for the location item or create a default one
  let curr_md = getLocationMetadata(locationItem); // Retrieves the metadata, or a default metadata frame (containing value/configuration).

  curr_md.configuration["mlmstate"] = JSON.parse(JSON.stringify(snapshot));

  // Build the value part of the metadata from the updated configuration. This will include the new machine state.
  curr_md.value = _buildMetadataValue(curr_md.configuration);

  locationItem.replaceMetadata(
    OH_NAMESPACE_MOTIONLIGHTS,
    curr_md.value,
    curr_md.configuration,
  );
}

/**
 * Retrieves the location metadata for a specific location item.
 *
 * @param   {Item}      locationItem   openHAB item containing a location
 * @param   {string}    key             The key to retrieve from the metadata
 * @return  {any}       The location metadata for the location or default metadata if not found.
 */
function _getLocationMetadata(locationItem): any {

  // Retrieve the existing metadata for the location item or create a default one
  let md = locationItem.getMetadata(OH_NAMESPACE_MOTIONLIGHTS);
  if (!objExist(md)) {

    // We have not found any motionlights metadata. Proceed with the default md framework.
    let defaultmd = OBJ_DEFAULT_METADATA_MOTIONLIGHTS;
    return JSON.parse(JSON.stringify(defaultmd)); // Return a copy of the default machine state
  }

  // Return a copy of the metadata to avoid accidental modifications of the original metadata
  let retVal = {};
  retVal["value"] = md.value;
  retVal["configuration"] = JSON.parse(JSON.stringify(md.configuration));
  return retVal;
}

function _getLocationMetadataParam(locationItem, requestedParam): string {

  // Retrieve the existing metadata for the location item or receive a default one
  const md = _getLocationMetadata(locationItem);

  // Set the default value to be returned if the requestedParam is not found in the metadata
  let paramValue = "";

  // We assume the configuration object is always available in the metadata and do not test for that.
  if (requestedParam in md.configuration && md.configuration[requestedParam] !== undefined) {
    paramValue = md.configuration[requestedParam];
  } else {
    paramValue = OBJ_DEFAULT_METADATA_MOTIONLIGHTS.configuration[requestedParam];
  }

  return paramValue;
}

function _getLocationMetadataMachineState(locationItem): any {

  let md = _getLocationMetadata(locationItem);

  // Set the default value to be returned if the requestedParam is not found in the metadata
  let machineState = OBJ_DEFAULT_METADATA_MOTIONLIGHTS.configuration["mlmstate"];

  // We assume the configuration object is always available in the metadata and do not test for that.
  if (
    "mlmstate" in md.configuration &&
    md.configuration["mlmstate"] !== undefined && // key exists in object and key's value is not undefined
    objExist(md.configuration["mlmstate"])
  ) {
    return JSON.parse(JSON.stringify(md.configuration["mlmstate"])); // Return a copy of the stored machine state
  } else {
    return JSON.parse(JSON.stringify(machineState)); // Return a copy of the empty machine state
  }
}

function getLocationMetadata(locationItem, key = "", valueIfNotExists = {}): any {
  if (key.length == 0) {
    // No key provided, return a copy of the entire metadata object
    return _getLocationMetadata(locationItem);
  } else if (key == "mlmstate") {
    // Machine state asked specifically, return a copy of the machine state object
    return _getLocationMetadataMachineState(locationItem);
  } else {
    // Specific key provided, return the value for that key
    return _getLocationMetadataParam(locationItem, key);
  }
}

/**
 * Removes the motionlights metadata from all location items.
 * This function can be used for cleanup purposes.
 *
 * @returns {void}
 */
function removeMotionMetadataFromAllLocations() {
  items.getItems()
    .filter(i => i.semantics.isLocation)
    .forEach((i) => {
      console.log(i.name);
      i.removeMetadata("MotionLights");
    })
}

I think I’ve found the culprit :smiley:

I was storing the ‘mlmstate’ as an object into the metadata. If I stringify the object and store it as a string, everything works as expected. Now I can sleep again :slight_smile:

Great you’be found the issue.

Some technical background:

In openHAB Core, Metadata configuration is stored as Map<String, Object>.
When working with Metadata configuration in JS, you will receive a JavaScript Object, where the keys are typically strings and the value can be anything. I think the Object value type for the Java Map was chosen to allow a wide variety of primitives, not real objects. When modifying metadata from Script Actions, the change will be persisted to a JSONDB, which obviously needs serialization. For simpler JavaScript object, this is no real problem I guess, but when passing more complex objects, this will fail.

Putting even the simplest JavaScript object into the value of a configuration key however passes a reference from the JavaScript object, which belongs to the ScriptEngine that executes that script. If you change the helper library or manually reload the script, the reference will be still there in openHAB’s Java layer, but lead to nothing, as the reload destroys the old ScriptEngine and creates a new one.

TL;DR: Putting any objects into metadata configuration is a very bad idea.
I will add some validation to openhab-js to disallow putting objects into metadata configuration: [items] Metadata: Don't allow objects as configuration value by florian-h05 · Pull Request #495 · openhab/openhab-js · GitHub

Apart from that, metadata is not the best way for storing such dynamic data, as any metadata change will cause writes to the JSONDB on disk, possibly wearing out a Pi’s SD card.
Have you considered using the cache for storing the required state?

1 Like

The validation will help for the next customer. Thanks! And thanks for your help/patience and work too!

I’m running openHAB of a small PC with a SSD hard disk, the wearout won’t be a big issue. But still maybe the cache is a better idea for the state. I’ll put it on the refactor ToDo list.

1 Like