]> git.basschouten.com Git - openhab-addons.git/commitdiff
[OJElectronics] Add SignalR for requesting data from OJ Electronics cloud (#13782)
authorChristian Kittel <EvilPingu@users.noreply.github.com>
Sat, 8 Apr 2023 09:40:59 +0000 (11:40 +0200)
committerGitHub <noreply@github.com>
Sat, 8 Apr 2023 09:40:59 +0000 (11:40 +0200)
* Fixed some Nullable annotation warnings
* Use SignalR instead pooling

Signed-off-by: Christian Kittel <ckittel@gmx.de>
32 files changed:
bundles/org.openhab.binding.ojelectronics/NOTICE
bundles/org.openhab.binding.ojelectronics/README.md
bundles/org.openhab.binding.ojelectronics/pom.xml
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/OJCloudHandler.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/OJCloudHandlerFactory.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/ThermostatHandler.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/ThermostatHandlerFactory.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/common/SignalRLogger.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/config/OJElectronicsBridgeConfiguration.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/SignalRResultModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/Thermostat.java [deleted file]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Day.java [deleted file]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/DayModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Event.java [deleted file]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/EventModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContent.java [deleted file]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContentModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContentResponseModel.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Schedule.java [deleted file]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/ScheduleModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModelBase.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatRealTimeValuesModel.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/UpdateThermostatRequestModel.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/OJDiscoveryService.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/RefreshGroupContentService.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/RefreshService.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/RefreshThermostatsService.java [new file with mode: 0644]
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/SignInService.java
bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/UpdateService.java
bundles/org.openhab.binding.ojelectronics/src/main/resources/OH-INF/i18n/ojelectronics.properties
bundles/org.openhab.binding.ojelectronics/src/main/resources/OH-INF/thing/thing-types.xml

index 38d625e349232ff5ffcc71bd75e4692cdac12768..fdb4ac8cc8be71597c0bbd4c1f54aaf1eaf92688 100644 (file)
@@ -11,3 +11,8 @@ https://www.eclipse.org/legal/epl-2.0/.
 == Source Code
 
 https://github.com/openhab/openhab-addons
+
+Signalr4j
+* License: Apache 2.0 License
+* Project: https://github.com/sputnikdev/bluetooth-gatt-parser
+* Source:  https://github.com/sputnikdev/bluetooth-gatt-parser
index b14b6bb6028be09f2448f160641ead06a4c8c10e..b7c4fd42c94ffa21a1fb9c7a96e0e742d1e360b9 100644 (file)
@@ -23,7 +23,6 @@ After the ojcloud bridge is successfully initialized all thermostats will be dis
 | password              | password from the OJElectronics App (required)                           |
 | apiKey                | API key. You get the key from your local distributor.                    |
 | apiUrl                | URL of the API endpoint. Optional, the default value should always work. |
-| refreshDelayInSeconds | Refresh interval in seconds. Optional, the default value is 30 seconds.  |
 | customerId            | Customer ID. Optional, the default value should always work.             |
 | softwareVersion       | Software version. Optional, the default value should always work.        |
 
index 91278e06e26053fbc30f08c2beded3cf1f201977..347db883432f5dff385124c41706e44d5f9a1c5f 100644 (file)
   <artifactId>org.openhab.binding.ojelectronics</artifactId>
 
   <name>openHAB Add-ons :: Bundles :: OJElectronics Binding</name>
+
+  <dependencies>
+    <!-- https://mvnrepository.com/artifact/com.github.signalr4j/signalr4j -->
+    <dependency>
+      <groupId>com.github.signalr4j</groupId>
+      <artifactId>signalr4j</artifactId>
+      <version>2.0.4</version>
+      <scope>compile</scope>
+    </dependency>
+    <!-- https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket -->
+    <dependency>
+      <groupId>org.java-websocket</groupId>
+      <artifactId>Java-WebSocket</artifactId>
+      <version>1.5.3</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
 </project>
index 5d4532a958c517c841cc5d8b37c653a7513fb473..7f7798e7b234730f9b3059906a494cae9739fb34 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.ojelectronics.internal;
 
 import java.util.Collection;
-import java.util.Collections;
+import java.util.Set;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
@@ -21,10 +21,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.ojelectronics.internal.config.OJElectronicsBridgeConfiguration;
+import org.openhab.binding.ojelectronics.internal.models.SignalRResultModel;
 import org.openhab.binding.ojelectronics.internal.models.groups.GroupContentResponseModel;
 import org.openhab.binding.ojelectronics.internal.services.OJDiscoveryService;
 import org.openhab.binding.ojelectronics.internal.services.RefreshGroupContentService;
 import org.openhab.binding.ojelectronics.internal.services.RefreshService;
+import org.openhab.binding.ojelectronics.internal.services.RefreshThermostatsService;
 import org.openhab.binding.ojelectronics.internal.services.SignInService;
 import org.openhab.binding.ojelectronics.internal.services.UpdateService;
 import org.openhab.core.thing.Bridge;
@@ -32,7 +34,6 @@ import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.binding.BaseBridgeHandler;
-import org.openhab.core.thing.binding.BridgeHandler;
 import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
 import org.slf4j.Logger;
@@ -44,7 +45,7 @@ import org.slf4j.LoggerFactory;
  * @author Christian Kittel - Initial Contribution
  */
 @NonNullByDefault
-public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
+public class OJCloudHandler extends BaseBridgeHandler {
 
     private final Logger logger = LoggerFactory.getLogger(OJCloudHandler.class);
     private final HttpClient httpClient;
@@ -54,6 +55,7 @@ public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
     private @Nullable SignInService signInService;
     private OJElectronicsBridgeConfiguration configuration;
     private @Nullable ScheduledFuture<?> signTask;
+    private @Nullable ScheduledFuture<?> updateTask;
     private @Nullable OJDiscoveryService discoveryService;
 
     /**
@@ -82,9 +84,9 @@ public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
      */
     @Override
     public void dispose() {
-        final RefreshService refreshService = this.refreshService;
-        if (refreshService != null) {
-            refreshService.stop();
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.stop();
         }
         final ScheduledFuture<?> signTask = this.signTask;
         if (signTask != null) {
@@ -99,64 +101,90 @@ public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
     public void handleCommand(ChannelUID channelUID, Command command) {
     }
 
+    public synchronized void updateThinksChannelValuesToCloud() {
+        final UpdateService localUpdateService = this.updateService;
+        if (localUpdateService != null) {
+            final ScheduledFuture<?> localUpdateTask = this.updateTask;
+            if (localUpdateTask != null) {
+                localUpdateTask.cancel(false);
+            }
+            this.updateTask = scheduler.schedule(() -> {
+                localUpdateService.updateAllThermostats(getThing().getThings());
+                this.updateTask = null;
+            }, 2, TimeUnit.SECONDS);
+        }
+    }
+
     private void ensureSignIn() {
         if (signInService == null) {
             signInService = new SignInService(configuration, httpClient);
         }
-        final SignInService signInService = this.signInService;
-        if (signInService != null) {
-            signInService.signIn(this::handleSignInDone, this::handleConnectionLost,
+        final SignInService localSignInService = this.signInService;
+        if (localSignInService != null) {
+            localSignInService.signIn(this::handleSignInDone, this::handleConnectionLost,
                     this::handleUnauthorizedWhileSignIn);
         }
     }
 
-    private void handleRefreshDone(@Nullable GroupContentResponseModel groupContentResponse,
+    private void initializationDone(@Nullable GroupContentResponseModel groupContentResponse,
             @Nullable String errorMessage) {
-        logger.trace("OJElectronicsCloudHandler.handleRefreshDone({})", groupContentResponse);
+        logger.trace("OJElectronicsCloudHandler.initializationDone({})", groupContentResponse);
         if (groupContentResponse != null && groupContentResponse.errorCode == 0) {
-            internalRefreshDone(groupContentResponse);
+            internalInitializationDone(groupContentResponse);
         } else {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     (errorMessage == null) ? "Wrong or no result model; Refreshing stoppped" : errorMessage);
-            final RefreshService refreshService = this.refreshService;
-            if (refreshService != null) {
-                refreshService.stop();
+            final RefreshService localRefreshService = this.refreshService;
+            if (localRefreshService != null) {
+                localRefreshService.stop();
             }
         }
     }
 
-    private void internalRefreshDone(GroupContentResponseModel groupContentResponse) {
-        new RefreshGroupContentService(groupContentResponse.groupContents, getThing().getThings()).handle();
-        final OJDiscoveryService discoveryService = this.discoveryService;
-        if (discoveryService != null) {
-            discoveryService.setScanResultForDiscovery(groupContentResponse.groupContents);
+    private void refreshDone(@Nullable SignalRResultModel resultModel, @Nullable String errorMessage) {
+        logger.trace("OJElectronicsCloudHandler.refreshDone({})", resultModel);
+        if (resultModel != null) {
+            new RefreshThermostatsService(resultModel.getThermostats(), resultModel.getThermostatRealTimes(),
+                    getThing().getThings()).handle();
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    (errorMessage == null) ? "Wrong or no result model; Refreshing stoppped" : errorMessage);
+            final RefreshService localRefreshService = this.refreshService;
+            if (localRefreshService != null) {
+                localRefreshService.stop();
+            }
         }
-        final UpdateService updateService = this.updateService;
-        if (updateService != null) {
-            updateService.updateAllThermostats(getThing().getThings());
+    }
+
+    private void internalInitializationDone(GroupContentResponseModel groupContentResponse) {
+        new RefreshGroupContentService(groupContentResponse.groupContents, getThing().getThings()).handle();
+        final OJDiscoveryService localDiscoveryService = this.discoveryService;
+        if (localDiscoveryService != null) {
+            localDiscoveryService.setScanResultForDiscovery(groupContentResponse.groupContents);
         }
     }
 
     private void handleSignInDone(String sessionId) {
         logger.trace("OJElectronicsCloudHandler.handleSignInDone({})", sessionId);
         if (refreshService == null) {
-            refreshService = new RefreshService(configuration, httpClient, scheduler);
+            refreshService = new RefreshService(configuration, httpClient);
         }
-        final RefreshService refreshService = this.refreshService;
-        if (refreshService != null) {
-            refreshService.start(sessionId, this::handleRefreshDone, this::handleConnectionLost,
-                    this::handleUnauthorized);
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.start(sessionId, this::initializationDone, this::refreshDone,
+                    this::handleConnectionLost, this::handleUnauthorized);
 
             updateStatus(ThingStatus.ONLINE);
         }
-        this.updateService = new UpdateService(configuration, httpClient, sessionId);
+        this.updateService = new UpdateService(configuration, httpClient, this::handleConnectionLost,
+                this::handleUnauthorized);
     }
 
     private void handleUnauthorized() {
         logger.trace("OJElectronicsCloudHandler.handleUnauthorized()");
-        final RefreshService refreshService = this.refreshService;
-        if (refreshService != null) {
-            refreshService.stop();
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.stop();
         }
         restartRefreshServiceAsync(1);
     }
@@ -165,20 +193,29 @@ public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
         logger.trace("OJElectronicsCloudHandler.handleUnauthorizedWhileSignIn()");
         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                 "Could not sign in. Check user name and password.");
-        final RefreshService refreshService = this.refreshService;
-        if (refreshService != null) {
-            refreshService.stop();
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.stop();
         }
     }
 
-    private void handleConnectionLost() {
+    public void reInitialize() {
+        logger.trace("OJElectronicsCloudHandler.reInitialize()");
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.stop();
+        }
+        restartRefreshServiceAsync(1);
+    }
+
+    private void handleConnectionLost(@Nullable String message) {
         logger.trace("OJElectronicsCloudHandler.handleConnectionLost()");
-        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
-        final RefreshService refreshService = this.refreshService;
-        if (refreshService != null) {
-            refreshService.stop();
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+        final RefreshService localRefreshService = this.refreshService;
+        if (localRefreshService != null) {
+            localRefreshService.stop();
         }
-        restartRefreshServiceAsync(configuration.refreshDelayInSeconds);
+        restartRefreshServiceAsync(30);
     }
 
     private void restartRefreshServiceAsync(long delayInSeconds) {
@@ -191,6 +228,6 @@ public class OJCloudHandler extends BaseBridgeHandler implements BridgeHandler {
 
     @Override
     public Collection<Class<? extends ThingHandlerService>> getServices() {
-        return Collections.singleton(OJDiscoveryService.class);
+        return Set.of(OJDiscoveryService.class);
     }
 }
index 04b92d0f825d9c04a17c7f196ff8c365559acc58..718494f0fcc81e8551a95f8ac04579ef23d3e78f 100644 (file)
@@ -14,7 +14,6 @@ package org.openhab.binding.ojelectronics.internal;
 
 import static org.openhab.binding.ojelectronics.internal.BindingConstants.THING_TYPE_OJCLOUD;
 
-import java.util.Collections;
 import java.util.Set;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -40,7 +39,7 @@ import org.osgi.service.component.annotations.Reference;
 @Component(configurationPid = "binding.ojelectronics", service = ThingHandlerFactory.class)
 public class OJCloudHandlerFactory extends BaseThingHandlerFactory {
 
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_OJCLOUD);
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OJCLOUD);
 
     private final HttpClient httpClient;
 
index 5bc823e2a1398ef4969ef8030158ef122a64cf75..2bcfec98fb01f88f66aa1a9ac1be532edc05a153 100644 (file)
@@ -20,6 +20,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.Map;
+import java.util.Objects;
 import java.util.function.Consumer;
 
 import javax.measure.quantity.Temperature;
@@ -27,7 +28,8 @@ import javax.measure.quantity.Temperature;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.ojelectronics.internal.config.OJElectronicsThermostatConfiguration;
-import org.openhab.binding.ojelectronics.internal.models.Thermostat;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatRealTimeValuesModel;
 import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
@@ -35,10 +37,12 @@ import org.openhab.core.library.types.OpenClosedType;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.library.unit.SIUnits;
+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.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
 import org.slf4j.Logger;
@@ -58,12 +62,13 @@ public class ThermostatHandler extends BaseThingHandler {
 
     private final String serialNumber;
     private final Logger logger = LoggerFactory.getLogger(ThermostatHandler.class);
-    private final Map<String, Consumer<Thermostat>> channelrefreshActions = createChannelRefreshActionMap();
+    private final Map<String, Consumer<ThermostatModel>> channelRefreshActions = createChannelRefreshActionMap();
+    private final Map<String, Consumer<ThermostatRealTimeValuesModel>> channelRealTimeRefreshActions = createRealTimeChannelRefreshActionMap();
     private final Map<String, Consumer<Command>> updateThermostatValueActions = createUpdateThermostatValueActionMap();
     private final TimeZoneProvider timeZoneProvider;
 
     private LinkedList<AbstractMap.SimpleImmutableEntry<String, Command>> updatedValues = new LinkedList<>();
-    private @Nullable Thermostat currentThermostat;
+    private @Nullable ThermostatModel currentThermostat;
 
     /**
      * Creates a new instance of {@link ThermostatHandler}
@@ -92,9 +97,9 @@ public class ThermostatHandler extends BaseThingHandler {
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         if (command instanceof RefreshType) {
-            final Thermostat thermostat = currentThermostat;
-            if (thermostat != null && channelrefreshActions.containsKey(channelUID.getId())) {
-                final @Nullable Consumer<Thermostat> consumer = channelrefreshActions.get(channelUID.getId());
+            final ThermostatModel thermostat = currentThermostat;
+            if (thermostat != null && channelRefreshActions.containsKey(channelUID.getId())) {
+                final @Nullable Consumer<ThermostatModel> consumer = channelRefreshActions.get(channelUID.getId());
                 if (consumer != null) {
                     consumer.accept(thermostat);
                 }
@@ -102,6 +107,14 @@ public class ThermostatHandler extends BaseThingHandler {
         } else {
             synchronized (this) {
                 updatedValues.add(new AbstractMap.SimpleImmutableEntry<String, Command>(channelUID.getId(), command));
+
+                BridgeHandler bridgeHandler = Objects.requireNonNull(getBridge()).getHandler();
+                if (bridgeHandler != null) {
+                    ((OJCloudHandler) (bridgeHandler)).updateThinksChannelValuesToCloud();
+                } else {
+                    currentThermostat = null;
+                    updateStatus(ThingStatus.OFFLINE);
+                }
             }
         }
     }
@@ -111,7 +124,15 @@ public class ThermostatHandler extends BaseThingHandler {
      */
     @Override
     public void initialize() {
-        updateStatus(ThingStatus.ONLINE);
+        @Nullable
+        Bridge bridge = getBridge();
+        if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE && currentThermostat == null) {
+            @Nullable
+            OJCloudHandler bridgeHandler = (OJCloudHandler) (bridge.getHandler());
+            if (bridgeHandler != null) {
+                bridgeHandler.reInitialize();
+            }
+        }
     }
 
     /**
@@ -119,17 +140,37 @@ public class ThermostatHandler extends BaseThingHandler {
      *
      * @param thermostat thermostat values
      */
-    public void handleThermostatRefresh(Thermostat thermostat) {
+    public void handleThermostatRefresh(ThermostatModel thermostat) {
+        if (currentThermostat == null) {
+            updateStatus(ThingStatus.ONLINE);
+        }
         currentThermostat = thermostat;
-        channelrefreshActions.forEach((channelUID, action) -> action.accept(thermostat));
+        channelRefreshActions.forEach((channelUID, action) -> action.accept(thermostat));
+    }
+
+    /**
+     * Sets the values after refreshing the thermostats values
+     *
+     * @param thermostat thermostat values
+     */
+    public void handleThermostatRefresh(ThermostatRealTimeValuesModel thermostat) {
+        final ThermostatModel currentThermostat = this.currentThermostat;
+        if (currentThermostat != null) {
+            currentThermostat.heating = thermostat.heating;
+            currentThermostat.floorTemperature = thermostat.floorTemperature;
+            currentThermostat.action = thermostat.action;
+            currentThermostat.online = thermostat.online;
+            currentThermostat.roomTemperature = thermostat.roomTemperature;
+            channelRealTimeRefreshActions.forEach((channelUID, action) -> action.accept(thermostat));
+        }
     }
 
     /**
-     * Gets a {@link Thermostat} with changed values or null if nothing has changed
+     * Gets a {@link ThermostatModel} with changed values or null if nothing has changed
      *
-     * @return The changed {@link Thermostat}
+     * @return The changed {@link ThermostatModel}
      */
-    public @Nullable Thermostat tryHandleAndGetUpdatedThermostat() {
+    public @Nullable ThermostatModel tryHandleAndGetUpdatedThermostat() {
         final LinkedList<SimpleImmutableEntry<String, Command>> updatedValues = this.updatedValues;
         if (updatedValues.isEmpty()) {
             return null;
@@ -146,59 +187,64 @@ public class ThermostatHandler extends BaseThingHandler {
         return currentThermostat;
     }
 
-    private void updateManualSetpoint(Thermostat thermostat) {
+    private ThermostatModel getCurrentThermostat() {
+        return Objects.requireNonNull(currentThermostat);
+    }
+
+    private void updateManualSetpoint(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_MANUALSETPOINT,
                 new QuantityType<Temperature>(thermostat.manualModeSetpoint / (double) 100, SIUnits.CELSIUS));
     }
 
     private void updateManualSetpoint(Command command) {
         if (command instanceof QuantityType<?>) {
-            currentThermostat.manualModeSetpoint = (int) (((QuantityType<?>) command).floatValue() * 100);
+            getCurrentThermostat().manualModeSetpoint = (int) (((QuantityType<?>) command).floatValue() * 100);
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateBoostEndTime(Thermostat thermostat) {
+    private void updateBoostEndTime(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_BOOSTENDTIME, new DateTimeType(
                 ZonedDateTime.ofInstant(thermostat.boostEndTime.toInstant(), timeZoneProvider.getTimeZone())));
     }
 
     private void updateBoostEndTime(Command command) {
         if (command instanceof DateTimeType) {
-            currentThermostat.boostEndTime = Date.from(((DateTimeType) command).getZonedDateTime().toInstant());
+            getCurrentThermostat().boostEndTime = Date.from(((DateTimeType) command).getZonedDateTime().toInstant());
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateComfortEndTime(Thermostat thermostat) {
+    private void updateComfortEndTime(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_COMFORTENDTIME, new DateTimeType(
                 ZonedDateTime.ofInstant(thermostat.comfortEndTime.toInstant(), timeZoneProvider.getTimeZone())));
     }
 
     private void updateComfortEndTime(Command command) {
         if (command instanceof DateTimeType) {
-            currentThermostat.comfortEndTime = Date.from(((DateTimeType) command).getZonedDateTime().toInstant());
+            getCurrentThermostat().comfortEndTime = Objects
+                    .requireNonNull(Date.from(((DateTimeType) command).getZonedDateTime().toInstant()));
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateComfortSetpoint(Thermostat thermostat) {
+    private void updateComfortSetpoint(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_COMFORTSETPOINT,
                 new QuantityType<Temperature>(thermostat.comfortSetpoint / (double) 100, SIUnits.CELSIUS));
     }
 
     private void updateComfortSetpoint(Command command) {
         if (command instanceof QuantityType<?>) {
-            currentThermostat.comfortSetpoint = (int) (((QuantityType<?>) command).floatValue() * 100);
+            getCurrentThermostat().comfortSetpoint = (int) (((QuantityType<?>) command).floatValue() * 100);
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateRegulationMode(Thermostat thermostat) {
+    private void updateRegulationMode(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_REGULATIONMODE,
                 StringType.valueOf(getRegulationMode(thermostat.regulationMode)));
     }
@@ -207,51 +253,71 @@ public class ThermostatHandler extends BaseThingHandler {
         if (command instanceof StringType && (REVERSE_REGULATION_MODES.containsKey(command.toString().toLowerCase()))) {
             final @Nullable Integer mode = REVERSE_REGULATION_MODES.get(command.toString().toLowerCase());
             if (mode != null) {
-                currentThermostat.regulationMode = mode;
+                getCurrentThermostat().regulationMode = mode;
             }
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateThermostatName(Thermostat thermostat) {
+    private void updateThermostatName(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_THERMOSTATNAME, StringType.valueOf(thermostat.thermostatName));
     }
 
-    private void updateFloorTemperature(Thermostat thermostat) {
+    private void updateFloorTemperature(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_FLOORTEMPERATURE,
                 new QuantityType<Temperature>(thermostat.floorTemperature / (double) 100, SIUnits.CELSIUS));
     }
 
-    private void updateRoomTemperature(Thermostat thermostat) {
+    private void updateFloorTemperature(ThermostatRealTimeValuesModel thermostatRealTimeValues) {
+        updateState(BindingConstants.CHANNEL_OWD5_FLOORTEMPERATURE, new QuantityType<Temperature>(
+                thermostatRealTimeValues.floorTemperature / (double) 100, SIUnits.CELSIUS));
+    }
+
+    private void updateRoomTemperature(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_ROOMTEMPERATURE,
                 new QuantityType<Temperature>(thermostat.roomTemperature / (double) 100, SIUnits.CELSIUS));
     }
 
-    private void updateHeating(Thermostat thermostat) {
+    private void updateRoomTemperature(ThermostatRealTimeValuesModel thermostatRealTimeValues) {
+        updateState(BindingConstants.CHANNEL_OWD5_ROOMTEMPERATURE, new QuantityType<Temperature>(
+                thermostatRealTimeValues.roomTemperature / (double) 100, SIUnits.CELSIUS));
+    }
+
+    private void updateHeating(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_HEATING,
                 thermostat.heating ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
     }
 
-    private void updateOnline(Thermostat thermostat) {
+    private void updateHeating(ThermostatRealTimeValuesModel thermostatRealTimeValues) {
+        updateState(BindingConstants.CHANNEL_OWD5_HEATING,
+                thermostatRealTimeValues.heating ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+    }
+
+    private void updateOnline(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_ONLINE,
                 thermostat.online ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
     }
 
-    private void updateGroupId(Thermostat thermostat) {
+    private void updateOnline(ThermostatRealTimeValuesModel thermostatRealTimeValues) {
+        updateState(BindingConstants.CHANNEL_OWD5_ONLINE,
+                thermostatRealTimeValues.online ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
+    }
+
+    private void updateGroupId(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_GROUPID, new DecimalType(thermostat.groupId));
     }
 
-    private void updateGroupName(Thermostat thermostat) {
+    private void updateGroupName(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_GROUPNAME, StringType.valueOf(thermostat.groupName));
     }
 
-    private void updateVacationEnabled(Thermostat thermostat) {
+    private void updateVacationEnabled(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_VACATIONENABLED,
                 thermostat.online ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
     }
 
-    private void updateVacationBeginDay(Thermostat thermostat) {
+    private void updateVacationBeginDay(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_VACATIONBEGINDAY,
                 new DateTimeType(
                         ZonedDateTime.ofInstant(thermostat.vacationBeginDay.toInstant(), timeZoneProvider.getTimeZone())
@@ -260,14 +326,14 @@ public class ThermostatHandler extends BaseThingHandler {
 
     private void updateVacationBeginDay(Command command) {
         if (command instanceof DateTimeType) {
-            currentThermostat.vacationBeginDay = Date
+            getCurrentThermostat().vacationBeginDay = Date
                     .from(((DateTimeType) command).getZonedDateTime().toInstant().truncatedTo(ChronoUnit.DAYS));
         } else {
             logger.warn("Unable to set value {}", command);
         }
     }
 
-    private void updateVacationEndDay(Thermostat thermostat) {
+    private void updateVacationEndDay(ThermostatModel thermostat) {
         updateState(BindingConstants.CHANNEL_OWD5_VACATIONENDDAY,
                 new DateTimeType(
                         ZonedDateTime.ofInstant(thermostat.vacationEndDay.toInstant(), timeZoneProvider.getTimeZone())
@@ -276,7 +342,7 @@ public class ThermostatHandler extends BaseThingHandler {
 
     private void updateVacationEndDay(Command command) {
         if (command instanceof DateTimeType) {
-            currentThermostat.vacationEndDay = Date
+            getCurrentThermostat().vacationEndDay = Date
                     .from(((DateTimeType) command).getZonedDateTime().toInstant().truncatedTo(ChronoUnit.DAYS));
         } else {
             logger.warn("Unable to set value {}", command);
@@ -311,8 +377,8 @@ public class ThermostatHandler extends BaseThingHandler {
         return map;
     };
 
-    private Map<String, Consumer<Thermostat>> createChannelRefreshActionMap() {
-        HashMap<String, Consumer<Thermostat>> map = new HashMap<>();
+    private Map<String, Consumer<ThermostatModel>> createChannelRefreshActionMap() {
+        HashMap<String, Consumer<ThermostatModel>> map = new HashMap<>();
         map.put(BindingConstants.CHANNEL_OWD5_GROUPNAME, this::updateGroupName);
         map.put(BindingConstants.CHANNEL_OWD5_GROUPID, this::updateGroupId);
         map.put(BindingConstants.CHANNEL_OWD5_ONLINE, this::updateOnline);
@@ -331,6 +397,15 @@ public class ThermostatHandler extends BaseThingHandler {
         return map;
     }
 
+    private Map<String, Consumer<ThermostatRealTimeValuesModel>> createRealTimeChannelRefreshActionMap() {
+        HashMap<String, Consumer<ThermostatRealTimeValuesModel>> map = new HashMap<>();
+        map.put(BindingConstants.CHANNEL_OWD5_ONLINE, this::updateOnline);
+        map.put(BindingConstants.CHANNEL_OWD5_HEATING, this::updateHeating);
+        map.put(BindingConstants.CHANNEL_OWD5_ROOMTEMPERATURE, this::updateRoomTemperature);
+        map.put(BindingConstants.CHANNEL_OWD5_FLOORTEMPERATURE, this::updateFloorTemperature);
+        return map;
+    }
+
     private Map<String, Consumer<Command>> createUpdateThermostatValueActionMap() {
         HashMap<String, Consumer<Command>> map = new HashMap<>();
         map.put(BindingConstants.CHANNEL_OWD5_REGULATIONMODE, this::updateRegulationMode);
index 62dc380516cfc92df7b9278cb6e8e3360b3a7af0..6e7ea95dedf7c75887bc880eb07bf43287e9603e 100644 (file)
@@ -14,7 +14,6 @@ package org.openhab.binding.ojelectronics.internal;
 
 import static org.openhab.binding.ojelectronics.internal.BindingConstants.THING_TYPE_OWD5;
 
-import java.util.Collections;
 import java.util.Set;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -38,7 +37,7 @@ import org.osgi.service.component.annotations.Reference;
 @Component(configurationPid = "binding.ojelectronics", service = ThingHandlerFactory.class)
 public class ThermostatHandlerFactory extends BaseThingHandlerFactory {
 
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_OWD5);
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OWD5);
     private final TimeZoneProvider timeZoneProvider;
 
     /**
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/common/SignalRLogger.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/common/SignalRLogger.java
new file mode 100644 (file)
index 0000000..af85dcd
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.common;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.LoggerFactory;
+
+import com.github.signalr4j.client.LogLevel;
+import com.github.signalr4j.client.Logger;
+
+/**
+ * Logs SignalR information
+ *
+ * @author Christian Kittel - Initial Contribution
+ */
+@NonNullByDefault
+public class SignalRLogger implements Logger {
+
+    private final org.slf4j.Logger logger = LoggerFactory.getLogger(SignalRLogger.class);
+
+    @Override
+    public void log(@Nullable String message, @Nullable LogLevel level) {
+        if (message == null || level == null) {
+            return;
+        }
+        switch (level) {
+            case Critical:
+                logger.warn("Critical SignalR Message: {}", message);
+                break;
+            case Information:
+                logger.info("SignalR information message: {}", message);
+                break;
+            case Verbose:
+            default:
+                logger.trace("SignalR information message: {}", message);
+                break;
+        }
+    }
+}
index a341e63d7a65140fc1637ef4086d1128ec1d60db..9e67b076d3138b78844f238f66fed0e7a5cd8bdd 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.ojelectronics.internal.config;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 
 /**
  * The configuration for {@link org.openhab.binding.ojelectronics.internal.OJElectronicsCloudHandler}
@@ -40,7 +41,7 @@ public class OJElectronicsBridgeConfiguration {
     /**
      * Url for API
      */
-    public String apiUrl = "https://OWD5-OJ001-App.ojelectronics.com/api";
+    private String apiUrl = "https://OWD5-OJ001-App.ojelectronics.com";
 
     /**
      * API-Key
@@ -52,8 +53,29 @@ public class OJElectronicsBridgeConfiguration {
      */
     public int softwareVersion = 1060;
 
-    /**
-     * Refresh-Delay
+    private @Nullable String restApiUrl;
+
+    /*
+     * Gets the Api-URL
+     */
+    public String getRestApiUrl() {
+        String localRestApiUrl = restApiUrl;
+        if (localRestApiUrl == null) {
+            localRestApiUrl = restApiUrl = apiUrl.replace("/api", "") + "/api";
+        }
+        return localRestApiUrl;
+    }
+
+    private @Nullable String signalRApiUrl;
+
+    /*
+     * Gets the SignalR Notification URL
      */
-    public long refreshDelayInSeconds = 30;
+    public String getSignalRUrl() {
+        String localSignalRApiUrl = signalRApiUrl;
+        if (localSignalRApiUrl == null) {
+            localSignalRApiUrl = signalRApiUrl = apiUrl.replace("/api", "") + "/ocd5notification";
+        }
+        return localSignalRApiUrl;
+    }
 }
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/SignalRResultModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/SignalRResultModel.java
new file mode 100644 (file)
index 0000000..8b8b622
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.ojelectronics.internal.models.groups.GroupContentModel;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatRealTimeValuesModel;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model for a SignalR query result
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class SignalRResultModel {
+    @SerializedName("Groups")
+    private List<GroupContentModel> groups = List.of();
+    @SerializedName("SequenceNr")
+    private int sequenceNr;
+
+    @SerializedName("ThermostatRealTimes")
+    private List<ThermostatRealTimeValuesModel> thermostatRealTimes = List.of();
+
+    @SerializedName("Thermostats")
+    private List<ThermostatModel> thermostats = List.of();
+
+    public List<GroupContentModel> getGroups() {
+        return this.groups;
+    }
+
+    public int getSequenceNr() {
+        return this.sequenceNr;
+    }
+
+    public List<ThermostatRealTimeValuesModel> getThermostatRealTimes() {
+        return this.thermostatRealTimes;
+    }
+
+    public List<ThermostatModel> getThermostats() {
+        return this.thermostats;
+    }
+
+    public void setGroups(List<GroupContentModel> paramArrayList) {
+        this.groups = paramArrayList;
+    }
+
+    public void setSequenceNr(int paramInt) {
+        this.sequenceNr = paramInt;
+    }
+
+    public void setThermostatRealTimes(List<ThermostatRealTimeValuesModel> paramArrayList) {
+        this.thermostatRealTimes = paramArrayList;
+    }
+
+    public void setThermostats(List<ThermostatModel> paramArrayList) {
+        this.thermostats = paramArrayList;
+    }
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/Thermostat.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/Thermostat.java
deleted file mode 100644 (file)
index 774d5b8..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.ojelectronics.internal.models;
-
-import java.util.Date;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.ojelectronics.internal.models.groups.Schedule;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Model for a thermostat
- *
- * @author Christian Kittel - Initial contribution
- */
-@NonNullByDefault
-public class Thermostat {
-
-    public int id;
-
-    public int action;
-
-    public String serialNumber = "";
-
-    public String groupName = "";
-
-    public int groupId;
-
-    public int customerId;
-
-    @SerializedName("SWversion")
-    public String softwareVersion = "";
-
-    public boolean online;
-
-    public boolean heating;
-
-    public int roomTemperature;
-
-    public int floorTemperature;
-
-    public int regulationMode;
-
-    public @Nullable Schedule schedule;
-
-    public int comfortSetpoint;
-
-    public Date comfortEndTime = new Date();
-
-    public int manualModeSetpoint;
-
-    public boolean vacationEnabled;
-
-    public Date vacationBeginDay = new Date();
-
-    public Date vacationEndDay = new Date();
-
-    public int vacationTemperature;
-
-    public boolean lastPrimaryModeIsAuto;
-
-    public Date boostEndTime = new Date();
-
-    public int frostProtectionTemperature;
-
-    public int errorCode;
-
-    public String thermostatName = "";
-
-    public boolean openWindow;
-
-    public boolean adaptiveMode;
-
-    public boolean daylightSaving;
-
-    public int sensorAppl;
-
-    public int minSetpoint;
-
-    public int maxSetpoint;
-
-    public int timeZone;
-
-    public boolean daylightSavingActive;
-
-    public int floorType;
-}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Day.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Day.java
deleted file mode 100644 (file)
index 6b088a1..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Model for a day
- *
- * @author Christian Kittel - Initial contribution
- */
-@NonNullByDefault
-public class Day {
-
-    public int weekDayGrpNo;
-
-    public List<Event> events = new ArrayList<>();
-}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/DayModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/DayModel.java
new file mode 100644 (file)
index 0000000..fca68d4
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Model for a day
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class DayModel {
+
+    public int weekDayGrpNo;
+
+    public List<EventModel> events = new ArrayList<>();
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Event.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Event.java
deleted file mode 100644 (file)
index e91d004..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Model for events
- *
- * @author Christian Kittel - Initial contribution
- */
-@NonNullByDefault
-public class Event {
-
-    public int scheduleType;
-
-    public String clock = "";
-
-    public int temperature;
-
-    public boolean active;
-
-    public boolean eventIsOnNextDay;
-}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/EventModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/EventModel.java
new file mode 100644 (file)
index 0000000..49cbd4c
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Model for events
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class EventModel {
+
+    public int scheduleType;
+
+    public String clock = "";
+
+    public int temperature;
+
+    public boolean active;
+
+    public boolean eventIsOnNextDay;
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContent.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContent.java
deleted file mode 100644 (file)
index 9ef2bbc..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.ojelectronics.internal.models.Thermostat;
-
-/**
- * Model for content of a group
- *
- * @author Christian Kittel - Initial contribution
- */
-@NonNullByDefault
-public class GroupContent {
-
-    public int action;
-
-    public int groupId;
-
-    public String groupName = "";
-
-    public List<Thermostat> thermostats = new ArrayList<Thermostat>();
-
-    public int regulationMode;
-
-    public @Nullable Schedule schedule;
-
-    public int comfortSetpoint;
-
-    public String comfortEndTime = "";
-
-    public int manualModeSetpoint;
-
-    public boolean vacationEnabled;
-
-    public String vacationBeginDay = "";
-
-    public String vacationEndDay = "";
-
-    public int vacationTemperature;
-
-    public boolean lastPrimaryModeIsAuto;
-
-    public String boostEndTime = "";
-
-    public int frostProtectionTemperature;
-}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContentModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/GroupContentModel.java
new file mode 100644 (file)
index 0000000..6d85763
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
+
+/**
+ * Model for content of a group
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class GroupContentModel {
+
+    public int action;
+
+    public int groupId;
+
+    public String groupName = "";
+
+    public List<ThermostatModel> thermostats = new ArrayList<ThermostatModel>();
+
+    public int regulationMode;
+
+    public @Nullable ScheduleModel schedule;
+
+    public int comfortSetpoint;
+
+    public String comfortEndTime = "";
+
+    public int manualModeSetpoint;
+
+    public boolean vacationEnabled;
+
+    public String vacationBeginDay = "";
+
+    public String vacationEndDay = "";
+
+    public int vacationTemperature;
+
+    public boolean lastPrimaryModeIsAuto;
+
+    public String boostEndTime = "";
+
+    public int frostProtectionTemperature;
+}
index b080ca0642cbd246596d4d840096f66db6fb18a4..c54cdeaa90c0f6d2f39ab1453949f3f91558d03d 100644 (file)
@@ -26,5 +26,5 @@ import org.openhab.binding.ojelectronics.internal.models.ResponseModelBase;
 @NonNullByDefault
 public class GroupContentResponseModel extends ResponseModelBase {
 
-    public List<GroupContent> groupContents = new ArrayList<GroupContent>();
+    public List<GroupContentModel> groupContents = new ArrayList<GroupContentModel>();
 }
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Schedule.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/Schedule.java
deleted file mode 100644 (file)
index 3ce6028..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Model for a schedule
- *
- * @author Christian Kittel - Initial contribution
- */
-@NonNullByDefault
-public class Schedule {
-
-    public List<Day> days = new ArrayList<Day>();
-
-    public boolean modifiedDueToVerification;
-}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/ScheduleModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/groups/ScheduleModel.java
new file mode 100644 (file)
index 0000000..22c63a0
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.groups;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Model for a schedule
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class ScheduleModel {
+
+    public List<DayModel> days = new ArrayList<DayModel>();
+
+    public boolean modifiedDueToVerification;
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModel.java
new file mode 100644 (file)
index 0000000..b0990bf
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.thermostat;
+
+import java.util.Date;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.ojelectronics.internal.models.groups.ScheduleModel;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Model for a thermostat
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class ThermostatModel extends ThermostatModelBase {
+
+    public int id;
+
+    public int action;
+
+    public String groupName = "";
+
+    public int groupId;
+
+    public int customerId;
+
+    @SerializedName("SWversion")
+    public String softwareVersion = "";
+
+    public boolean online;
+
+    public boolean heating;
+
+    public int roomTemperature;
+
+    public int floorTemperature;
+
+    public int regulationMode;
+
+    public @Nullable ScheduleModel schedule;
+
+    public int comfortSetpoint;
+
+    public Date comfortEndTime = new Date();
+
+    public int manualModeSetpoint;
+
+    public boolean vacationEnabled;
+
+    public Date vacationBeginDay = new Date();
+
+    public Date vacationEndDay = new Date();
+
+    public int vacationTemperature;
+
+    public boolean lastPrimaryModeIsAuto;
+
+    public Date boostEndTime = new Date();
+
+    public int frostProtectionTemperature;
+
+    public int errorCode;
+
+    public String thermostatName = "";
+
+    public boolean openWindow;
+
+    public boolean adaptiveMode;
+
+    public boolean daylightSaving;
+
+    public int sensorAppl;
+
+    public int minSetpoint;
+
+    public int maxSetpoint;
+
+    public int timeZone;
+
+    public boolean daylightSavingActive;
+
+    public int floorType;
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModelBase.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatModelBase.java
new file mode 100644 (file)
index 0000000..ed874e2
--- /dev/null
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.thermostat;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Base-Model for thermostat models
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class ThermostatModelBase {
+
+    public String serialNumber = "";
+}
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatRealTimeValuesModel.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/models/thermostat/ThermostatRealTimeValuesModel.java
new file mode 100644 (file)
index 0000000..d41b3bc
--- /dev/null
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.models.thermostat;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Model for realtime values of a thermostat
+ *
+ * @author Christian Kittel - Initial contribution
+ */
+@NonNullByDefault
+public class ThermostatRealTimeValuesModel extends ThermostatModelBase {
+
+    public int action;
+
+    public int floorTemperature;
+
+    public boolean heating;
+
+    public int id;
+
+    public boolean online;
+
+    public int roomTemperature;
+
+    public int sensorAppl;
+}
index c3ccdba3f89a3be7dbecbc6bc9f2d0a805f2421c..d3b6ec796d826226143dce33fdd0dc6d5eb833fc 100644 (file)
@@ -14,7 +14,6 @@ package org.openhab.binding.ojelectronics.internal.models.thermostat;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.ojelectronics.internal.models.RequestModelBase;
-import org.openhab.binding.ojelectronics.internal.models.Thermostat;
 
 /**
  * Model for updating a thermostat
@@ -24,12 +23,12 @@ import org.openhab.binding.ojelectronics.internal.models.Thermostat;
 @NonNullByDefault
 public class UpdateThermostatRequestModel extends RequestModelBase {
 
-    public UpdateThermostatRequestModel(Thermostat thermostat) {
+    public UpdateThermostatRequestModel(ThermostatModel thermostat) {
         setThermostat = thermostat;
         thermostatID = thermostat.serialNumber;
     }
 
-    public Thermostat setThermostat;
+    public ThermostatModel setThermostat;
 
     public String thermostatID;
 }
index 7409752eb8eca994066545e02745fe33fb244e68..d37334601a4b03a2218f8ab7b695712546dd2d78 100644 (file)
@@ -15,14 +15,13 @@ package org.openhab.binding.ojelectronics.internal.services;
 import static org.openhab.binding.ojelectronics.internal.BindingConstants.*;
 
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.ojelectronics.internal.OJCloudHandler;
-import org.openhab.binding.ojelectronics.internal.models.groups.GroupContent;
+import org.openhab.binding.ojelectronics.internal.models.groups.GroupContentModel;
 import org.openhab.core.config.discovery.AbstractDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
 import org.openhab.core.config.discovery.DiscoveryService;
@@ -39,12 +38,11 @@ import org.osgi.service.component.annotations.Component;
  */
 @NonNullByDefault
 @Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.ojelectronics")
-public final class OJDiscoveryService extends AbstractDiscoveryService
-        implements DiscoveryService, ThingHandlerService {
+public final class OJDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
 
-    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_OJCLOUD);
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_OJCLOUD);
     private @Nullable OJCloudHandler bridgeHandler;
-    private @Nullable Collection<GroupContent> groupContents;
+    private @Nullable Collection<GroupContentModel> groupContents;
 
     /**
      * Creates a new instance of {@link OJDiscoveryService}
@@ -59,14 +57,14 @@ public final class OJDiscoveryService extends AbstractDiscoveryService
      *
      * @param groupContents Content from API
      */
-    public void setScanResultForDiscovery(List<GroupContent> groupContents) {
+    public void setScanResultForDiscovery(List<GroupContentModel> groupContents) {
         this.groupContents = groupContents;
     }
 
     @Override
     protected void startScan() {
         final OJCloudHandler bridgeHandler = this.bridgeHandler;
-        final Collection<GroupContent> groupContents = this.groupContents;
+        final Collection<GroupContentModel> groupContents = this.groupContents;
         if (groupContents != null && bridgeHandler != null) {
             groupContents.stream().flatMap(content -> content.thermostats.stream())
                     .forEach(thermostat -> thingDiscovered(bridgeHandler.getThing().getUID(), thermostat.serialNumber));
index 8cdaf1ea4d4d6123f7c8d7472d5e132be8ef6e5d..bba63f40473ce4376ffe7f0873f1432bae771ad6 100644 (file)
  */
 package org.openhab.binding.ojelectronics.internal.services;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.ojelectronics.internal.ThermostatHandler;
-import org.openhab.binding.ojelectronics.internal.models.Thermostat;
-import org.openhab.binding.ojelectronics.internal.models.groups.GroupContent;
+import org.openhab.binding.ojelectronics.internal.models.groups.GroupContentModel;
 import org.openhab.core.thing.Thing;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -30,17 +31,17 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class RefreshGroupContentService {
 
-    private final List<GroupContent> groupContentList;
+    private final List<GroupContentModel> groupContentList;
     private final Logger logger = LoggerFactory.getLogger(RefreshGroupContentService.class);
     private List<Thing> things;
 
     /**
      * Creates a new instance of {@link RefreshGroupContentService}
      *
-     * @param groupContents {@link GroupContent}
+     * @param groupContents {@link GroupContentModel}
      * @param things Things
      */
-    public RefreshGroupContentService(List<GroupContent> groupContents, List<Thing> things) {
+    public RefreshGroupContentService(List<GroupContentModel> groupContents, List<Thing> things) {
         this.groupContentList = groupContents;
         this.things = things;
         if (this.things.isEmpty()) {
@@ -52,13 +53,7 @@ public class RefreshGroupContentService {
      * Handles the changes to all things.
      */
     public void handle() {
-        groupContentList.stream().flatMap(entry -> entry.thermostats.stream()).forEach(this::handleThermostat);
-    }
-
-    private void handleThermostat(Thermostat thermostat) {
-        things.stream().filter(thing -> thing.getHandler() instanceof ThermostatHandler)
-                .map(thing -> (ThermostatHandler) thing.getHandler())
-                .filter(thingHandler -> thingHandler.getSerialNumber().equals(thermostat.serialNumber))
-                .forEach(thingHandler -> thingHandler.handleThermostatRefresh(thermostat));
+        new RefreshThermostatsService(groupContentList.stream().flatMap(entry -> entry.thermostats.stream())
+                .collect(Collectors.toCollection(ArrayList::new)), things).handle();
     }
 }
index 446931de7257efa2e71c0a86c0a901b12a9b2c6f..399307660811437537b1bd2c57c8172a8a7b8f93 100644 (file)
  */
 package org.openhab.binding.ojelectronics.internal.services;
 
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+import java.util.Objects;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -26,11 +25,16 @@ import org.eclipse.jetty.client.util.BufferingResponseListener;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
 import org.openhab.binding.ojelectronics.internal.common.OJGSonBuilder;
+import org.openhab.binding.ojelectronics.internal.common.SignalRLogger;
 import org.openhab.binding.ojelectronics.internal.config.OJElectronicsBridgeConfiguration;
+import org.openhab.binding.ojelectronics.internal.models.SignalRResultModel;
 import org.openhab.binding.ojelectronics.internal.models.groups.GroupContentResponseModel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.github.signalr4j.client.Connection;
+import com.github.signalr4j.client.ConnectionState;
+import com.github.signalr4j.client.Platform;
 import com.google.gson.Gson;
 import com.google.gson.JsonSyntaxException;
 
@@ -47,14 +51,14 @@ public final class RefreshService implements AutoCloseable {
     private final HttpClient httpClient;
     private final Gson gson = OJGSonBuilder.getGSon();
 
-    private final ScheduledExecutorService schedulerService;
-
-    private @Nullable Runnable connectionLost;
-    private @Nullable BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> refreshDone;
-    private @Nullable ScheduledFuture<?> scheduler;
+    private @Nullable Consumer<@Nullable String> connectionLost;
+    private @Nullable BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> initializationDone;
+    private @Nullable BiConsumer<@Nullable SignalRResultModel, @Nullable String> refreshDone;
     private @Nullable Runnable unauthorized;
     private @Nullable String sessionId;
-    private static boolean destroyed = false;
+    private @Nullable Connection signalRConnection;
+    private boolean destroyed = false;
+    private boolean isInitializing = false;
 
     /**
      * Creates a new instance of {@link RefreshService}
@@ -63,11 +67,10 @@ public final class RefreshService implements AutoCloseable {
      * @param httpClient HTTP client
      * @param updateService Service to update the thermostat in the cloud
      */
-    public RefreshService(OJElectronicsBridgeConfiguration config, HttpClient httpClient,
-            ScheduledExecutorService schedulerService) {
+    public RefreshService(OJElectronicsBridgeConfiguration config, HttpClient httpClient) {
         this.config = config;
         this.httpClient = httpClient;
-        this.schedulerService = schedulerService;
+        Platform.loadPlatformComponent(null);
     }
 
     /**
@@ -78,17 +81,21 @@ public final class RefreshService implements AutoCloseable {
      * @param connectionLosed This method is called if no connection could established.
      * @param unauthorized This method is called if the result is unauthorized.
      */
-    public void start(String sessionId, BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> refreshDone,
-            Runnable connectionLost, Runnable unauthorized) {
+    public void start(String sessionId,
+            BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> initializationDone,
+            BiConsumer<@Nullable SignalRResultModel, @Nullable String> refreshDone,
+            Consumer<@Nullable String> connectionLost, Runnable unauthorized) {
         logger.trace("RefreshService.startService({})", sessionId);
         this.connectionLost = connectionLost;
+        this.initializationDone = initializationDone;
         this.refreshDone = refreshDone;
         this.unauthorized = unauthorized;
         this.sessionId = sessionId;
-        long refreshTime = config.refreshDelayInSeconds;
-        scheduler = schedulerService.scheduleWithFixedDelay(this::refresh, refreshTime, refreshTime, TimeUnit.SECONDS);
-        refresh();
+
+        signalRConnection = createSignalRConnection();
         destroyed = false;
+        isInitializing = false;
+        initializeGroups(true);
     }
 
     /**
@@ -96,25 +103,70 @@ public final class RefreshService implements AutoCloseable {
      */
     public void stop() {
         destroyed = true;
-        final ScheduledFuture<?> scheduler = this.scheduler;
-        if (scheduler != null) {
-            scheduler.cancel(false);
+        final Connection localSignalRConnection = signalRConnection;
+        if (localSignalRConnection != null) {
+            localSignalRConnection.stop();
+            signalRConnection = null;
         }
-        this.scheduler = null;
     }
 
-    private void refresh() {
+    private Connection createSignalRConnection() {
+        Connection signalRConnection = new Connection(config.getSignalRUrl(), new SignalRLogger());
+        signalRConnection.setReconnectOnError(false);
+        signalRConnection.received(json -> {
+            if (json != null && json.isJsonObject()) {
+                BiConsumer<@Nullable SignalRResultModel, @Nullable String> refreshDone = this.refreshDone;
+                if (refreshDone != null) {
+                    logger.trace("refresh {}", json);
+                    try {
+                        SignalRResultModel content = Objects
+                                .requireNonNull(gson.fromJson(json, SignalRResultModel.class));
+                        refreshDone.accept(content, null);
+                    } catch (JsonSyntaxException exception) {
+                        logger.debug("Error mapping Result to model", exception);
+                        refreshDone.accept(null, exception.getMessage());
+                    }
+                }
+            }
+        });
+        signalRConnection.stateChanged((oldState, newState) -> {
+            logger.trace("Connection state changed from {} to {}", oldState, newState);
+            if (newState == ConnectionState.Disconnected && !destroyed) {
+                handleConnectionLost("Connection broken");
+            }
+        });
+        signalRConnection.reconnected(() -> {
+            initializeGroups(false);
+        });
+        signalRConnection.connected(() -> {
+            signalRConnection.send(sessionId);
+        });
+        signalRConnection.error(error -> logger.info("SignalR error {}", error.getLocalizedMessage()));
+        return signalRConnection;
+    }
+
+    private void initializeGroups(boolean shouldStartSignalRService) {
+        if (destroyed || isInitializing) {
+            return;
+        }
         final String sessionId = this.sessionId;
         if (sessionId == null) {
-            handleConnectionLost();
+            handleConnectionLost("No session id");
         }
+        isInitializing = true;
+        logger.trace("initializeGroups started");
         final Runnable unauthorized = this.unauthorized;
         createRequest().send(new BufferingResponseListener() {
             @Override
             public void onComplete(@Nullable Result result) {
-                if (!destroyed) {
-                    if (result == null || result.isFailed()) {
-                        handleConnectionLost();
+                try {
+                    if (destroyed || result == null) {
+                        return;
+                    }
+                    if (result.isFailed()) {
+                        final Throwable failure = result.getFailure();
+                        logger.error("Error initializing groups", failure);
+                        handleConnectionLost(failure.getLocalizedMessage());
                     } else {
                         int status = result.getResponse().getStatus();
                         logger.trace("HTTP-Status {}", status);
@@ -122,32 +174,40 @@ public final class RefreshService implements AutoCloseable {
                             if (unauthorized != null) {
                                 unauthorized.run();
                             } else {
-                                handleConnectionLost();
+                                handleConnectionLost(null);
                             }
                         } else if (status == HttpStatus.OK_200) {
-                            handleRefreshDone(getContentAsString());
+                            initializationDone(Objects.requireNonNull(getContentAsString()));
+                            final Connection localSignalRConnection = signalRConnection;
+                            if (shouldStartSignalRService && localSignalRConnection != null) {
+                                localSignalRConnection.start();
+                            }
                         } else {
                             logger.warn("unsupported HTTP-Status {}", status);
-                            handleConnectionLost();
+                            handleConnectionLost(null);
                         }
                     }
+                } finally {
+                    logger.trace("initializeGroups completed");
+                    isInitializing = false;
                 }
             }
         });
     }
 
     private Request createRequest() {
-        Request request = httpClient.newRequest(config.apiUrl + "/Group/GroupContents").param("sessionid", sessionId)
-                .param("apiKey", config.apiKey).method(HttpMethod.GET);
+        Request request = httpClient.newRequest(config.getRestApiUrl() + "/Group/GroupContents")
+                .param("sessionid", sessionId).param("apiKey", config.apiKey).method(HttpMethod.GET);
         return request;
     }
 
-    private void handleRefreshDone(String responseBody) {
-        BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> refreshDone = this.refreshDone;
+    private void initializationDone(String responseBody) {
+        BiConsumer<@Nullable GroupContentResponseModel, @Nullable String> refreshDone = this.initializationDone;
         if (refreshDone != null) {
-            logger.trace("refresh {}", responseBody);
+            logger.trace("initializationDone {}", responseBody);
             try {
-                GroupContentResponseModel content = gson.fromJson(responseBody, GroupContentResponseModel.class);
+                GroupContentResponseModel content = Objects
+                        .requireNonNull(gson.fromJson(responseBody, GroupContentResponseModel.class));
                 refreshDone.accept(content, null);
             } catch (JsonSyntaxException exception) {
                 logger.debug("Error mapping Result to model", exception);
@@ -156,15 +216,15 @@ public final class RefreshService implements AutoCloseable {
         }
     }
 
-    private void handleConnectionLost() {
-        final Runnable connectionLost = this.connectionLost;
+    private void handleConnectionLost(@Nullable String message) {
+        final Consumer<@Nullable String> connectionLost = this.connectionLost;
         if (connectionLost != null) {
-            connectionLost.run();
+            connectionLost.accept(message);
         }
     }
 
     @Override
-    public void close() throws Exception {
+    public void close() {
         stop();
     }
 }
diff --git a/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/RefreshThermostatsService.java b/bundles/org.openhab.binding.ojelectronics/src/main/java/org/openhab/binding/ojelectronics/internal/services/RefreshThermostatsService.java
new file mode 100644 (file)
index 0000000..8b3dfa1
--- /dev/null
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2010-2023 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.ojelectronics.internal.services;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.ojelectronics.internal.ThermostatHandler;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModelBase;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatRealTimeValuesModel;
+import org.openhab.core.thing.Thing;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Refreshes values of {@link ThermostatHandler}
+ *
+ * @author Christian Kittel - Initial Contribution
+ */
+@NonNullByDefault
+public class RefreshThermostatsService {
+
+    private final List<ThermostatModel> thermostats;
+    private final Logger logger = LoggerFactory.getLogger(RefreshThermostatsService.class);
+    private final List<Thing> things;
+    private final List<ThermostatRealTimeValuesModel> realTimeValues;
+
+    /**
+     * Creates a new instance of {@link RefreshThermostatsService}
+     *
+     * @param thermostats {@link ThermostatModel}
+     * @param things Things
+     */
+    public RefreshThermostatsService(List<ThermostatModel> thermostats, List<Thing> things) {
+        this(thermostats, new ArrayList<>(), things);
+    }
+
+    /**
+     * Creates a new instance of {@link RefreshThermostatsService}
+     *
+     * @param thermostats {@link ThermostatModel}
+     * @param realTimeValues {@link ThermostatRealTimeValuesModel}
+     * @param things Things
+     */
+    public RefreshThermostatsService(List<ThermostatModel> thermostats,
+            List<ThermostatRealTimeValuesModel> realTimeValues, List<Thing> things) {
+        this.thermostats = thermostats;
+        this.things = things;
+        this.realTimeValues = realTimeValues;
+        if (this.things.isEmpty()) {
+            logger.warn("Bridge contains no thermostats.");
+        }
+    }
+
+    /**
+     * Handles the changes to all things.
+     */
+    public synchronized void handle() {
+        thermostats.forEach(thermostat -> handleThermostat(thermostat, this::handleThermostatRefresh));
+        realTimeValues.forEach(thermostat -> handleThermostat(thermostat, this::handleThermostatRealTimeValueRefresh));
+    }
+
+    private <T extends ThermostatModelBase> void handleThermostat(T thermostat,
+            BiConsumer<ThermostatHandler, T> refreshHandler) {
+        things.stream().filter(thing -> thing.getHandler() instanceof ThermostatHandler)
+                .map(thing -> (ThermostatHandler) thing.getHandler())
+                .filter(thingHandler -> thingHandler.getSerialNumber().equals(thermostat.serialNumber))
+                .forEach(thingHandler -> {
+                    try {
+                        refreshHandler.accept(Objects.requireNonNull(thingHandler), thermostat);
+                    } catch (Exception e) {
+                        logger.info("Error Handling Refresh of thermostat {}", thermostat, e);
+                    }
+                });
+    }
+
+    private void handleThermostatRefresh(ThermostatHandler thingHandler, ThermostatModel thermostat) {
+        thingHandler.handleThermostatRefresh(thermostat);
+    }
+
+    private void handleThermostatRealTimeValueRefresh(ThermostatHandler thingHandler,
+            ThermostatRealTimeValuesModel thermostat) {
+        thingHandler.handleThermostatRefresh(thermostat);
+    }
+}
index dbc95dd3e615a567697529127376f95dadc8d6d7..1a364c1d70416210d1be16c19ea11f9f16cf5a7a 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.ojelectronics.internal.services;
 
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -27,6 +29,8 @@ import org.openhab.binding.ojelectronics.internal.config.OJElectronicsBridgeConf
 import org.openhab.binding.ojelectronics.internal.models.RequestModelBase;
 import org.openhab.binding.ojelectronics.internal.models.userprofile.PostSignInQueryModel;
 import org.openhab.binding.ojelectronics.internal.models.userprofile.PostSignInResponseModel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
 
@@ -39,7 +43,7 @@ import com.google.gson.Gson;
 public class SignInService {
 
     private final Gson gson = OJGSonBuilder.getGSon();
-
+    private final Logger logger = LoggerFactory.getLogger(SignInService.class);
     private final HttpClient httpClient;
     private final OJElectronicsBridgeConfiguration config;
 
@@ -61,30 +65,41 @@ public class SignInService {
      * @param connectionLosed This method is called if no connection could established.
      * @param unauthorized This method is called if the result is unauthorized.
      */
-    public void signIn(Consumer<String> signInDone, Runnable connectionLosed, Runnable unauthorized) {
-        Request request = httpClient.POST(config.apiUrl + "/UserProfile/SignIn")
+    public void signIn(Consumer<String> signInDone, Consumer<@Nullable String> connectionLosed, Runnable unauthorized) {
+        logger.trace("Trying to sign in");
+
+        Request request = httpClient.POST(config.getRestApiUrl() + "/UserProfile/SignIn")
                 .header(HttpHeader.CONTENT_TYPE, "application/json")
-                .content(new StringContentProvider(gson.toJson(getPostSignInQueryModel())));
+                .content(new StringContentProvider(gson.toJson(getPostSignInQueryModel())))
+                .timeout(1, TimeUnit.MINUTES);
 
         request.send(new BufferingResponseListener() {
             @Override
             public void onComplete(@Nullable Result result) {
-                if (result == null || result.isFailed()) {
-                    connectionLosed.run();
+                if (result == null) {
+                    return;
+                }
+
+                if (result.isFailed()) {
+                    final Throwable failure = result.getFailure();
+                    logger.error("Signing in failed", failure);
+                    connectionLosed.accept(failure.getLocalizedMessage());
                     return;
                 }
+
                 if (result.getResponse().getStatus() == 200) {
-                    PostSignInResponseModel signInModel = gson.fromJson(getContentAsString(),
-                            PostSignInResponseModel.class);
-                    if (signInModel == null || signInModel.errorCode != 0 || signInModel.sessionId.equals("")) {
+                    PostSignInResponseModel signInModel = Objects
+                            .requireNonNull(gson.fromJson(getContentAsString(), PostSignInResponseModel.class));
+                    if (signInModel.errorCode != 0 || signInModel.sessionId.equals("")) {
                         unauthorized.run();
                         return;
                     }
+                    logger.trace("Signing in successful {}", getContentAsString());
                     signInDone.accept(signInModel.sessionId);
                     return;
                 }
 
-                connectionLosed.run();
+                connectionLosed.accept(null);
                 return;
             }
         });
index 7beb926a0f2157e9154cbf18a3bc7854bcb7730e..22ac84d6c01aed92f9e044adf7700bd9b878f862 100644 (file)
@@ -13,6 +13,8 @@
 package org.openhab.binding.ojelectronics.internal.services;
 
 import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -26,7 +28,7 @@ import org.openhab.binding.ojelectronics.internal.ThermostatHandler;
 import org.openhab.binding.ojelectronics.internal.common.OJGSonBuilder;
 import org.openhab.binding.ojelectronics.internal.config.OJElectronicsBridgeConfiguration;
 import org.openhab.binding.ojelectronics.internal.models.SimpleResponseModel;
-import org.openhab.binding.ojelectronics.internal.models.Thermostat;
+import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
 import org.openhab.binding.ojelectronics.internal.models.thermostat.UpdateThermostatRequestModel;
 import org.openhab.core.thing.Thing;
 import org.slf4j.Logger;
@@ -45,14 +47,17 @@ public final class UpdateService {
     private final Gson gson = OJGSonBuilder.getGSon();
     private final Logger logger = LoggerFactory.getLogger(UpdateService.class);
 
-    private final String sessionId;
     private final HttpClient httpClient;
     private final OJElectronicsBridgeConfiguration configuration;
+    private final Runnable unauthorized;
+    private final Consumer<@Nullable String> connectionLost;
 
-    public UpdateService(OJElectronicsBridgeConfiguration configuration, HttpClient httpClient, String sessionId) {
+    public UpdateService(OJElectronicsBridgeConfiguration configuration, HttpClient httpClient,
+            Consumer<@Nullable String> connectionLost, Runnable unauthorized) {
         this.configuration = configuration;
         this.httpClient = httpClient;
-        this.sessionId = sessionId;
+        this.unauthorized = unauthorized;
+        this.connectionLost = connectionLost;
     }
 
     /**
@@ -61,34 +66,42 @@ public final class UpdateService {
      * @param things
      */
     public void updateAllThermostats(List<Thing> things) {
+        new SignInService(configuration, httpClient).signIn((sessionId) -> updateAllThermostats(things, sessionId),
+                connectionLost, unauthorized);
+    }
+
+    private void updateAllThermostats(List<Thing> things, String sessionId) {
         things.stream().filter(thing -> thing.getHandler() instanceof ThermostatHandler)
                 .map(thing -> (ThermostatHandler) thing.getHandler())
-                .map(handler -> handler.tryHandleAndGetUpdatedThermostat()).forEach(this::updateThermostat);
+                .map(handler -> handler.tryHandleAndGetUpdatedThermostat())
+                .forEach((thermostat) -> updateThermostat(thermostat, sessionId));
     }
 
-    private void updateThermostat(@Nullable Thermostat thermostat) {
+    private void updateThermostat(@Nullable ThermostatModel thermostat, String sessionId) {
         if (thermostat == null) {
             return;
         }
-        Request request = httpClient.POST(configuration.apiUrl + "/Thermostat/UpdateThermostat")
+        String jsonPayload = gson.toJson(new UpdateThermostatRequestModel(thermostat).withApiKey(configuration.apiKey));
+        Request request = httpClient.POST(configuration.getRestApiUrl() + "/Thermostat/UpdateThermostat")
                 .param("sessionid", sessionId).header(HttpHeader.CONTENT_TYPE, "application/json")
-                .content(new StringContentProvider(
-                        gson.toJson(new UpdateThermostatRequestModel(thermostat).withApiKey(configuration.apiKey))));
+                .content(new StringContentProvider(jsonPayload));
+        logger.trace("updateThermostat payload for themostat with serial {} is {}", thermostat.serialNumber,
+                jsonPayload);
 
         request.send(new BufferingResponseListener() {
             @Override
             public void onComplete(@Nullable Result result) {
                 if (result != null) {
-                    logger.trace("onComplete {}", result);
+                    logger.trace("onComplete Http Status {} {}", result.getResponse().getStatus(), result);
                     if (result.isFailed()) {
-                        logger.warn("updateThermostat failed {}", thermostat);
+                        logger.warn("updateThermostat failed for themostat with serial {}", thermostat.serialNumber);
+                        return;
                     }
-                    SimpleResponseModel responseModel = gson.fromJson(getContentAsString(), SimpleResponseModel.class);
-                    if (responseModel == null) {
-                        logger.warn("updateThermostat failed with empty result {}", thermostat);
-                    } else if (responseModel.errorCode != 0) {
-                        logger.warn("updateThermostat failed with errorCode {} {}", responseModel.errorCode,
-                                thermostat);
+                    SimpleResponseModel responseModel = Objects
+                            .requireNonNull(gson.fromJson(getContentAsString(), SimpleResponseModel.class));
+                    if (responseModel.errorCode != 0) {
+                        logger.warn("updateThermostat failed with errorCode {} for thermostat with serial {}",
+                                responseModel.errorCode, thermostat.serialNumber);
                     }
                 }
             }
index 10910ce756210038ef7446a1c78575067550c40b..177912d5a8658893c22d237a76da40a28af5d372 100644 (file)
@@ -15,13 +15,11 @@ thing-type.ojelectronics.owd5.description = OWD5/MWD5 Thermostat
 thing-type.config.ojelectronics.ojcloud.apiKey.label = API Key
 thing-type.config.ojelectronics.ojcloud.apiKey.description = API-Key from your local distributor
 thing-type.config.ojelectronics.ojcloud.apiUrl.label = API-URL
-thing-type.config.ojelectronics.ojcloud.apiUrl.description = URL to cloud API-service.
+thing-type.config.ojelectronics.ojcloud.apiUrl.description = URL to cloud API-service and Socket-Notification.
 thing-type.config.ojelectronics.ojcloud.customerId.label = Customer ID
 thing-type.config.ojelectronics.ojcloud.customerId.description = Customer ID
 thing-type.config.ojelectronics.ojcloud.password.label = Password
 thing-type.config.ojelectronics.ojcloud.password.description = Password for access cloud service.
-thing-type.config.ojelectronics.ojcloud.refreshDelayInSeconds.label = Refresh Delay
-thing-type.config.ojelectronics.ojcloud.refreshDelayInSeconds.description = Refresh delay in seconds.
 thing-type.config.ojelectronics.ojcloud.softwareVersion.label = Software Version
 thing-type.config.ojelectronics.ojcloud.softwareVersion.description = Software Version
 thing-type.config.ojelectronics.ojcloud.userName.label = User Name
index ba64ef5d99f59acbdc7bb354b40352b07bc6ebd8..251f62555535c6aceee6438e62bae18ac445c781 100644 (file)
                        </parameter>
                        <parameter name="apiUrl" type="text" required="true">
                                <label>API-URL</label>
-                               <description>URL to cloud API-service.</description>
+                               <description>URL to cloud API-service and Socket-Notification.</description>
                                <context>url</context>
                                <advanced>true</advanced>
-                               <default>https://OWD5-OJ001-App.ojelectronics.com/api</default>
-                       </parameter>
-                       <parameter name="refreshDelayInSeconds" type="integer" required="true" min="15" unit="s">
-                               <label>Refresh Delay</label>
-                               <description>Refresh delay in seconds.</description>
-                               <advanced>true</advanced>
-                               <default>30</default>
+                               <default>https://OWD5-OJ001-App.ojelectronics.com</default>
                        </parameter>
                        <parameter name="customerId" type="integer" required="true">
                                <label>Customer ID</label>