Skip to content

Commit

Permalink
[somfytahoma] Fix getting history events (#18323)
Browse files Browse the repository at this point in the history
Signed-off-by: Ondrej Pecta <[email protected]>
  • Loading branch information
octa22 authored Mar 1, 2025
1 parent 7268276 commit b01d822
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 56 deletions.
2 changes: 1 addition & 1 deletion bundles/org.openhab.binding.jablotron/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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;
Expand Down Expand Up @@ -72,7 +71,7 @@ public abstract class JablotronAlarmHandler extends BaseThingHandler {
protected @Nullable ScheduledFuture<?> future = null;

protected @Nullable ExpiringCache<JablotronDataUpdateResponse> dataCache;
protected ExpiringCache<List<JablotronHistoryDataEvent>> eventCache;
protected ExpiringCache<JablotronHistoryDataEvent> eventCache;

public JablotronAlarmHandler(Thing thing, String alarmName) {
super(thing);
Expand Down Expand Up @@ -186,20 +185,19 @@ protected synchronized boolean updateAlarmStatus() {
logger.debug("Error during alarm status update: {}", dataUpdate.getErrorMessage());
}

List<JablotronHistoryDataEvent> 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<JablotronHistoryDataEvent> sendGetEventHistory() {
protected @Nullable JablotronHistoryDataEvent sendGetEventHistory() {
return sendGetEventHistory(alarmName);
}

private @Nullable List<JablotronHistoryDataEvent> sendGetEventHistory(String alarm) {
private @Nullable JablotronHistoryDataEvent sendGetEventHistory(String alarm) {
JablotronBridgeHandler handler = getBridgeHandler();
if (handler != null) {
return handler.sendGetEventHistory(getThing(), alarm);
Expand All @@ -208,7 +206,7 @@ protected synchronized boolean updateAlarmStatus() {
}

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()));
Expand All @@ -220,12 +218,11 @@ protected void updateLastEvent(JablotronHistoryDataEvent event) {
}

protected void updateEventChannel(String channel) {
List<JablotronHistoryDataEvent> 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()));
Expand All @@ -243,11 +240,6 @@ protected void updateEventChannel(String channel) {
}
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
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;
Expand All @@ -54,6 +54,9 @@
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;

/**
Expand All @@ -71,6 +74,8 @@ public class JablotronBridgeHandler extends BaseBridgeHandler {

final HttpClient httpClient;

private String accessToken = "";

private @Nullable ScheduledFuture<?> future = null;

/**
Expand Down Expand Up @@ -169,14 +174,47 @@ public void dispose() {
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> T sendMessage(String url, String urlParameters, Class<T> 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);
Expand Down Expand Up @@ -212,13 +250,31 @@ protected synchronized void login() {
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);
}
}
Expand Down Expand Up @@ -287,27 +343,45 @@ protected void logout() {
return response;
}

protected @Nullable List<JablotronHistoryDataEvent> 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) {
logger.debug("Thing handler is null");
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) {
Expand Down Expand Up @@ -399,8 +473,16 @@ private String getCommonUrlParameters(String serviceId) {
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,8 @@ protected synchronized boolean updateAlarmStatus() {
}

// update events
List<JablotronHistoryDataEvent> events = sendGetEventHistory();
if (events != null && !events.isEmpty()) {
JablotronHistoryDataEvent event = events.get(0);
JablotronHistoryDataEvent event = sendGetEventHistory();
if (event != null) {
updateLastEvent(event);
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<JablotronHistoryDataEvent> events = new ArrayList<>();
public class JablotronAccessTokenData {
@SerializedName("access-token")
private String accessToken = "";

@SerializedName("access-token-expiration")
private String accessTokenExpiration = "";

public String getAccessToken() {
return accessToken;
}

public List<JablotronHistoryDataEvent> getEvents() {
return events;
public String getAccessTokenExpiration() {
return accessTokenExpiration;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,22 @@
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;
}
}
Loading

0 comments on commit b01d822

Please sign in to comment.