diff --git a/bundles/org.openhab.binding.jablotron/README.md b/bundles/org.openhab.binding.jablotron/README.md index af3895845c6..d53d40fdb0c 100644 --- a/bundles/org.openhab.binding.jablotron/README.md +++ b/bundles/org.openhab.binding.jablotron/README.md @@ -49,7 +49,7 @@ Binding itself doesn't require specific configuration. | JA-80/JA-100/JA-100F | lastEventTime | DateTime | the time of the last event | | JA-80/JA-100/JA-100F | lastCheckTime | DateTime | the time of the last checking | | JA-80/JA-100/JA-100F | alarm | N/A | the alarm trigger, might fire ALARM or TAMPER events | -| JA-100/JA-100F | lastEventSection | String | the section of the last event | +| JA-80/JA-100/JA-100F | lastEventSection | String | the section of the last event | | JA-100 | state_%nr% | String | the section %nr% status/control | | JA-100 | pgm_%nr% | Switch | the PG switch %nr% status/control | | JA-100 | thermometer_%nr% | Number:Temperature | the thermometer %nr% value | diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/JablotronBindingConstants.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/JablotronBindingConstants.java index 3cd0e46cf3f..001cb71a5cb 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/JablotronBindingConstants.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/JablotronBindingConstants.java @@ -55,13 +55,16 @@ public class JablotronBindingConstants { // Constants public static final String JABLOTRON_API_URL = "https://api.jablonet.net/api/2.2/"; - public static final String AGENT = "net.jablonet/8.3.5.3331 (iPhone 14 Pro Max; iOS 17.4; )"; - public static final int TIMEOUT_SEC = 10; + public static final String JABLOTRON_GQL_URL = "https://graph.jablotron.cloud/graphql"; + public static final String APP_VERSION = "8.6.1.3887"; + public static final String AGENT = "net.jablonet/" + APP_VERSION; + public static final int TIMEOUT_SEC = 15; + public static final int TIMEOUT_LIMIT = 3; public static final String SYSTEM = "openHAB"; public static final String VENDOR = "JABLOTRON:Jablotron"; - public static final String CLIENT_VERSION = "MYJ-PUB-IOS-8.3.5.3331"; - public static final String CLIENT_DEVICE = "Apple|iPhone 14 Pro Max|17.4"; + public static final String CLIENT_VERSION = "MYJ-PUB-IOS-" + APP_VERSION; public static final String APPLICATION_JSON = "application/json"; + public static final String MULTIPART_MIXED = "multipart/mixed;deferSpec=20220824"; public static final String WWW_FORM_URLENCODED = "application/x-www-form-urlencoded; charset=UTF-8"; public static final String AUTHENTICATION_CHALLENGE = "Authentication challenge without WWW-Authenticate header"; public static final String PROPERTY_SERVICE_ID = "serviceId"; diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronAlarmHandler.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronAlarmHandler.java index c089551ebe5..d09c29c5435 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronAlarmHandler.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronAlarmHandler.java @@ -17,7 +17,6 @@ import static org.openhab.binding.jablotron.JablotronBindingConstants.*; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -72,7 +71,7 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler { protected @Nullable ScheduledFuture future = null; protected @Nullable ExpiringCache dataCache; - protected ExpiringCache> eventCache; + protected ExpiringCache eventCache; public JablotronAlarmHandler(Thing thing, String alarmName) { super(thing); @@ -186,20 +185,19 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler { logger.debug("Error during alarm status update: {}", dataUpdate.getErrorMessage()); } - List events = sendGetEventHistory(); - if (events != null && !events.isEmpty()) { - JablotronHistoryDataEvent event = events.get(0); + JablotronHistoryDataEvent event = sendGetEventHistory(); + if (event != null) { updateLastEvent(event); } return true; } - protected @Nullable List sendGetEventHistory() { + protected @Nullable JablotronHistoryDataEvent sendGetEventHistory() { return sendGetEventHistory(alarmName); } - private @Nullable List sendGetEventHistory(String alarm) { + private @Nullable JablotronHistoryDataEvent sendGetEventHistory(String alarm) { JablotronBridgeHandler handler = getBridgeHandler(); if (handler != null) { return handler.sendGetEventHistory(getThing(), alarm); @@ -208,7 +206,7 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler { } protected void updateLastEvent(JablotronHistoryDataEvent event) { - updateState(CHANNEL_LAST_EVENT_TIME, new DateTimeType(getZonedDateTime(event.getDate()))); + updateState(CHANNEL_LAST_EVENT_TIME, new DateTimeType(Instant.parse(event.getDate()))); updateState(CHANNEL_LAST_EVENT, new StringType(event.getEventText())); updateState(CHANNEL_LAST_EVENT_CLASS, new StringType(event.getIconType())); updateState(CHANNEL_LAST_EVENT_INVOKER, new StringType(event.getInvokerName())); @@ -220,12 +218,11 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler { } protected void updateEventChannel(String channel) { - List events = eventCache.getValue(); - if (events != null && !events.isEmpty()) { - JablotronHistoryDataEvent event = events.get(0); + JablotronHistoryDataEvent event = eventCache.getValue(); + if (event != null) { switch (channel) { case CHANNEL_LAST_EVENT_TIME: - updateState(CHANNEL_LAST_EVENT_TIME, new DateTimeType(getZonedDateTime(event.getDate()))); + updateState(CHANNEL_LAST_EVENT_TIME, new DateTimeType(Instant.parse(event.getDate()))); break; case CHANNEL_LAST_EVENT: updateState(CHANNEL_LAST_EVENT, new StringType(event.getEventText())); @@ -243,11 +240,6 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler { } } - public ZonedDateTime getZonedDateTime(String date) { - return ZonedDateTime.parse(date.substring(0, 22) + ":" + date.substring(22, 24), - DateTimeFormatter.ISO_DATE_TIME); - } - protected @Nullable JablotronControlResponse sendUserCode(String section, String key, String status, String code) { JablotronBridgeHandler handler = getBridgeHandler(); if (handler != null) { diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronBridgeHandler.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronBridgeHandler.java index bf5e8fe90c8..9f4ea564c2b 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronBridgeHandler.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronBridgeHandler.java @@ -32,10 +32,10 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.openhab.binding.jablotron.internal.config.JablotronBridgeConfig; import org.openhab.binding.jablotron.internal.discovery.JablotronDiscoveryService; +import org.openhab.binding.jablotron.internal.model.JablotronAccessTokenResponse; import org.openhab.binding.jablotron.internal.model.JablotronControlResponse; import org.openhab.binding.jablotron.internal.model.JablotronDataUpdateResponse; import org.openhab.binding.jablotron.internal.model.JablotronDiscoveredService; -import org.openhab.binding.jablotron.internal.model.JablotronGetEventHistoryResponse; import org.openhab.binding.jablotron.internal.model.JablotronGetServiceResponse; import org.openhab.binding.jablotron.internal.model.JablotronHistoryDataEvent; import org.openhab.binding.jablotron.internal.model.JablotronLoginResponse; @@ -54,6 +54,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; /** @@ -71,6 +74,8 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { final HttpClient httpClient; + private String accessToken = ""; + private @Nullable ScheduledFuture future = null; /** @@ -169,14 +174,47 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { return sendMessage(url, urlParameters, classOfT, WWW_FORM_URLENCODED, true); } + private @Nullable String sendGQLMessage(String url, String urlParameters) { + String line = ""; + try { + logger.trace("Request: {} with data: {}", url, urlParameters); + ContentResponse resp = createGQLRequest(url) + .content(new StringContentProvider(urlParameters), APPLICATION_JSON).send(); + + line = resp.getContentAsString(); + logger.trace("Response: {}", line); + return line; + } catch (TimeoutException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Timeout during calling url: " + url); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Interrupt during calling url: " + url); + Thread.currentThread().interrupt(); + } catch (JsonSyntaxException e) { + logger.debug("Invalid JSON received: {}", line); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Syntax error during calling url: " + url); + } catch (ExecutionException e) { + if (e.getMessage().contains(AUTHENTICATION_CHALLENGE)) { + relogin(); + return null; + } + logger.debug("Error during calling url: {}", url, e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Error during calling url: " + url); + } + return null; + } + private @Nullable T sendMessage(String url, String urlParameters, Class classOfT, String encoding, boolean relogin) { String line = ""; try { + logger.trace("Request: {} with data: {}", url, urlParameters); ContentResponse resp = createRequest(url).content(new StringContentProvider(urlParameters), encoding) .send(); - logger.trace("Request: {} with data: {}", url, urlParameters); line = resp.getContentAsString(); logger.trace("Response: {}", line); return gson.fromJson(line, classOfT); @@ -212,13 +250,31 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { JablotronLoginResponse response = sendJsonMessage(url, urlParameters, JablotronLoginResponse.class, false); if (response == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Null login response"); return; } if (response.getHttpCode() != 200) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Http error: " + response.getHttpCode()); + "Login http error: " + response.getHttpCode()); + return; + } + + url = JABLOTRON_API_URL + "accessTokenGet.json"; + urlParameters = "{ \"force-renew\": true }"; + JablotronAccessTokenResponse token_response = sendJsonMessage(url, urlParameters, + JablotronAccessTokenResponse.class, false); + + if (token_response == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Null get access token response"); + return; + } + + if (token_response.getHttpCode() != 200) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Get access token http error: " + response.getHttpCode()); } else { + accessToken = token_response.getData().getAccessToken(); updateStatus(ThingStatus.ONLINE); } } @@ -287,8 +343,7 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { return response; } - protected @Nullable List sendGetEventHistory(Thing th, String alarm) { - String url = JABLOTRON_API_URL + alarm + "/eventHistoryGet.json"; + protected @Nullable JablotronHistoryDataEvent sendGetEventHistory(Thing th, String alarm) { JablotronAlarmHandler handler = (JablotronAlarmHandler) th.getHandler(); if (handler == null) { @@ -296,18 +351,37 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { return null; } - String urlParameters = "{\"limit\":1, \"service-id\":" + handler.thingConfig.getServiceId() + "}"; - JablotronGetEventHistoryResponse response = sendJsonMessage(url, urlParameters, - JablotronGetEventHistoryResponse.class); + String urlParameters = "{\"operationName\":\"GetEvents\",\"query\":\"query GetEvents($cloudEntityIds: [CloudEntityID!]!, $pagination: Pagination, $lang: String!, $filter: EventsFilter, $eventIds: [ID!]!) { forEndUser { __typename events { __typename events(sources: $cloudEntityIds, pagination: $pagination, filter: $filter) { __typename edges { __typename node { __typename ...EventsFragment } } pageInfo { __typename hasNextPage endCursor startCursor } } batchEvents(sources: $cloudEntityIds, eventIds: $eventIds) { __typename ...EventsFragment } } } }\\nfragment EventsAttachementFragment on EventsAttachment { __typename files { __typename name mimeType downloadUrl } images { __typename name mimeType downloadUrl available widthPx heightPx } videos { __typename name mimeType downloadUrl duration } id type occurredAt }\\nfragment EventsEventFragment on EventsEvent { __typename id name { __typename translation(lang: $lang) } type occurredAt sources { __typename cloudEntityId } invokers { __typename ...EventsInvokerFragment } subjects { __typename ...EventsSubjectFragment } icon }\\nfragment EventsFragment on EventsEvent { __typename ...EventsEventFragment attachments { __typename ...EventsAttachementFragment } childEvents { __typename id name { __typename translation(lang: $lang) } type occurredAt sources { __typename cloudEntityId } invokers { __typename ...EventsInvokerFragment } subjects { __typename ...EventsSubjectFragment } icon attachments { __typename ...EventsAttachementFragment } } }\\nfragment EventsInvokerFragment on EventsInvoker { __typename cloudEntityId defaultName { __typename translation(lang: $lang) } name }\\nfragment EventsSubjectFragment on EventsSubject { __typename name cloudEntityId defaultName { __typename translation(lang: $lang) } }\",\"variables\":{\"cloudEntityIds\":[\"SERVICE_" + + alarm + ":" + handler.thingConfig.getServiceId() + "\"],\"eventIds\":[],\"lang\":\"" + + bridgeConfig.getLang() + "\",\"pagination\":{\"first\":1}}}"; + String response = sendGQLMessage(JABLOTRON_GQL_URL, urlParameters); if (response == null) { + logger.debug("Null response while getting event history"); return null; } - if (200 != response.getHttpCode()) { - logger.debug("Got error while getting history with http code: {}", response.getHttpCode()); - } - return response.getData().getEvents(); + return parseEventHistoryResponse(response); + } + + private @Nullable JablotronHistoryDataEvent parseEventHistoryResponse(String response) { + JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject(); + JsonArray edges = jsonObject.getAsJsonObject("data").getAsJsonObject("forEndUser").getAsJsonObject("events") + .getAsJsonObject("events").getAsJsonArray("edges"); + + JsonObject node = edges.get(0).getAsJsonObject().getAsJsonObject("node").getAsJsonObject(); + + JsonObject invoker = node.getAsJsonArray("invokers").get(0).getAsJsonObject(); + JsonObject subject = node.getAsJsonArray("subjects").get(0).getAsJsonObject(); + + JablotronHistoryDataEvent event = new JablotronHistoryDataEvent(); + event.setIconType(node.get("icon").getAsString()); + event.setEventText(node.getAsJsonObject("name").get("translation").getAsString()); + event.setDate(node.get("occurredAt").getAsString()); + event.setSectionName(subject.getAsJsonObject("defaultName").get("translation").getAsString()); + event.setInvokerName(invoker.getAsJsonObject("defaultName").get("translation").getAsString()); + + return event; } protected @Nullable JablotronDataUpdateResponse sendGetStatusRequest(Thing th) { @@ -399,8 +473,16 @@ public class JablotronBridgeHandler extends BaseBridgeHandler { private Request createRequest(String url) { return httpClient.newRequest(url).method(HttpMethod.POST).header(HttpHeader.ACCEPT, APPLICATION_JSON) .header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfig.getLang()).header(HttpHeader.ACCEPT_ENCODING, "*") - .header("x-vendor-id", VENDOR).header("x-client-version", CLIENT_VERSION) - .header("x-client-device", CLIENT_DEVICE).agent(AGENT).timeout(TIMEOUT_SEC, TimeUnit.SECONDS); + .header("x-vendor-id", VENDOR).header("x-client-version", CLIENT_VERSION).agent(AGENT) + .timeout(TIMEOUT_SEC, TimeUnit.SECONDS); + } + + private Request createGQLRequest(String url) { + return httpClient.newRequest(url).method(HttpMethod.POST).accept(MULTIPART_MIXED, APPLICATION_JSON) + .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken) + .header(HttpHeader.ACCEPT_LANGUAGE, bridgeConfig.getLang()) + .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br").agent(AGENT) + .timeout(TIMEOUT_SEC, TimeUnit.SECONDS); } private void relogin() { diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronJa100FHandler.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronJa100FHandler.java index 47b1d20dba0..47f3dfdd9ef 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronJa100FHandler.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/handler/JablotronJa100FHandler.java @@ -198,9 +198,8 @@ public class JablotronJa100FHandler extends JablotronAlarmHandler { } // update events - List events = sendGetEventHistory(); - if (events != null && !events.isEmpty()) { - JablotronHistoryDataEvent event = events.get(0); + JablotronHistoryDataEvent event = sendGetEventHistory(); + if (event != null) { updateLastEvent(event); } return true; diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryData.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenData.java similarity index 53% rename from bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryData.java rename to bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenData.java index fcda258d040..3dbc063c78d 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryData.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenData.java @@ -12,22 +12,28 @@ */ package org.openhab.binding.jablotron.internal.model; -import java.util.ArrayList; -import java.util.List; - import org.eclipse.jdt.annotation.NonNullByDefault; +import com.google.gson.annotations.SerializedName; + /** - * The {@link JablotronHistoryData} class defines the data object for the - * getEventHistory response + * The {@link JablotronAccessTokenData} class defines the data object for access token * * @author Ondrej Pecta - Initial contribution */ @NonNullByDefault -public class JablotronHistoryData { - List events = new ArrayList<>(); +public class JablotronAccessTokenData { + @SerializedName("access-token") + private String accessToken = ""; - public List getEvents() { - return events; + @SerializedName("access-token-expiration") + private String accessTokenExpiration = ""; + + public String getAccessToken() { + return accessToken; + } + + public String getAccessTokenExpiration() { + return accessTokenExpiration; } } diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronGetEventHistoryResponse.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenResponse.java similarity index 71% rename from bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronGetEventHistoryResponse.java rename to bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenResponse.java index 68a8bf3b04f..673bedf3a91 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronGetEventHistoryResponse.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronAccessTokenResponse.java @@ -17,24 +17,22 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import com.google.gson.annotations.SerializedName; /** - * The {@link JablotronGetEventHistoryResponse} class defines the response for the - * getEventHistory operation + * The {@link JablotronAccessTokenResponse} class defines the get access token call + * response * * @author Ondrej Pecta - Initial contribution */ @NonNullByDefault -public class JablotronGetEventHistoryResponse { - +public class JablotronAccessTokenResponse { @SerializedName("http-code") - int httpCode = -1; - - JablotronHistoryData data = new JablotronHistoryData(); + private int httpCode = -1; + private JablotronAccessTokenData data = new JablotronAccessTokenData(); public int getHttpCode() { return httpCode; } - public JablotronHistoryData getData() { + public JablotronAccessTokenData getData() { return data; } } diff --git a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryDataEvent.java b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryDataEvent.java index 97c8a45ec88..1151137afe3 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryDataEvent.java +++ b/bundles/org.openhab.binding.jablotron/src/main/java/org/openhab/binding/jablotron/internal/model/JablotronHistoryDataEvent.java @@ -57,4 +57,24 @@ public class JablotronHistoryDataEvent { public String getSectionName() { return sectionName; } + + public void setIconType(String iconType) { + this.iconType = iconType; + } + + public void setEventText(String eventText) { + this.eventText = eventText; + } + + public void setInvokerName(String invokerName) { + this.invokerName = invokerName; + } + + public void setSectionName(String sectionName) { + this.sectionName = sectionName; + } + + public void setDate(String date) { + this.date = date; + } } diff --git a/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/thing/oasis.xml b/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/thing/oasis.xml index eab286b55d6..74b2b8f86db 100644 --- a/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/thing/oasis.xml +++ b/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/thing/oasis.xml @@ -22,9 +22,15 @@ + + + + 1 + + diff --git a/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/update/update.xml new file mode 100644 index 00000000000..910ae951e26 --- /dev/null +++ b/bundles/org.openhab.binding.jablotron/src/main/resources/OH-INF/update/update.xml @@ -0,0 +1,15 @@ + + + + + + + jablotron:lastEventSection + + + + + +