From 0020450ec87c64b9f0066dccd32335e9a3f76efc Mon Sep 17 00:00:00 2001 From: Arne Seime Date: Sat, 4 Feb 2023 14:15:09 +0100 Subject: [PATCH] WIP pubnub --- bundles/org.openhab.binding.august/README.md | 6 +- bundles/org.openhab.binding.august/TODO.md | 34 +++ bundles/org.openhab.binding.august/pom.xml | 130 ++++++++++ .../src/main/feature/feature.xml | 4 + .../august/internal/AugustException.java | 4 +- .../AccessTokenUpdatedListener.java | 2 +- .../august/internal/comm/PubNubListener.java | 32 +++ .../internal/comm/PubNubMessageException.java | 27 ++ .../comm/PubNubMessageSubscriber.java | 243 ++++++++++++++++++ .../RestApiClient.java} | 42 ++- .../RestCommunicationException.java} | 13 +- .../internal/config/AccountConfiguration.java | 2 +- .../internal/config/LockConfiguration.java | 5 +- .../discovery/AugustDiscoveryService.java | 6 +- .../august/internal/dto/AbstractRequest.java | 6 +- .../august/internal/dto/GetLockRequest.java | 2 +- .../august/internal/dto/GetLockResponse.java | 2 + .../august/internal/dto/GetLocksRequest.java | 2 +- .../internal/dto/GetSessionRequest.java | 2 +- .../internal/dto/GetSessionResponse.java | 3 + .../dto/GetValidationCodeRequest.java | 2 +- .../dto/RemoteOperateLockRequest.java | 2 +- .../internal/dto/ValidateCodeRequest.java | 2 +- .../internal/dto/ValidateCodeResponse.java | 5 +- .../handler/AugustAccountHandler.java | 113 ++++++-- .../internal/handler/AugustLockHandler.java | 156 ++++++++--- .../handler/AugustThingHandlerFactory.java | 13 +- .../main/resources/OH-INF/binding/binding.xml | 2 +- .../main/resources/OH-INF/config/config.xml | 8 +- .../internal/PubNubMessageSubscriberTest.java | 71 +++++ .../dto/SerializationDeserializationTest.java | 62 +++-- .../august/internal/dto/WireHelper.java | 11 +- .../handler/AugustAccountHandlerTest.java | 58 +++-- .../handler/AugustLockHandlerTest.java | 151 +++++++---- .../src/test/resources/logback-test.xml | 18 ++ .../mock_responses/event_unlock.json | 21 ++ .../get_lock_response.json | 0 .../get_lock_status_response.json | 0 .../get_locks_response.json | 0 .../get_session_request.json | 0 .../get_session_response.json | 0 .../get_validation_code_request.json | 0 .../get_validation_code_response.json | 0 .../mock_responses/lock_status_async.json | 5 + .../remoteoperate_lock_response.json | 0 .../validate_code_request.json | 0 .../validate_code_response.json | 0 47 files changed, 1024 insertions(+), 243 deletions(-) create mode 100644 bundles/org.openhab.binding.august/TODO.md rename bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/{handler => comm}/AccessTokenUpdatedListener.java (92%) create mode 100644 bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubListener.java create mode 100644 bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageException.java create mode 100644 bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageSubscriber.java rename bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/{ApiBridge.java => comm/RestApiClient.java} (70%) rename bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/{CommunicationException.java => comm/RestCommunicationException.java} (62%) create mode 100644 bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/PubNubMessageSubscriberTest.java create mode 100644 bundles/org.openhab.binding.august/src/test/resources/logback-test.xml create mode 100644 bundles/org.openhab.binding.august/src/test/resources/mock_responses/event_unlock.json rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_lock_response.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_lock_status_response.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_locks_response.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_session_request.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_session_response.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_validation_code_request.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/get_validation_code_response.json (100%) create mode 100644 bundles/org.openhab.binding.august/src/test/resources/mock_responses/lock_status_async.json rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/remoteoperate_lock_response.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/validate_code_request.json (100%) rename bundles/org.openhab.binding.august/src/test/resources/{ => mock_responses}/validate_code_response.json (100%) diff --git a/bundles/org.openhab.binding.august/README.md b/bundles/org.openhab.binding.august/README.md index ce406e10bbf84..69e8eb6cebbd8 100644 --- a/bundles/org.openhab.binding.august/README.md +++ b/bundles/org.openhab.binding.august/README.md @@ -33,17 +33,15 @@ To find the `lockId`, add the bridge and let it discover your locks * `phone` = Mobile number used in the mobile app. Use full number with country code, ie `+4712345678` * `password` = Same as you use in the mobile app * `refreshInterval` = number of seconds between refresh calls to the server. This applies to the bridge itself, not the - locks. They can be configured individually. + locks. Defaults to once every hour * `validationCode` = one time code requested from the service after authenticating with email + phone + password. ### Lock * `lockId` = id of lock, typically a long string of numbers and letters -* `refreshInterval` = number of seconds between refresh calls to the server ## TODO -* Support for push messages via PubSub. * Support 2-factor code via SMS ## Tested devices @@ -73,7 +71,7 @@ status. This may take 10-20 seconds to complete, but you get the latest and most august.things: ``` -Bridge august:account:accountName "Yale Access account" [ email="XXX@XXX.COM", phone="+4712345678", password="XXXXXXX", refreshInterval="120", validationCode="REPLACE" ] { +Bridge august:account:accountName "Yale Access account" [ email="XXX@XXX.COM", phone="+4712345678", password="XXXXXXX", refreshInterval="3600", validationCode="REPLACE" ] { Thing lock frontdoor "Front door" [ lockId="344KJLK32KJ234LKJ234JLKJK34" ] } ``` diff --git a/bundles/org.openhab.binding.august/TODO.md b/bundles/org.openhab.binding.august/TODO.md new file mode 100644 index 0000000000000..3af61007bde47 --- /dev/null +++ b/bundles/org.openhab.binding.august/TODO.md @@ -0,0 +1,34 @@ +2023-01-10 05:42:50.022 [WARN ] [t.internal.handler.AugustLockHandler] - 92968A1940BEEC48B9F99C6FA898A6B9 Error +contacting lock +org.openhab.binding.august.internal.RestCommunicationException: Error sending request to server. Server responded with +531 +and payload {"code":31,"message":"Mechanical Position"} +at org.openhab.binding.august.internal.ApiBridge.sendRequestInternal(ApiBridge.java:132) ~[bundleFile:?] +at org.openhab.binding.august.internal.ApiBridge.sendRequest(ApiBridge.java:97) ~[bundleFile:?] +at org.openhab.binding.august.internal.handler.AugustLockHandler.handleLockStateCommand(AugustLockHandler.java: + +196) [bundleFile:?] + at org.openhab.binding.august.internal.handler.AugustLockHandler.handleCommandInternal(AugustLockHandler.java: +167) [bundleFile:?] + at org.openhab.binding.august.internal.handler.AugustLockHandler.handleCommand(AugustLockHandler.java: +158) [bundleFile:?] + at jdk.internal.reflect.GeneratedMethodAccessor154.invoke(Unknown Source) ~[?:?] + at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?] + at org.openhab.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java: +154) [bundleFile:?] + at org.openhab.core.internal.common.InvocationHandlerSync.invoke(InvocationHandlerSync.java:59) [bundleFile:?] + at com.sun.proxy.$Proxy8590.handleCommand(Unknown Source) [?:?] + at org.openhab.core.thing.internal.profiles.ProfileCallbackImpl.handleCommand(ProfileCallbackImpl.java: +80) [bundleFile:?] + at org.openhab.core.thing.internal.profiles.SystemDefaultProfile.onCommandFromItem(SystemDefaultProfile.java: +48) [bundleFile:?] + at jdk.internal.reflect.GeneratedMethodAccessor153.invoke(Unknown Source) ~[?:?] + at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?] + at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?] + at org.openhab.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java: +154) [bundleFile:?] + at org.openhab.core.internal.common.Invocation.call(Invocation.java:52) [bundleFile:?] + at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?] + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?] + diff --git a/bundles/org.openhab.binding.august/pom.xml b/bundles/org.openhab.binding.august/pom.xml index d9aa5dd6219c8..7a7c8cc8a7c99 100644 --- a/bundles/org.openhab.binding.august/pom.xml +++ b/bundles/org.openhab.binding.august/pom.xml @@ -14,13 +14,143 @@ openHAB Add-ons :: Bundles :: August Binding + + + kotlin.internal.jdk7;resolution:=optional,kotlin.internal.jdk8;resolution:=optional,android.*;resolution:=optional,org.bouncycastle.*;resolution:=optional,org.openjsse.javax.net.ssl.*;resolution:=optional,org.openjsse.net.ssl.*;resolution:=optional,com.android.org.*;resolution:=optional,dalvik.*;resolution:=optional,javax.annotation.meta.*;resolution:=optional,org.apache.harmony.*;resolution:=optional,org.conscrypt.*;resolution:=optional,sun.security.*;resolution:=optional,org.apache.http.*;resolution:=optional + + + + + com.google.code.gson + gson + 2.9.0 + + + + com.pubnub + pubnub-gson + 6.3.1 + + + + org.jetbrains.kotlin + kotlin-stdlib-common + 1.5.31 + + + org.jetbrains.kotlin + kotlin-stdlib + 1.6.20 + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-cbor + 2.14.2 + + + com.fasterxml.jackson.core + jackson-annotations + 2.14.2 + + + + + com.fasterxml.jackson.core + jackson-core + 2.14.2 + + + + com.fasterxml.jackson.core + jackson-databind + 2.14.2 + + + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.6.4 + + + + org.jetbrains.kotlin + kotlin-reflect + 1.6.10 + + + + com.squareup.retrofit2 + converter-gson + 2.6.2 + + + + com.squareup.retrofit2 + retrofit + 2.6.2 + + + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.okhttp + 4.10.0_3 + compile + + + + org.apache.servicemix.bundles + org.apache.servicemix.bundles.okio + 3.2.0_2 + compile + + + + + org.slf4j + slf4j-api + 1.7.28 + provided + com.github.tomakehurst wiremock-standalone 2.23.0 test + + + org.awaitility + awaitility + 4.2.0 + test + + + + + ch.qos.logback + logback-classic + 1.2.3 + test + + + + diff --git a/bundles/org.openhab.binding.august/src/main/feature/feature.xml b/bundles/org.openhab.binding.august/src/main/feature/feature.xml index 8e78fd33910c7..694b381ba1603 100644 --- a/bundles/org.openhab.binding.august/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.august/src/main/feature/feature.xml @@ -4,6 +4,10 @@ openhab-runtime-base + mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.okhttp/4.10.0_3 + mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.okio/3.2.0_2 + mvn:org.json/json/20200518 mvn:org.openhab.addons.bundles/org.openhab.binding.august/${project.version} + diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/AugustException.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/AugustException.java index da8577e028956..499746696e83b 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/AugustException.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/AugustException.java @@ -24,11 +24,11 @@ public abstract class AugustException extends Exception { private static final long serialVersionUID = 1L; - public AugustException(String message) { + protected AugustException(String message) { super(message); } - public AugustException(String message, Throwable cause) { + protected AugustException(String message, Throwable cause) { super(message, cause); } } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AccessTokenUpdatedListener.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/AccessTokenUpdatedListener.java similarity index 92% rename from bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AccessTokenUpdatedListener.java rename to bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/AccessTokenUpdatedListener.java index 0ec5ca3463d7c..705763848d557 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AccessTokenUpdatedListener.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/AccessTokenUpdatedListener.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.august.internal.handler; +package org.openhab.binding.august.internal.comm; /** * The {@link AccessTokenUpdatedListener} is a callback interface if the access token returned from the service changes diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubListener.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubListener.java new file mode 100644 index 0000000000000..3cb016eb68c1e --- /dev/null +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubListener.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.august.internal.comm; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.JsonElement; + +/** + * The {@link PubNubListener} interface is for handling connects and disconects from PubNub + * + * @author Arne Seime - Initial contribution + */ +@NonNullByDefault +public interface PubNubListener { + + void onPushMessage(String channelName, JsonElement message); + + void onDisconnect(String channelName); + + void onConnect(String channelName); +} diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageException.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageException.java new file mode 100644 index 0000000000000..037b98683a2e3 --- /dev/null +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageException.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.august.internal.comm; + +import org.openhab.binding.august.internal.AugustException; + +/** + * The {@link PubNubMessageException} class wraps exceptions raised when communicating with the PubNub async + * message api + * + * @author Arne Seime - Initial contribution + */ +public class PubNubMessageException extends AugustException { + public PubNubMessageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageSubscriber.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageSubscriber.java new file mode 100644 index 0000000000000..c1c91d4e9c5d8 --- /dev/null +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/PubNubMessageSubscriber.java @@ -0,0 +1,243 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.august.internal.comm; + +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.pubnub.api.PNConfiguration; +import com.pubnub.api.PubNub; +import com.pubnub.api.PubNubException; +import com.pubnub.api.UserId; +import com.pubnub.api.callbacks.SubscribeCallback; +import com.pubnub.api.enums.PNLogVerbosity; +import com.pubnub.api.enums.PNReconnectionPolicy; +import com.pubnub.api.models.consumer.PNStatus; +import com.pubnub.api.models.consumer.message_actions.PNMessageAction; +import com.pubnub.api.models.consumer.objects_api.channel.PNChannelMetadataResult; +import com.pubnub.api.models.consumer.objects_api.membership.PNMembershipResult; +import com.pubnub.api.models.consumer.objects_api.uuid.PNUUIDMetadataResult; +import com.pubnub.api.models.consumer.pubsub.PNMessageResult; +import com.pubnub.api.models.consumer.pubsub.PNPresenceEventResult; +import com.pubnub.api.models.consumer.pubsub.PNSignalResult; +import com.pubnub.api.models.consumer.pubsub.files.PNFileEventResult; +import com.pubnub.api.models.consumer.pubsub.message_actions.PNMessageActionResult; + +/** + * The {@link PubNubMessageSubscriber} wrapper class around PubNub async notifications + * + * @author Arne Seime - Initial contribution + */ +public class PubNubMessageSubscriber { + + private static final String SUBSCRIBE_KEY = "sub-c-1030e062-0ebe-11e5-a5c2-0619f8945a4f"; + private static final Logger logger = LoggerFactory.getLogger(PubNubMessageSubscriber.class); + + private final Set channels = new HashSet<>(); + + private PubNub pub; + + public void init(String userIdString, PubNubListener messageListener) throws PubNubMessageException { + + try { + final UserId userId = new UserId("fn-" + userIdString.toUpperCase(Locale.ROOT)); + PNConfiguration pnConfiguration = new PNConfiguration(userId); + pnConfiguration.setSubscribeKey(SUBSCRIBE_KEY); + pnConfiguration.setReconnectionPolicy(PNReconnectionPolicy.EXPONENTIAL); + pnConfiguration.setLogVerbosity(PNLogVerbosity.BODY); + + pub = new PubNub(pnConfiguration); + + logger.debug("Starting PubNub subscription"); + + pub.addListener(new SubscribeCallback() { + + // PubNub status + @Override + public void status(@NotNull PubNub pubnub, @NotNull PNStatus status) { + logger.debug("Event {} ", status); + switch (status.getOperation()) { + // combine unsubscribe and subscribe handling for ease of use + case PNSubscribeOperation: + case PNUnsubscribeOperation: + // Note: subscribe statuses never have traditional errors, + // just categories to represent different issues or successes + // that occur as part of subscribe + switch (status.getCategory()) { + case PNConnectedCategory: + case PNReconnectedCategory: + status.getAffectedChannels().forEach(e -> messageListener.onConnect(e)); + break; + // Subscribe temporarily failed but reconnected. + // There is no longer any issue. + case PNDisconnectedCategory: + case PNUnexpectedDisconnectCategory: + // Usually an issue with the internet connection. + // This is an error: handle appropriately. + status.getAffectedChannels().forEach(e -> messageListener.onDisconnect(e)); + break; + case PNAccessDeniedCategory: + // PAM does not allow this client to subscribe to this + // channel and channel group configuration. This is + // another explicit error. + logger.warn("Permission denied subscribing to notifications. This is unexpected"); + break; + default: + // You can directly specify more errors by creating + // explicit cases for other error categories of + // `PNStatusCategory` such as `PNTimeoutCategory` or + // `PNMalformedFilterExpressionCategory` or + // `PNDecryptionErrorCategory`. + } + break; + + case PNHeartbeatOperation: + // Heartbeat operations can in fact have errors, + // so it's important to check first for an error. + // For more information on how to configure heartbeat notifications + // through the status PNObjectEventListener callback, refer to + // /docs/android-java/api-reference-configuration#configuration_basic_usage + if (status.isError()) { + // There was an error with the heartbeat operation, handle here + } else { + // heartbeat operation was successful + } + break; + default: { + // Encountered unknown status type + } + } + } + + // Messages + @Override + public void message(@NotNull PubNub pubnub, @NotNull PNMessageResult message) { + // Handle new message stored in message.message + logger.debug("Message {} ", message); + if (message.getChannel() != null) { + logger.debug("Received message on channel {}: {}", message.getChannel(), message.getMessage()); + messageListener.onPushMessage(message.getChannel(), message.getMessage()); + } + } + + // Presence + @Override + public void presence(@NotNull PubNub pubnub, @NotNull PNPresenceEventResult presence) { + logger.debug("Presence Event: {}", presence.getEvent()); + // Can be join, leave, state-change or timeout + + logger.debug("Presence Channel: {}", presence.getChannel()); + // The channel to which the message was published + + logger.debug("Presence Occupancy: {}", presence.getOccupancy()); + // Number of users subscribed to the channel + + logger.debug("Presence State: {}", presence.getState()); + // User state + + logger.debug("Presence UUID: {}", presence.getUuid()); + // UUID to which this event is related + } + + // Signals + @Override + public void signal(@NotNull PubNub pubnub, @NotNull PNSignalResult signal) { + logger.debug("Signal publisher: {}", signal.getPublisher()); + logger.debug("Signal payload: {}", signal.getMessage()); + logger.debug("Signal subscription: {}", signal.getSubscription()); + logger.debug("Signal channel: {}", signal.getChannel()); + logger.debug("Signal timetoken: {}", signal.getTimetoken()); + } + + @Override + public void uuid(@NotNull PubNub pubnub, @NotNull PNUUIDMetadataResult pnUUIDMetadataResult) { + logger.debug("UUID {}", pnUUIDMetadataResult); + } + + @Override + public void channel(@NotNull PubNub pubnub, @NotNull PNChannelMetadataResult pnChannelMetadataResult) { + logger.debug("Channel {}", pnChannelMetadataResult); + } + + @Override + public void membership(@NotNull PubNub pubnub, @NotNull PNMembershipResult pnMembershipResult) { + logger.debug("Membership {}", pnMembershipResult); + } + + // Message actions + @Override + public void messageAction(@NotNull PubNub pubnub, @NotNull PNMessageActionResult pnActionResult) { + PNMessageAction pnMessageAction = pnActionResult.getMessageAction(); + logger.debug("Message action type: {}", pnMessageAction.getType()); + logger.debug("Message action value: {}", pnMessageAction.getValue()); + logger.debug("Message action uuid: {}", pnMessageAction.getUuid()); + logger.debug("Message action actionTimetoken: {}", pnMessageAction.getActionTimetoken()); + logger.debug("Message action messageTimetoken: {}", pnMessageAction.getMessageTimetoken()); + logger.debug("Message action subscription: {}", pnActionResult.getSubscription()); + logger.debug("Message action channel: {}", pnActionResult.getChannel()); + logger.debug("Message action timetoken: {}", pnActionResult.getTimetoken()); + } + + // Files + @Override + public void file(@NotNull PubNub pubnub, @NotNull PNFileEventResult pnFileEventResult) { + logger.debug("File channel: {}", pnFileEventResult.getChannel()); + logger.debug("File publisher: {}", pnFileEventResult.getPublisher()); + logger.debug("File message: {}", pnFileEventResult.getMessage()); + logger.debug("File timetoken: {}", pnFileEventResult.getTimetoken()); + logger.debug("File file.id: {}", pnFileEventResult.getFile().getId()); + logger.debug("File file.name: {}", pnFileEventResult.getFile().getName()); + logger.debug("File file.url: {}", pnFileEventResult.getFile().getUrl()); + } + }); + + } catch (PubNubException e) { + throw new PubNubMessageException("Error creating PubNub subscription", e); + } + } + + public synchronized void addListener(String channelName) { + + boolean added = channels.add(channelName); + if (added) { + logger.debug("Adding listener for channel {}", channelName); + pub.subscribe().channels(List.of(channelName)).execute(); + } else { + logger.warn("Duplicate listener registered for channel {} ignored", channelName); + } + } + + public synchronized void removeListener(String channelName) { + logger.debug("Removing listener for channel {}", channelName); + + boolean removed = channels.remove(channelName); + if (removed) { + pub.unsubscribe().channels(List.of(channelName)).execute(); + } else { + logger.warn("Listener for channel {} not found, cannot remove", channelName); + } + } + + public void dispose() { + pub.unsubscribeAll(); + pub.disconnect(); + pub.destroy(); + channels.clear(); + } +} diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/ApiBridge.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestApiClient.java similarity index 70% rename from bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/ApiBridge.java rename to bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestApiClient.java index ff69207056955..db34ff30dfde4 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/ApiBridge.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestApiClient.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.august.internal; +package org.openhab.binding.august.internal.comm; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -25,8 +25,9 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.august.internal.AugustException; +import org.openhab.binding.august.internal.ConfigurationException; import org.openhab.binding.august.internal.dto.AbstractRequest; -import org.openhab.binding.august.internal.handler.AccessTokenUpdatedListener; import org.openhab.binding.august.internal.logging.RequestLogger; import org.openhab.core.thing.ThingUID; @@ -35,12 +36,13 @@ import com.google.gson.JsonParser; /** - * The {@link ApiBridge} is responsible for API login and communication + * The {@link RestApiClient} is responsible for API login and communication * * @author Arne Seime - Initial contribution */ -public class ApiBridge { +public class RestApiClient { public static final String HEADER_ACCESS_TOKEN = "x-august-access-token"; + public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json"; public static String API_ENDPOINT = "https://api-production.august.com"; private static final String API_KEY = "79fd0eb6-381d-4adf-95a0-47721289d1d9"; @@ -54,9 +56,9 @@ public class ApiBridge { private RequestLogger requestLogger = null; private AccessTokenUpdatedListener listener; - public ApiBridge(HttpClient httpClient) { + public RestApiClient(HttpClient httpClient, Gson gson) { this.httpClient = httpClient; - gson = GsonFactory.create(); + this.gson = gson; } public void init(ThingUID bridgeUid, AccessTokenUpdatedListener listener) { @@ -70,8 +72,8 @@ private Request buildRequest(final AbstractRequest req) { request.getHeaders().remove(HttpHeader.USER_AGENT); request.getHeaders().remove(HttpHeader.ACCEPT); request.header(HttpHeader.USER_AGENT, "August/2019.12.16.4708 CFNetwork/1121.2.2 Darwin/19.3.0"); - request.header(HttpHeader.ACCEPT, "application/json"); - request.header(HttpHeader.CONTENT_TYPE, "application/json"); + request.header(HttpHeader.ACCEPT, CONTENT_TYPE_APPLICATION_JSON); + request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON); request.header("Accept-Version", "0.0.1"); request.header("x-kease-api-key", API_KEY); request.header("x-august-api-key", API_KEY); @@ -82,7 +84,7 @@ private Request buildRequest(final AbstractRequest req) { if (!req.getMethod().contentEquals(HttpMethod.GET.asString())) { // POST, PATCH, PUT final String reqJson = gson.toJson(req); request = request.content(new BytesContentProvider(reqJson.getBytes(StandardCharsets.UTF_8)), - "application/json"); + CONTENT_TYPE_APPLICATION_JSON); } requestLogger.listenTo(request, new String[] {}); @@ -96,7 +98,9 @@ public T sendRequest(final AbstractRequest req, final Type responseType) thr return sendRequestInternal(buildRequest(req), req, responseType); } catch (InterruptedException | TimeoutException | ExecutionException e) { - throw new CommunicationException(String.format("Error sending request to server: %s", e.getMessage()), e); + Thread.currentThread().interrupt(); + throw new RestCommunicationException(String.format("Error sending request to server: %s", e.getMessage()), + e); } } @@ -114,22 +118,34 @@ public T sendRequestInternal(final Request httpRequest, final AbstractReques if (contentResponse.getStatus() == HttpStatus.OK_200) { final JsonObject o = JsonParser.parseString(responseJson).getAsJsonObject(); if (o.has("message")) { - throw new CommunicationException(req, o.get("message").getAsString()); + throw new RestCommunicationException(req, o.get("message").getAsString()); } else { return gson.fromJson(responseJson, responseType); } } else if (contentResponse.getStatus() == HttpStatus.UNAUTHORIZED_401) { if (accessToken == null) { - throw new CommunicationException("Could not renew token"); + throw new RestCommunicationException("Could not renew token"); } else { accessToken = null; // expired return sendRequest(req, responseType); // Retry login + request } + // See some error codes here; + // https://github.com/snjoetw/py-august/blob/78b25da03194d68d70115f36bf926a3a6443e555/august/api_async.py#L260 + } else if (contentResponse.getStatus() == HttpStatus.FORBIDDEN_403) { throw new ConfigurationException("Invalid credentials"); + } else if (contentResponse.getStatus() == HttpStatus.REQUEST_TIMEOUT_408) { + throw new RestCommunicationException( + "The operation timed out because the bridge (connect) failed to respond"); + } else if (contentResponse.getStatus() == HttpStatus.LOCKED_423) { + throw new RestCommunicationException("The operation failed because the bridge (connect) is in use"); + } else if (contentResponse.getStatus() == HttpStatus.UNPROCESSABLE_ENTITY_422) { + throw new RestCommunicationException("The operation failed because the bridge (connect) is offline."); + } else if (contentResponse.getStatus() == HttpStatus.TOO_MANY_REQUESTS_429) { + throw new RestCommunicationException("Too many requests, reduce polling time"); } else { - throw new CommunicationException("Error sending request to server. Server responded with " + throw new RestCommunicationException("Error sending request to server. Server responded with " + contentResponse.getStatus() + " and payload " + responseJson); } } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/CommunicationException.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestCommunicationException.java similarity index 62% rename from bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/CommunicationException.java rename to bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestCommunicationException.java index 64a1c0ea136a2..220814c9775d5 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/CommunicationException.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/comm/RestCommunicationException.java @@ -10,29 +10,30 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.august.internal; +package org.openhab.binding.august.internal.comm; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.august.internal.AugustException; import org.openhab.binding.august.internal.dto.AbstractRequest; /** - * The {@link CommunicationException} class wraps exceptions raised when communicating with the API + * The {@link RestCommunicationException} class wraps exceptions raised when communicating with the API * * @author Arne Seime - Initial contribution */ @NonNullByDefault -public class CommunicationException extends AugustException { +public class RestCommunicationException extends AugustException { private static final long serialVersionUID = 1L; - public CommunicationException(final String message, final Throwable cause) { + public RestCommunicationException(final String message, final Throwable cause) { super(message, cause); } - public CommunicationException(final String message) { + public RestCommunicationException(final String message) { super(message); } - public CommunicationException(final AbstractRequest req, final String overallStatus) { + public RestCommunicationException(final AbstractRequest req, final String overallStatus) { super("Server responded with error to request " + req.getClass().getSimpleName() + "/" + req.getRequestUrl() + ": " + overallStatus); } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/AccountConfiguration.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/AccountConfiguration.java index 486cc1c7f25eb..3d72beea23c34 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/AccountConfiguration.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/AccountConfiguration.java @@ -35,7 +35,7 @@ public class AccountConfiguration { @Nullable public String validationCode; - public int refreshIntervalSeconds = 120; + public int refreshIntervalSeconds = 3600; @java.lang.Override public java.lang.String toString() { diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/LockConfiguration.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/LockConfiguration.java index 7b6841adc2e92..7c94d3ada8006 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/LockConfiguration.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/config/LockConfiguration.java @@ -23,11 +23,8 @@ public class LockConfiguration { */ public String lockId; - public long refreshIntervalSeconds = 120; - @Override public String toString() { - return "LockConfiguration{" + "lockId='" + lockId + '\'' + ", refreshIntervalSeconds=" + refreshIntervalSeconds - + '}'; + return "LockConfiguration{" + "lockId='" + lockId + '\'' + '}'; } } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/discovery/AugustDiscoveryService.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/discovery/AugustDiscoveryService.java index 32f64fa546f8e..6cce7c7b27072 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/discovery/AugustDiscoveryService.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/discovery/AugustDiscoveryService.java @@ -36,7 +36,7 @@ public class AugustDiscoveryService extends AbstractDiscoveryService { public static final Set DISCOVERABLE_THING_TYPES_UIDS = Collections .singleton(BindingConstants.THING_TYPE_LOCK); - private static final long REFRESH_INTERVAL_MINUTES = 60 * 60 * 4; // EVERY 4 HOUR + private static final long DISCOVERY_INTERVAL_MINUTES = 60 * 4; // EVERY 4 HOUR public static final String LOCK_ID_PROPERTY = "lockId"; private final Logger logger = LoggerFactory.getLogger(AugustDiscoveryService.class); private final AugustAccountHandler accountHandler; @@ -50,7 +50,7 @@ public AugustDiscoveryService(final AugustAccountHandler accountHandler) { @Override protected void startBackgroundDiscovery() { discoveryJob = Optional - .of(scheduler.scheduleWithFixedDelay(this::startScan, 0, REFRESH_INTERVAL_MINUTES, TimeUnit.MINUTES)); + .of(scheduler.scheduleWithFixedDelay(this::startScan, 0, DISCOVERY_INTERVAL_MINUTES, TimeUnit.MINUTES)); } @Override @@ -88,7 +88,7 @@ protected void stopBackgroundDiscovery() { } @Override - protected void stopScan() { + protected synchronized void stopScan() { logger.debug("Stop scan for devices."); super.stopScan(); } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/AbstractRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/AbstractRequest.java index 9f46672ecf6fb..47a5272e39033 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/AbstractRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/AbstractRequest.java @@ -20,10 +20,10 @@ * * @author Arne Seime - Initial contribution. */ -public abstract class AbstractRequest { - public abstract String getRequestUrl(); +public interface AbstractRequest { + String getRequestUrl(); - public String getMethod() { + default String getMethod() { return HttpMethod.GET.asString(); } } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockRequest.java index 3caa9fe4686f3..7b134be19e02e 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockRequest.java @@ -18,7 +18,7 @@ * * @author Arne Seime - Initial contribution. */ -public class GetLockRequest extends AbstractRequest { +public class GetLockRequest implements AbstractRequest { String lockId; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockResponse.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockResponse.java index 6ae7d96beab00..7c44325bf48fe 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockResponse.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLockResponse.java @@ -51,4 +51,6 @@ public class GetLockResponse { public LockStatusDTO lockStatus; public String currentFirmwareVersion; + + public String pubsubChannel; } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLocksRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLocksRequest.java index afc4ce158190d..2192c7176082f 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLocksRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetLocksRequest.java @@ -18,7 +18,7 @@ * * @author Arne Seime - Initial contribution. */ -public class GetLocksRequest extends AbstractRequest { +public class GetLocksRequest implements AbstractRequest { @Override public String getRequestUrl() { return "/users/locks/mine"; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionRequest.java index 11e14a8ee8928..340208180d659 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionRequest.java @@ -21,7 +21,7 @@ * @author Arne Seime - Initial contribution. */ -public class GetSessionRequest extends AbstractRequest { +public class GetSessionRequest implements AbstractRequest { @SerializedName("identifier") public String loginId; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionResponse.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionResponse.java index 115bd37b1b80e..b9156d670c556 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionResponse.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetSessionResponse.java @@ -26,4 +26,7 @@ public class GetSessionResponse { // If true, installId is verified - no need for 2 factor check public boolean hasInstallId; + + // UserId logged in - used for async message subscription + public String userId; } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetValidationCodeRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetValidationCodeRequest.java index a3b107442cb0c..ea2dd24dfe8f6 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetValidationCodeRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/GetValidationCodeRequest.java @@ -19,7 +19,7 @@ * @author Arne Seime - Initial contribution. */ -public class GetValidationCodeRequest extends AbstractRequest { +public class GetValidationCodeRequest implements AbstractRequest { public String value; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/RemoteOperateLockRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/RemoteOperateLockRequest.java index 3c190c7e7f109..8e1ee6dd8c5a0 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/RemoteOperateLockRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/RemoteOperateLockRequest.java @@ -20,7 +20,7 @@ * * @author Arne Seime - Initial contribution. */ -public class RemoteOperateLockRequest extends AbstractRequest { +public class RemoteOperateLockRequest implements AbstractRequest { String lockId; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeRequest.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeRequest.java index edead489b2262..b26378be242a5 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeRequest.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeRequest.java @@ -19,7 +19,7 @@ * @author Arne Seime - Initial contribution. */ -public class ValidateCodeRequest extends AbstractRequest { +public class ValidateCodeRequest implements AbstractRequest { public String code; diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeResponse.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeResponse.java index 79373093a7e35..0ddfe21d668e2 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeResponse.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/dto/ValidateCodeResponse.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.august.internal.dto; +import com.google.gson.annotations.SerializedName; + /** * All classes in the .dto are data transfer classes used by the GSON mapper. This class reflects a * part of a request/response data structure. @@ -22,7 +24,8 @@ public class ValidateCodeResponse { public String userId; - public String _value; + @SerializedName("_value") + public String value; public String resolution; } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustAccountHandler.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustAccountHandler.java index 59dc69ee64f92..7e48b057ef8f3 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustAccountHandler.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustAccountHandler.java @@ -10,7 +10,6 @@ * * SPDX-License-Identifier: EPL-2.0 */ - package org.openhab.binding.august.internal.handler; import java.time.ZonedDateTime; @@ -26,10 +25,13 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.august.internal.ApiBridge; import org.openhab.binding.august.internal.AugustException; import org.openhab.binding.august.internal.AuthenticationStatus; -import org.openhab.binding.august.internal.CommunicationException; +import org.openhab.binding.august.internal.comm.AccessTokenUpdatedListener; +import org.openhab.binding.august.internal.comm.PubNubListener; +import org.openhab.binding.august.internal.comm.PubNubMessageSubscriber; +import org.openhab.binding.august.internal.comm.RestApiClient; +import org.openhab.binding.august.internal.comm.RestCommunicationException; import org.openhab.binding.august.internal.config.AccountConfiguration; import org.openhab.binding.august.internal.dto.GetLocksRequest; import org.openhab.binding.august.internal.dto.GetLocksResponse; @@ -50,34 +52,40 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; /** * The {@link AugustAccountHandler} is responsible for authentication * - * + * * @author Arne Seime - Initial contribution */ @NonNullByDefault -public class AugustAccountHandler extends BaseBridgeHandler implements AccessTokenUpdatedListener { +public class AugustAccountHandler extends BaseBridgeHandler implements AccessTokenUpdatedListener, PubNubListener { public static final String STORAGE_KEY_AUTH_STATUS = "AUTH_STATUS"; public static final String STORAGE_KEY_INSTALLID = "INSTALL_ID"; public static final String STORAGE_KEY_ACCESS_TOKEN = "ACCESS_TOKEN"; public static final String STORAGE_KEY_ACCESS_TOKEN_EXPIRY = "ACCESS_TOKEN_EXPIRY"; + public static final String STORAGE_KEY_USERID = "USERID"; private final Logger logger = LoggerFactory.getLogger(AugustAccountHandler.class); private Optional> statusFuture = Optional.empty(); - @NonNullByDefault({}) + @Nullable AccountConfiguration config; - private ApiBridge apiBridge; + private RestApiClient restApiClient; + + private PubNubMessageSubscriber messageSubscriber; private Storage storage; private Map locks = new HashMap<>(); + private Map eventListeners = new HashMap<>(); - public AugustAccountHandler(final Bridge bridge, ApiBridge apiBridge, Storage storage) { + public AugustAccountHandler(final Bridge bridge, RestApiClient restApiClient, Storage storage) { super(bridge); - this.apiBridge = apiBridge; + this.restApiClient = restApiClient; this.storage = storage; - apiBridge.init(bridge.getUID(), this); + this.messageSubscriber = new PubNubMessageSubscriber(); + restApiClient.init(bridge.getUID(), this); } @Override @@ -92,6 +100,7 @@ public Map getLocks() { @Override public void initialize() { + logger.debug("Initializing bridge"); // Stop any pending updates if any stopScheduledUpdate(); @@ -118,7 +127,7 @@ public void initialize() { @Nullable String accessToken = storage.get(STORAGE_KEY_ACCESS_TOKEN); if (accessToken != null) { - apiBridge.setAccessToken(accessToken); + restApiClient.setAccessToken(accessToken); } else { logger.debug("No previous access token"); } @@ -135,7 +144,7 @@ public void initialize() { // Initiate 2 factor GetValidationCodeRequest validationCodeRequest = new GetValidationCodeRequest(config.email); - GetValidationCodeResponse validationCodeResponse = apiBridge.sendRequest(validationCodeRequest, + GetValidationCodeResponse validationCodeResponse = restApiClient.sendRequest(validationCodeRequest, new TypeToken() { }.getType()); if ("sent".equals(validationCodeResponse.code)) { @@ -165,7 +174,7 @@ public void initialize() { logger.info("Verifying 2 factor code"); ValidateCodeRequest validateCodeRequest = new ValidateCodeRequest(config.validationCode, config.email, config.phone); - ValidateCodeResponse validateCodeResponse = apiBridge.sendRequest(validateCodeRequest, + ValidateCodeResponse validateCodeResponse = restApiClient.sendRequest(validateCodeRequest, new TypeToken() { }.getType()); if ("token_incomplete".equals(validateCodeResponse.resolution)) { @@ -173,15 +182,12 @@ public void initialize() { logger.info("2 factor authentication complete"); getThing().getConfiguration().remove("validationCode"); storage.put(STORAGE_KEY_AUTH_STATUS, AuthenticationStatus.VALIDATED.toString()); - doPoll(); + loginComplete(); } } break; case VALIDATED: - if (isSessionExpired() || storage.get(STORAGE_KEY_ACCESS_TOKEN) == null) { - obtainNewSession(); - } - doPoll(); + loginComplete(); break; } @@ -191,7 +197,12 @@ public void initialize() { logger.warn("Error logging in", e); clearStorage(); } + } + private void loginComplete() throws AugustException { + obtainNewSession(); + messageSubscriber.init(storage.get(STORAGE_KEY_USERID), this); + doPoll(); statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::doPoll, config.refreshIntervalSeconds, config.refreshIntervalSeconds, TimeUnit.SECONDS)); } @@ -199,14 +210,15 @@ public void initialize() { private void obtainNewSession() throws AugustException { GetSessionRequest getSessionRequestRefresh = new GetSessionRequest(config.email, config.password, storage.get(STORAGE_KEY_INSTALLID)); - GetSessionResponse getSessionResponseRefresh = apiBridge.sendRequest(getSessionRequestRefresh, + GetSessionResponse getSessionResponseRefresh = restApiClient.sendRequest(getSessionRequestRefresh, new TypeToken() { }.getType()); - logger.debug("New access token obtained, expiry {}", getSessionResponseRefresh.expiresAt.toString()); + logger.debug("New access token obtained, expiry {}", getSessionResponseRefresh.expiresAt); - storage.put(STORAGE_KEY_ACCESS_TOKEN, apiBridge.getLastAccessTokenFromHeader()); + storage.put(STORAGE_KEY_ACCESS_TOKEN, restApiClient.getLastAccessTokenFromHeader()); storage.put(STORAGE_KEY_ACCESS_TOKEN_EXPIRY, getSessionResponseRefresh.expiresAt.toString()); + storage.put(STORAGE_KEY_USERID, getSessionResponseRefresh.userId); } private void clearStorage() { @@ -214,15 +226,18 @@ private void clearStorage() { storage.remove(STORAGE_KEY_ACCESS_TOKEN_EXPIRY); storage.remove(STORAGE_KEY_INSTALLID); storage.remove(STORAGE_KEY_AUTH_STATUS); + storage.remove(STORAGE_KEY_USERID); } @Override public void dispose() { + messageSubscriber.dispose(); stopScheduledUpdate(); super.dispose(); } public synchronized void doPoll() { + logger.info("Polling for new account status/lock overview"); try { if (isSessionExpired()) { @@ -230,7 +245,7 @@ public synchronized void doPoll() { } GetLocksRequest getLocksRequest = new GetLocksRequest(); - final GetLocksResponse getLocksResponse = apiBridge.sendRequest(getLocksRequest, + final GetLocksResponse getLocksResponse = restApiClient.sendRequest(getLocksRequest, new TypeToken() { }.getType()); @@ -238,10 +253,7 @@ public synchronized void doPoll() { .collect(Collectors.toMap(e -> e.getKey(), e -> new Lock(e.getValue()))); updateStatus(ThingStatus.ONLINE); logger.info("Fetching lock overview success, found {} lock(s)", locks.size()); - getThing().getThings().stream() - .filter(e -> e.isEnabled() && ((AugustLockHandler) e.getHandler()).isInitialized()) - .map(e -> (AugustLockHandler) e.getHandler()).forEach(e -> e.doPoll()); - } catch (final CommunicationException e) { + } catch (final RestCommunicationException e) { logger.warn("Error initializing data: {}, retrying at specified refreshInterval", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error fetching data: " + e.getMessage()); @@ -278,8 +290,8 @@ private void stopScheduledUpdate() { }); } - public ApiBridge getApiBridge() { - return apiBridge; + public RestApiClient getApiBridge() { + return restApiClient; } @Override @@ -289,4 +301,49 @@ public void onAccessTokenUpdated(@Nullable String updatedAccessToken) { storage.put(STORAGE_KEY_ACCESS_TOKEN, updatedAccessToken); } } + + @Override + public void onDisconnect(String channelName) { + // Forward to lock handler + PubNubListener pubNubListener = eventListeners.get(channelName); + if (pubNubListener != null) { + pubNubListener.onDisconnect(channelName); + } + } + + @Override + public void onConnect(String channelName) { + // Forward to lock handler + PubNubListener pubNubListener = eventListeners.get(channelName); + if (pubNubListener != null) { + pubNubListener.onConnect(channelName); + } + } + + @Override + public void onPushMessage(String channelName, JsonElement message) { + // Find correct handler + PubNubListener pubNubListener = eventListeners.get(channelName); + if (pubNubListener != null) { + pubNubListener.onPushMessage(channelName, message); + } + } + + public void deregisterForEvents(AugustLockHandler augustLockHandler) { + Optional> first = eventListeners.entrySet().stream() + .filter(e -> e.getValue() == augustLockHandler).findFirst(); + first.ifPresent(e -> { + String channelName = e.getKey(); + messageSubscriber.removeListener(channelName); + eventListeners.remove(channelName); + + }); + } + + public void registerForEvents(AugustLockHandler augustLockHandler, String channelName) { + if (!eventListeners.containsKey(channelName)) { + eventListeners.put(channelName, augustLockHandler); + messageSubscriber.addListener(channelName); + } + } } diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustLockHandler.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustLockHandler.java index 4bfaebb08dbd6..7a31af74a8422 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustLockHandler.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustLockHandler.java @@ -23,13 +23,14 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.august.internal.ApiBridge; import org.openhab.binding.august.internal.AugustException; +import org.openhab.binding.august.internal.comm.PubNubListener; +import org.openhab.binding.august.internal.comm.RestApiClient; import org.openhab.binding.august.internal.config.LockConfiguration; import org.openhab.binding.august.internal.dto.GetLockRequest; import org.openhab.binding.august.internal.dto.GetLockResponse; +import org.openhab.binding.august.internal.dto.LockStatusDTO; import org.openhab.binding.august.internal.dto.RemoteOperateLockRequest; import org.openhab.binding.august.internal.dto.RemoteOperateLockResponse; import org.openhab.core.library.types.OnOffType; @@ -44,10 +45,13 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; /** @@ -56,70 +60,75 @@ * * @author Arne Seime - Initial contribution */ -public class AugustLockHandler extends BaseThingHandler { +public class AugustLockHandler extends BaseThingHandler implements PubNubListener { public static final String ERROR_MESSAGE_UNSUPPORTED_COMMAND = "Unsupported command {} for channel {}"; + public static final int LOCK_POLLING_SECONDS = 1800; private final Logger logger = LoggerFactory.getLogger(AugustLockHandler.class); private LockConfiguration config; - @NonNullByDefault({}) - private ApiBridge apiBridge; - public AugustLockHandler(Thing thing, ApiBridge apiBridge) { - super(thing); - this.apiBridge = apiBridge; - } + private RestApiClient restApiClient; + private Gson gson; + + @Nullable + private AugustAccountHandler handler; - public AugustLockHandler(Thing thing) { + public AugustLockHandler(Thing thing, Gson gson) { super(thing); + this.gson = gson; } private GetLockResponse lock; private Optional> statusFuture = Optional.empty(); - private boolean initialized = false; - @Override public void initialize() { - updateStatus(ThingStatus.OFFLINE); + updateStatus(ThingStatus.UNKNOWN); config = getConfigAs(LockConfiguration.class); logger.info("Initializing lock {}", config.lockId); stopScheduledUpdate(); // If any - // Workaround for testing - need to inject ApiBridge but having difficulties injecting the bridge handler in the + // Workaround for testing - need to inject RestApiClient but having difficulties injecting the bridge handler in + // the // tests - if (getBridge() != null) { - AugustAccountHandler handler = (AugustAccountHandler) getBridge().getHandler(); + Bridge bridge = getBridge(); + if (bridge != null) { + AugustAccountHandler handler = (AugustAccountHandler) bridge.getHandler(); if (handler != null) { - apiBridge = handler.getApiBridge(); + restApiClient = handler.getApiBridge(); + this.handler = handler; } } - Objects.requireNonNull(apiBridge, - "ApiBridge is null - must be set either directly in constructor or fetched via getBridge().getHandler()"); - logger.debug("{} Initializing lock", config.lockId); - statusFuture = Optional.of(scheduler.scheduleWithFixedDelay(this::doPoll, config.refreshIntervalSeconds, - config.refreshIntervalSeconds, TimeUnit.SECONDS)); + Objects.requireNonNull(restApiClient, + "RestApiClient is null - must be set either directly in constructor or fetched via getBridge().getHandler()"); + statusFuture = Optional.of(scheduler.schedule(this::doPoll, 1, TimeUnit.SECONDS)); logger.info("{} Lock init successful", config.lockId); - initialized = true; } @Override public void dispose() { - initialized = false; + handler.deregisterForEvents(this); + stopScheduledUpdate(); super.dispose(); } public void doPoll() { - if (getBridge().getStatus() != ThingStatus.ONLINE) { + + Bridge bridge = getBridge(); + + if (bridge != null && bridge.getStatus() != ThingStatus.ONLINE) { logger.warn("{} Not polling lock since bridge isn't online yet. Bridge reported status {}", config.lockId, getBridge().getStatus()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); - stopScheduledUpdate(); + // Schedule reconnect retry every 5 seconds + statusFuture = Optional.of(scheduler.schedule(this::doPoll, 5, TimeUnit.SECONDS)); + return; } @@ -127,19 +136,22 @@ public void doPoll() { try { final GetLockRequest getLockRequest = new GetLockRequest(config.lockId); - lock = apiBridge.sendRequest(getLockRequest, new TypeToken() { + lock = restApiClient.sendRequest(getLockRequest, new TypeToken() { }.getType()); Map properties = createProperties(lock); updateThing(editThing().withProperties(properties).build()); - updateStatus(ThingStatus.ONLINE); thing.getChannels().forEach(e -> handleCommandInternal(e.getUID(), null)); + // Register listener + handler.registerForEvents(this, lock.pubsubChannel); } catch (AugustException ex) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Error retrieving data from server: " + ex.getMessage()); // Undef all channels if error thing.getChannels().forEach(e -> updateState(e.getUID(), UnDefType.UNDEF)); } + // Do poll to catch up with any message not received via PubNub + statusFuture = Optional.of(scheduler.schedule(this::doPoll, LOCK_POLLING_SECONDS, TimeUnit.SECONDS)); } private Map createProperties(GetLockResponse lockResponse) { @@ -153,6 +165,18 @@ private Map createProperties(GetLockResponse lockResponse) { return properties; } + /** + * Stops this thing's polling future + */ + private void stopScheduledUpdate() { + statusFuture.ifPresent(future -> { + if (!future.isCancelled()) { + future.cancel(true); + } + statusFuture = Optional.empty(); + }); + } + @Override public void handleCommand(final ChannelUID channelUID, final @Nullable Command command) { handleCommandInternal(channelUID, command); @@ -193,11 +217,12 @@ private void handleLockStateCommand(ChannelUID channelUID, Command command) { logger.info("{} Querying lock/performing operation for lock state", config.lockId); final RemoteOperateLockRequest operateLockRequest = new RemoteOperateLockRequest(config.lockId, getOperationFromCommand(command)); - RemoteOperateLockResponse rsp = apiBridge.sendRequest(operateLockRequest, + RemoteOperateLockResponse rsp = restApiClient.sendRequest(operateLockRequest, new TypeToken() { }.getType()); - updateState(channelUID, rsp.lockStatus.equals("kAugLockState_Unlocked") ? OnOffType.OFF : OnOffType.ON); + // updateState(channelUID, rsp.lockStatus.equals("kAugLockState_Unlocked") ? OnOffType.OFF : + // OnOffType.ON); } catch (AugustException e) { logger.warn("{} Error contacting lock", config.lockId, e); updateState(channelUID, UnDefType.UNDEF); @@ -227,26 +252,73 @@ private RemoteOperateLockRequest.Operation getOperationFromCommand(Command comma } } - /** - * Stops this thing's polling future - */ - private void stopScheduledUpdate() { - statusFuture.ifPresent(future -> { - if (!future.isCancelled()) { - future.cancel(true); + @Override + public void onConnect(String channelName) { + logger.info("PubNub connected"); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void onDisconnect(String channelName) { + logger.info("PubNub disconnected"); + stopScheduledUpdate(); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Server push message connection lost"); + } + + @Override + public void onPushMessage(String channelName, JsonElement message) { + + try { + logger.info("Received pubsub message {}", gson.toJson(message)); + JsonElement remoteEvent = message.getAsJsonObject().get("remoteEvent"); + if (remoteEvent != null) { + logger.info("Unhandled EVENT"); + } else { + LockStatusDTO asyncStatus = gson.fromJson(message, new TypeToken() { + }.getType()); + + State lockState = UnDefType.UNDEF; + switch (asyncStatus.lockStatus) { + case "locked": + lockState = OnOffType.ON; + break; + case "unlocked": + lockState = OnOffType.OFF; + break; + default: + logger.warn("Unexpected lockState from async message {}", asyncStatus.lockStatus); + } + + updateState(CHANNEL_LOCK_STATE, lockState); + + State doorState = UnDefType.UNDEF; + switch (asyncStatus.doorStatus) { + case "open": + doorState = OpenClosedType.OPEN; + break; + case "closed": + doorState = OpenClosedType.CLOSED; + break; + default: + logger.warn("Unexpected doorState from async message {}", asyncStatus.doorStatus); + } + + updateState(CHANNEL_DOOR_STATE, doorState); + } - statusFuture = Optional.empty(); - }); + } catch (Exception e) { + logger.error("Error handling pubnub message on channel {}: {}", channelName, message, e); + } } @Override - public boolean isInitialized() { - return initialized; + public String toString() { + return "AugustLockHandler{" + "config=" + config + '}'; } /** * Only used for testing, need to make public for mocking purposes - * + * * @return */ @Override diff --git a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustThingHandlerFactory.java b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustThingHandlerFactory.java index c46059214306c..0790c4685999d 100644 --- a/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustThingHandlerFactory.java +++ b/bundles/org.openhab.binding.august/src/main/java/org/openhab/binding/august/internal/handler/AugustThingHandlerFactory.java @@ -23,8 +23,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.openhab.binding.august.internal.ApiBridge; import org.openhab.binding.august.internal.BindingConstants; +import org.openhab.binding.august.internal.GsonFactory; +import org.openhab.binding.august.internal.comm.RestApiClient; import org.openhab.binding.august.internal.discovery.AugustDiscoveryService; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.io.net.http.HttpClientFactory; @@ -43,6 +44,8 @@ import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; +import com.google.gson.Gson; + /** * The {@link AugustThingHandlerFactory} is responsible for creating things and thing * handlers. @@ -60,6 +63,8 @@ public class AugustThingHandlerFactory extends BaseThingHandlerFactory { private StorageService storageService; private Map> discoveryServiceRegs = new HashMap<>(); + private Gson gson = GsonFactory.create(); + @Activate public AugustThingHandlerFactory(@Reference HttpClientFactory httpClientFactory, @Reference StorageService storageService) { @@ -71,10 +76,10 @@ public AugustThingHandlerFactory(@Reference HttpClientFactory httpClientFactory, protected @Nullable ThingHandler createHandler(final Thing thing) { final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (BindingConstants.THING_TYPE_LOCK.equals(thingTypeUID)) { - return new AugustLockHandler(thing); + return new AugustLockHandler(thing, gson); } else if (BindingConstants.THING_TYPE_ACCOUNT.equals(thingTypeUID)) { - ApiBridge apiBridge = new ApiBridge(httpClient); - AugustAccountHandler accountHandler = new AugustAccountHandler((Bridge) thing, apiBridge, + RestApiClient restApiClient = new RestApiClient(httpClient, gson); + AugustAccountHandler accountHandler = new AugustAccountHandler((Bridge) thing, restApiClient, storageService.getStorage(thing.getUID().toString(), FrameworkUtil.getBundle(getClass()).adapt(BundleWiring.class).getClassLoader())); registerDeviceDiscoveryService(accountHandler); diff --git a/bundles/org.openhab.binding.august/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.august/src/main/resources/OH-INF/binding/binding.xml index 6e7bfb069045a..a579958ffe135 100644 --- a/bundles/org.openhab.binding.august/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.august/src/main/resources/OH-INF/binding/binding.xml @@ -4,6 +4,6 @@ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> August/Yale Access Binding - This is the binding for August door locks such as the Yale Doorman L3 + This is the binding for August/Yale WiFi connected door locks diff --git a/bundles/org.openhab.binding.august/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.august/src/main/resources/OH-INF/config/config.xml index de4377dda8659..5a676bfb87eda 100644 --- a/bundles/org.openhab.binding.august/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.august/src/main/resources/OH-INF/config/config.xml @@ -28,7 +28,7 @@ https://openhab.org/schemas/config-description-1.0.0.xsd"> How often in seconds to fetch updates from August service (polling interval) - 120 + 3600 true @@ -38,12 +38,6 @@ https://openhab.org/schemas/config-description-1.0.0.xsd"> Id of lock - - - How often in seconds to fetch updates from August service (polling interval) - 60 - true - diff --git a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/PubNubMessageSubscriberTest.java b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/PubNubMessageSubscriberTest.java new file mode 100644 index 0000000000000..63f1dfad68b42 --- /dev/null +++ b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/PubNubMessageSubscriberTest.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.august.internal; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.august.internal.comm.PubNubListener; +import org.openhab.binding.august.internal.comm.PubNubMessageException; +import org.openhab.binding.august.internal.comm.PubNubMessageSubscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +/** + * Simple manual testing helper class to debug/analyse PubNub server push messages + * Must populate userId and channelName + set breakpoint + * + * @author Arne Seime - Initial contribution + */ +class PubNubMessageSubscriberTest implements PubNubListener { + + private static final Logger logger = LoggerFactory.getLogger(PubNubMessageSubscriberTest.class); + + private String userId = "d1174952-e923-4e76-bc8c-05c6cad5f654";// = "INSERT_USERID_FROM_GET_SESSION_RESPONSE"; + private String channelName = "54aece2f-b021-4362-9d0f-2cdcd400f3e6";// = "INSERT_PUBSUBCHANNEL_FROM_LOCKS_RESPONSE"; + + private Gson gson = new Gson(); + + @Test + // @Disabled("Needs userId and channelName populated - for manual testing/analyzing responses from PubNub") + void testSubscribe() throws PubNubMessageException { + + PubNubMessageSubscriber subscriber = new PubNubMessageSubscriber(); + subscriber.init(userId, this); + subscriber.addListener(channelName); + // Intentionally duplicate + subscriber.addListener(channelName); + logger.debug("Started listener"); + + subscriber.removeListener(channelName); + // Intentionally duplicate + subscriber.removeListener(channelName); + subscriber.dispose(); + } + + @Override + public void onPushMessage(String channelName, JsonElement message) { + logger.debug("Message received on channel {}: {}", channelName, gson.toJson(message)); + } + + @Override + public void onDisconnect(String channelName) { + logger.debug("Channel {} disconnected", channelName); + } + + @Override + public void onConnect(String channelName) { + logger.debug("Channel {} connected", channelName); + } +} diff --git a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/SerializationDeserializationTest.java b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/SerializationDeserializationTest.java index 32e05bd2c759d..5b397daefbeea 100644 --- a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/SerializationDeserializationTest.java +++ b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/SerializationDeserializationTest.java @@ -28,17 +28,17 @@ * * @author Arne Seime - Initial contribution */ -public class SerializationDeserializationTest { +class SerializationDeserializationTest { protected org.openhab.binding.august.internal.dto.WireHelper wireHelper = new org.openhab.binding.august.internal.dto.WireHelper(); @Test - public void testGetSessionRequest() throws IOException { + void testGetSessionRequest() throws IOException { final Type type = new TypeToken() { }.getType(); - final GetSessionRequest message = wireHelper.deSerializeFromClasspathResource("/get_session_request.json", - type); + final GetSessionRequest message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/get_session_request.json", type); assertEquals("installId", message.installId); assertEquals("email:email@address.com", message.loginId); @@ -46,47 +46,47 @@ public void testGetSessionRequest() throws IOException { } @Test - public void testGetSessionResponse() throws IOException { + void testGetSessionResponse() throws IOException { final Type type = new TypeToken() { }.getType(); - final GetSessionResponse message = wireHelper.deSerializeFromClasspathResource("/get_session_response.json", - type); + final GetSessionResponse message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/get_session_response.json", type); assertEquals(ZonedDateTime.parse("2023-05-07T15:24:20.799Z"), message.expiresAt); assertEquals(true, message.hasInstallId); } @Test - public void testGetValidationCodeRequest() throws IOException { + void testGetValidationCodeRequest() throws IOException { final Type type = new TypeToken() { }.getType(); final GetValidationCodeRequest message = wireHelper - .deSerializeFromClasspathResource("/get_validation_code_request.json", type); + .deSerializeFromClasspathResource("/mock_responses/get_validation_code_request.json", type); assertEquals("email@address.com", message.value); } @Test - public void testGetValidationCodeResponse() throws IOException { + void testGetValidationCodeResponse() throws IOException { final Type type = new TypeToken() { }.getType(); final GetValidationCodeResponse message = wireHelper - .deSerializeFromClasspathResource("/get_validation_code_response.json", type); + .deSerializeFromClasspathResource("/mock_responses/get_validation_code_response.json", type); assertEquals("sent", message.code); assertEquals("email@address.com", message.value); } @Test - public void testValidateCodeRequest() throws IOException { + void testValidateCodeRequest() throws IOException { final Type type = new TypeToken() { }.getType(); - final ValidateCodeRequest message = wireHelper.deSerializeFromClasspathResource("/validate_code_request.json", - type); + final ValidateCodeRequest message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/validate_code_request.json", type); assertEquals("email@address.com", message.email); assertEquals("+4700000000", message.phone); @@ -94,24 +94,25 @@ public void testValidateCodeRequest() throws IOException { } @Test - public void testValidateCodeResponse() throws IOException { + void testValidateCodeResponse() throws IOException { final Type type = new TypeToken() { }.getType(); - final ValidateCodeResponse message = wireHelper.deSerializeFromClasspathResource("/validate_code_response.json", - type); + final ValidateCodeResponse message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/validate_code_response.json", type); assertEquals("UUID", message.userId); - assertEquals("email:email@address.com", message._value); + assertEquals("email:email@address.com", message.value); assertEquals("token_incomplete", message.resolution); } @Test - public void testGetLocksResponse() throws IOException { + void testGetLocksResponse() throws IOException { final Type type = new TypeToken() { }.getType(); - final GetLocksResponse locks = wireHelper.deSerializeFromClasspathResource("/get_locks_response.json", type); + final GetLocksResponse locks = wireHelper + .deSerializeFromClasspathResource("/mock_responses/get_locks_response.json", type); assertNotNull(locks); assertEquals(2, locks.size()); @@ -125,11 +126,12 @@ public void testGetLocksResponse() throws IOException { } @Test - public void testGetLockResponse() throws IOException { + void testGetLockResponse() throws IOException { final Type type = new TypeToken() { }.getType(); - final GetLockResponse message = wireHelper.deSerializeFromClasspathResource("/get_lock_response.json", type); + final GetLockResponse message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/get_lock_response.json", type); assertEquals("LockName", message.lockName); assertEquals("LockId1", message.lockId); @@ -151,14 +153,26 @@ public void testGetLockResponse() throws IOException { } @Test - public void testRemoteOperateLockResponse() throws IOException { + void testRemoteOperateLockResponse() throws IOException { final Type type = new TypeToken() { }.getType(); final RemoteOperateLockResponse message = wireHelper - .deSerializeFromClasspathResource("/remoteoperate_lock_response.json", type); + .deSerializeFromClasspathResource("/mock_responses/remoteoperate_lock_response.json", type); assertEquals("kAugLockState_Unlocked", message.lockStatus); assertEquals("kAugDoorState_Closed", message.doorStatus); } + + @Test + void testLockPushMessage() throws IOException { + final Type type = new TypeToken() { + }.getType(); + + final LockStatusDTO message = wireHelper + .deSerializeFromClasspathResource("/mock_responses/lock_status_async.json", type); + + assertEquals("unlocked", message.lockStatus); + assertEquals("closed", message.doorStatus); + } } diff --git a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/WireHelper.java b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/WireHelper.java index caf88923fb48a..d77b4697b8d37 100644 --- a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/WireHelper.java +++ b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/dto/WireHelper.java @@ -31,24 +31,17 @@ public WireHelper() { gson = GsonFactory.create(); } - public T deSerializeResponse(final String jsonClasspathName, final Type type) throws IOException { - final String json = new String(WireHelper.class.getResourceAsStream(jsonClasspathName).readAllBytes(), - StandardCharsets.UTF_8); - - return gson.fromJson(json, type); - } - public T deSerializeFromClasspathResource(final String jsonClasspathName, final Type type) throws IOException { final String json = new String(WireHelper.class.getResourceAsStream(jsonClasspathName).readAllBytes(), StandardCharsets.UTF_8); return deSerializeFromString(json, type); } - public T deSerializeFromString(final String json, final Type type) throws IOException { + public T deSerializeFromString(final String json, final Type type) { return gson.fromJson(json, type); } - public String serialize(final AbstractRequest req) throws IOException { + public String serialize(final AbstractRequest req) { return gson.toJson(req); } } diff --git a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustAccountHandlerTest.java b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustAccountHandlerTest.java index 8d9a292bcc17f..d55156536f6c0 100644 --- a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustAccountHandlerTest.java +++ b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustAccountHandlerTest.java @@ -19,15 +19,13 @@ import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -import static org.openhab.binding.august.internal.ApiBridge.HEADER_ACCESS_TOKEN; +import static org.openhab.binding.august.internal.comm.RestApiClient.HEADER_ACCESS_TOKEN; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.util.List; import java.util.Map; import org.eclipse.jetty.client.HttpClient; @@ -38,8 +36,9 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.openhab.binding.august.internal.ApiBridge; import org.openhab.binding.august.internal.AuthenticationStatus; +import org.openhab.binding.august.internal.GsonFactory; +import org.openhab.binding.august.internal.comm.RestApiClient; import org.openhab.binding.august.internal.config.AccountConfiguration; import org.openhab.binding.august.internal.model.Lock; import org.openhab.core.config.core.Configuration; @@ -51,13 +50,14 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.google.gson.Gson; /** * * @author Arne Seime - Initial contribution */ @ExtendWith(MockitoExtension.class) -public class AugustAccountHandlerTest { +class AugustAccountHandlerTest { private WireMockServer wireMockServer; @@ -68,7 +68,9 @@ public class AugustAccountHandlerTest { private Storage storage; - private ApiBridge apiBridge; + private RestApiClient restApiClient; + + private Gson gson = GsonFactory.create(); @BeforeEach public void setUp() throws Exception { @@ -77,12 +79,12 @@ public void setUp() throws Exception { int port = wireMockServer.port(); WireMock.configureFor("localhost", port); - ApiBridge.API_ENDPOINT = "http://localhost:" + port; + RestApiClient.API_ENDPOINT = "http://localhost:" + port; httpClient = new HttpClient(); httpClient.start(); - apiBridge = new ApiBridge(httpClient); + restApiClient = new RestApiClient(httpClient, gson); storage = new VolatileStorage<>(); } @@ -93,36 +95,37 @@ public void shutdown() throws Exception { } @Test - public void testInitial2FactorLogin() throws IOException { + void testInitial2FactorLogin() throws IOException { // Setup account final AccountConfiguration accountConfig = new AccountConfiguration(); accountConfig.email = "email@address.com"; accountConfig.phone = "+4700000000"; accountConfig.password = "password"; - when(configuration.as(eq(AccountConfiguration.class))).thenReturn(accountConfig); + when(configuration.as(AccountConfiguration.class)).thenReturn(accountConfig); // Setup get session response - preparePostNetworkResponse("/session", "/get_session_response.json", 200); + preparePostNetworkResponse("/session", "/mock_responses/get_session_response.json", 200); // Setup get 2 factor code response - preparePostNetworkResponse("/validation/email", "/get_validation_code_response.json", 200); + preparePostNetworkResponse("/validation/email", "/mock_responses/get_validation_code_response.json", 200); when(bridge.getConfiguration()).thenReturn(configuration); when(bridge.getUID()).thenReturn(new ThingUID("august:account:thinguid")); - when(bridge.getThings()).thenReturn(List.of()); - AugustAccountHandler accountHandler = Mockito.spy(new AugustAccountHandler(bridge, apiBridge, storage)); + AugustAccountHandler accountHandler = Mockito.spy(new AugustAccountHandler(bridge, restApiClient, storage)); + // First init accountHandler.initialize(); assertAuthState(AuthenticationStatus.VALIDATION_REQUESTED); // Setup validate 2 factor code response - preparePostNetworkResponse("/validate/email", "/validate_code_response.json", 200); + preparePostNetworkResponse("/validate/email", "/mock_responses/validate_code_response.json", 200); // Setup get locks response - prepareGetNetworkResponse("/users/locks/mine", "/get_locks_response.json", 200); + prepareGetNetworkResponse("/users/locks/mine", "/mock_responses/get_locks_response.json", 200); // Second init / TODO must check what kind of event is sent when config is updated accountConfig.validationCode = "000000"; + // After code has been provided accountHandler.initialize(); assertAuthState(AuthenticationStatus.VALIDATED); @@ -132,26 +135,27 @@ public void testInitial2FactorLogin() throws IOException { } @Test - public void testAlreadyLoggedInValidToken() throws IOException { + void testAlreadyLoggedInValidToken() throws IOException { // Setup account final AccountConfiguration accountConfig = new AccountConfiguration(); accountConfig.email = "email@address.com"; accountConfig.phone = "+4700000000"; accountConfig.password = "password"; - when(configuration.as(eq(AccountConfiguration.class))).thenReturn(accountConfig); + when(configuration.as(AccountConfiguration.class)).thenReturn(accountConfig); storage.put(AugustAccountHandler.STORAGE_KEY_AUTH_STATUS, AuthenticationStatus.VALIDATED.toString()); storage.put(AugustAccountHandler.STORAGE_KEY_INSTALLID, "InstallID"); storage.put(AugustAccountHandler.STORAGE_KEY_ACCESS_TOKEN, "ACCESSTOKEN"); storage.put(AugustAccountHandler.STORAGE_KEY_ACCESS_TOKEN_EXPIRY, ZonedDateTime.now().plus(1, ChronoUnit.MONTHS).toString()); + // storage.put(AugustAccountHandler.STORAGE_KEY_USERID, "UserId"); - prepareGetNetworkResponse("/users/locks/mine", "/get_locks_response.json", 200); + preparePostNetworkResponse("/session", "/mock_responses/get_session_response.json", 200); + prepareGetNetworkResponse("/users/locks/mine", "/mock_responses/get_locks_response.json", 200); when(bridge.getConfiguration()).thenReturn(configuration); when(bridge.getUID()).thenReturn(new ThingUID("august:account:thinguid")); - when(bridge.getThings()).thenReturn(List.of()); - AugustAccountHandler accountHandler = new AugustAccountHandler(bridge, apiBridge, storage); + AugustAccountHandler accountHandler = new AugustAccountHandler(bridge, restApiClient, storage); accountHandler.initialize(); @@ -160,27 +164,27 @@ public void testAlreadyLoggedInValidToken() throws IOException { } @Test - public void testAlreadyLoggedInExpiredToken() throws IOException { + void testAlreadyLoggedInExpiredToken() throws IOException { // Setup account final AccountConfiguration accountConfig = new AccountConfiguration(); accountConfig.email = "email@address.com"; accountConfig.phone = "+4700000000"; accountConfig.password = "password"; - when(configuration.as(eq(AccountConfiguration.class))).thenReturn(accountConfig); + when(configuration.as(AccountConfiguration.class)).thenReturn(accountConfig); storage.put(AugustAccountHandler.STORAGE_KEY_AUTH_STATUS, AuthenticationStatus.VALIDATED.toString()); storage.put(AugustAccountHandler.STORAGE_KEY_INSTALLID, "InstallID"); storage.put(AugustAccountHandler.STORAGE_KEY_ACCESS_TOKEN, "ACCESSTOKEN"); storage.put(AugustAccountHandler.STORAGE_KEY_ACCESS_TOKEN_EXPIRY, ZonedDateTime.now().minus(1, ChronoUnit.MONTHS).toString()); + // storage.put(AugustAccountHandler.STORAGE_KEY_USERID, "UserId"); - preparePostNetworkResponse("/session", "/get_session_response.json", 200); - prepareGetNetworkResponse("/users/locks/mine", "/get_locks_response.json", 200); + preparePostNetworkResponse("/session", "/mock_responses/get_session_response.json", 200); + prepareGetNetworkResponse("/users/locks/mine", "/mock_responses/get_locks_response.json", 200); when(bridge.getConfiguration()).thenReturn(configuration); when(bridge.getUID()).thenReturn(new ThingUID("august:account:thinguid")); - when(bridge.getThings()).thenReturn(List.of()); - AugustAccountHandler accountHandler = new AugustAccountHandler(bridge, apiBridge, storage); + AugustAccountHandler accountHandler = new AugustAccountHandler(bridge, restApiClient, storage); accountHandler.initialize(); diff --git a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustLockHandlerTest.java b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustLockHandlerTest.java index 227be7b0fc2d8..bec4c72ec0a89 100644 --- a/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustLockHandlerTest.java +++ b/bundles/org.openhab.binding.august/src/test/java/org/openhab/binding/august/internal/handler/AugustLockHandlerTest.java @@ -20,7 +20,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.openhab.binding.august.internal.ApiBridge.HEADER_ACCESS_TOKEN; +import static org.openhab.binding.august.internal.comm.RestApiClient.HEADER_ACCESS_TOKEN; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -33,8 +33,11 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; -import org.openhab.binding.august.internal.ApiBridge; import org.openhab.binding.august.internal.BindingConstants; +import org.openhab.binding.august.internal.GsonFactory; +import org.openhab.binding.august.internal.comm.PubNubListener; +import org.openhab.binding.august.internal.comm.PubNubMessageSubscriber; +import org.openhab.binding.august.internal.comm.RestApiClient; import org.openhab.binding.august.internal.config.LockConfiguration; import org.openhab.binding.august.internal.dto.RemoteOperateLockRequest; import org.openhab.core.config.core.Configuration; @@ -42,10 +45,10 @@ import org.openhab.core.library.types.OpenClosedType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; -import org.openhab.core.storage.Storage; import org.openhab.core.test.storage.VolatileStorage; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandlerCallback; @@ -55,6 +58,9 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; /** * @@ -62,7 +68,7 @@ */ @ExtendWith(MockitoExtension.class) -public class AugustLockHandlerTest { +class AugustLockHandlerTest implements PubNubListener { private WireMockServer wireMockServer; @@ -71,9 +77,21 @@ public class AugustLockHandlerTest { private @Mock Configuration configuration; private @Mock Bridge bridge; - private Storage storage; + private @Mock AugustAccountHandler accountHandler; - private ApiBridge apiBridge; + private RestApiClient restApiClient; + private PubNubMessageSubscriber messageSubscriber; + private VolatileStorage storage; + + private Gson gson = GsonFactory.create(); + + private Thing thing; + + private AugustLockHandler lockHandler; + + private ThingHandlerCallback thingHandlerCallback; + + LockConfiguration lockConfiguration; @BeforeEach public void setUp() throws Exception { @@ -82,94 +100,101 @@ public void setUp() throws Exception { int port = wireMockServer.port(); WireMock.configureFor("localhost", port); - ApiBridge.API_ENDPOINT = "http://localhost:" + port; + RestApiClient.API_ENDPOINT = "http://localhost:" + port; httpClient = new HttpClient(); httpClient.start(); - apiBridge = new ApiBridge(httpClient); - apiBridge.init(new ThingUID("august:bridge:1"), updatedAccessToken -> { + restApiClient = new RestApiClient(httpClient, gson); + restApiClient.init(new ThingUID("august:bridge:1"), updatedAccessToken -> { }); + restApiClient.setAccessToken("ACCESSTOKEN"); + + messageSubscriber = new PubNubMessageSubscriber(); + messageSubscriber.init("null", this); + + lockConfiguration = new LockConfiguration(); + lockConfiguration.lockId = "LockId1"; + when(configuration.as(LockConfiguration.class)).thenReturn(lockConfiguration); + + thing = createLockThing(); + lockHandler = Mockito.spy(new AugustLockHandler(thing, gson)); + thingHandlerCallback = Mockito.mock(ThingHandlerCallback.class); + lockHandler.setCallback(thingHandlerCallback); + + when(bridge.getStatus()).thenReturn(ThingStatus.ONLINE); + when(lockHandler.getBridge()).thenReturn(bridge); + when(bridge.getHandler()).thenReturn(accountHandler); + when(accountHandler.getApiBridge()).thenReturn(restApiClient); + storage = new VolatileStorage<>(); } @AfterEach public void shutdown() throws Exception { httpClient.stop(); + lockHandler.dispose(); + verify(accountHandler).deregisterForEvents(eq(lockHandler)); } @Test - public void testInitialize() throws IOException { - // Setup account - final LockConfiguration lockConfiguration = new LockConfiguration(); - lockConfiguration.lockId = "LockId1"; - when(configuration.as(eq(LockConfiguration.class))).thenReturn(lockConfiguration); - - ThingImpl lockThing = createLockThing(); - - apiBridge.setAccessToken("ACCESSTOKEN"); + void testInitialize() throws IOException, InterruptedException { // Setup get lock response - prepareGetNetworkResponse("/locks/" + lockConfiguration.lockId, "/get_lock_response.json", 200); - - AugustLockHandler lockHandler = Mockito.spy(new AugustLockHandler(lockThing, apiBridge)); - ThingHandlerCallback thingHandlerCallback = Mockito.mock(ThingHandlerCallback.class); - lockHandler.setCallback(thingHandlerCallback); + prepareGetNetworkResponse("/locks/" + lockConfiguration.lockId, "/mock_responses/get_lock_response.json", 200); - when(bridge.getStatus()).thenReturn(ThingStatus.ONLINE); - when(lockHandler.getBridge()).thenReturn(bridge); lockHandler.initialize(); - lockHandler.doPoll(); - verify(thingHandlerCallback).stateUpdated(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_BATTERY), + Thread.sleep(2000); + + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_BATTERY), new QuantityType<>(47.75072124321014, Units.PERCENT)); - verify(thingHandlerCallback) - .stateUpdated(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), OnOffType.ON); - verify(thingHandlerCallback).stateUpdated( - new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_DOOR_STATE), OpenClosedType.CLOSED); + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), + OnOffType.ON); + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_DOOR_STATE), + OpenClosedType.CLOSED); + + lockHandler.onConnect("ignored"); + + verify(accountHandler).registerForEvents(eq(lockHandler), eq("PubsubChannelUUID")); } @Test - public void testUnlockDoor() throws IOException { + void testUnlockDoor() throws IOException, InterruptedException { // Setup account - final LockConfiguration lockConfiguration = new LockConfiguration(); - lockConfiguration.lockId = "LockId1"; - when(configuration.as(eq(LockConfiguration.class))).thenReturn(lockConfiguration); - - ThingImpl lockThing = createLockThing(); - - apiBridge.setAccessToken("ACCESSTOKEN"); // Setup get lock response - prepareGetNetworkResponse("/locks/" + lockConfiguration.lockId, "/get_lock_response.json", 200); + prepareGetNetworkResponse("/locks/" + lockConfiguration.lockId, "/mock_responses/get_lock_response.json", 200); - AugustLockHandler lockHandler = Mockito.spy(new AugustLockHandler(lockThing, apiBridge)); - ThingHandlerCallback thingHandlerCallback = Mockito.mock(ThingHandlerCallback.class); - lockHandler.setCallback(thingHandlerCallback); - - when(bridge.getStatus()).thenReturn(ThingStatus.ONLINE); - when(lockHandler.getBridge()).thenReturn(bridge); lockHandler.initialize(); - lockHandler.doPoll(); - verify(thingHandlerCallback).stateUpdated(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_BATTERY), + Thread.sleep(2000); + + // await().until(() -> lockHandler.getThing().getStatus() == ThingStatus.ONLINE); + + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_BATTERY), new QuantityType<>(47.75072124321014, Units.PERCENT)); - verify(thingHandlerCallback) - .stateUpdated(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), OnOffType.ON); - verify(thingHandlerCallback).stateUpdated( - new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_DOOR_STATE), OpenClosedType.CLOSED); + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), + OnOffType.ON); + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_DOOR_STATE), + OpenClosedType.CLOSED); + + lockHandler.onConnect("ignored"); + + verify(accountHandler).registerForEvents(eq(lockHandler), eq("PubsubChannelUUID")); preparePutNetworkResponse( String.format("/remoteoperate/%s/%s", lockConfiguration.lockId, RemoteOperateLockRequest.Operation.UNLOCK.getUrlWord()), - "/remoteoperate_lock_response.json", 200); + "/mock_responses/remoteoperate_lock_response.json", 200); - lockHandler.handleCommand(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), - OnOffType.OFF); + lockHandler.handleCommand(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), OnOffType.OFF); - verify(thingHandlerCallback) - .stateUpdated(new ChannelUID(lockThing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), OnOffType.OFF); + lockHandler.onPushMessage("ignored", + JsonParser.parseString(getClasspathJSONContent("/mock_responses/lock_status_async.json"))); + verify(thingHandlerCallback).stateUpdated(new ChannelUID(thing.getUID(), BindingConstants.CHANNEL_LOCK_STATE), + OnOffType.OFF); } private ThingImpl createLockThing() { @@ -199,4 +224,16 @@ private void prepareGetNetworkResponse(String urlPath, String responseResource, private String getClasspathJSONContent(String path) throws IOException { return new String(getClass().getResourceAsStream(path).readAllBytes(), StandardCharsets.UTF_8); } + + @Override + public void onPushMessage(String channelName, JsonElement message) { + } + + @Override + public void onDisconnect(String channelName) { + } + + @Override + public void onConnect(String channelName) { + } } diff --git a/bundles/org.openhab.binding.august/src/test/resources/logback-test.xml b/bundles/org.openhab.binding.august/src/test/resources/logback-test.xml new file mode 100644 index 0000000000000..f74aa3fe4033d --- /dev/null +++ b/bundles/org.openhab.binding.august/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.august/src/test/resources/mock_responses/event_unlock.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/event_unlock.json new file mode 100644 index 0000000000000..4e25c25df6df5 --- /dev/null +++ b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/event_unlock.json @@ -0,0 +1,21 @@ +{ + "remoteEvent": 1, + "status": "kAugLockState_Unlocking", + "info": { + "action": "unlock", + "startTime": "2023-02-05T08:15:10.015Z", + "context": { + "transactionID": "TxID", + "startDate": "2023-02-05T08:15:10.010Z", + "retryCount": 1 + }, + "lockType": "lock_version_7", + "serialNumber": "Serial", + "rssi": -76, + "wlanRSSI": -48, + "wlanSNR": 50, + "duration": 1385, + "lockID": "LockID", + "bridgeID": "bridgeID" + } +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_lock_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_lock_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_lock_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_lock_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_lock_status_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_lock_status_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_lock_status_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_lock_status_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_locks_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_locks_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_locks_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_locks_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_session_request.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_session_request.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_session_request.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_session_request.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_session_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_session_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_session_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_session_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_validation_code_request.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_validation_code_request.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_validation_code_request.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_validation_code_request.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/get_validation_code_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_validation_code_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/get_validation_code_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/get_validation_code_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/mock_responses/lock_status_async.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/lock_status_async.json new file mode 100644 index 0000000000000..52f009debb2ce --- /dev/null +++ b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/lock_status_async.json @@ -0,0 +1,5 @@ +{ + "status": "unlocked", + "callingUserID": "manuallock", + "doorState": "closed" +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.august/src/test/resources/remoteoperate_lock_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/remoteoperate_lock_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/remoteoperate_lock_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/remoteoperate_lock_response.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/validate_code_request.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/validate_code_request.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/validate_code_request.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/validate_code_request.json diff --git a/bundles/org.openhab.binding.august/src/test/resources/validate_code_response.json b/bundles/org.openhab.binding.august/src/test/resources/mock_responses/validate_code_response.json similarity index 100% rename from bundles/org.openhab.binding.august/src/test/resources/validate_code_response.json rename to bundles/org.openhab.binding.august/src/test/resources/mock_responses/validate_code_response.json