[OJElectronics] Add SignalR for requesting data from OJ Electronics cloud (#13782)

* Fixed some Nullable annotation warnings
* Use SignalR instead pooling

Signed-off-by: Christian Kittel <ckittel@gmx.de>
This commit is contained in:
Christian Kittel 2023-04-08 11:40:59 +02:00 committed by GitHub
parent 6748dfedd7
commit 64723db7aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 716 additions and 207 deletions

View 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

View 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. |

View File

@ -13,4 +13,21 @@
<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>

View 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() {
logger.trace("OJElectronicsCloudHandler.handleConnectionLost()");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
final RefreshService refreshService = this.refreshService;
if (refreshService != null) {
refreshService.stop();
public void reInitialize() {
logger.trace("OJElectronicsCloudHandler.reInitialize()");
final RefreshService localRefreshService = this.refreshService;
if (localRefreshService != null) {
localRefreshService.stop();
}
restartRefreshServiceAsync(configuration.refreshDelayInSeconds);
restartRefreshServiceAsync(1);
}
private void handleConnectionLost(@Nullable String message) {
logger.trace("OJElectronicsCloudHandler.handleConnectionLost()");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
final RefreshService localRefreshService = this.refreshService;
if (localRefreshService != null) {
localRefreshService.stop();
}
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);
}
}

View 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;

View 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));
}
/**
* Gets a {@link Thermostat} with changed values or null if nothing has changed
* Sets the values after refreshing the thermostats values
*
* @return The changed {@link Thermostat}
* @param thermostat thermostat values
*/
public @Nullable Thermostat tryHandleAndGetUpdatedThermostat() {
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 ThermostatModel} with changed values or null if nothing has changed
*
* @return The changed {@link ThermostatModel}
*/
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);

View 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;
/**

View File

@ -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;
}
}
}

View 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 long refreshDelayInSeconds = 30;
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 String getSignalRUrl() {
String localSignalRApiUrl = signalRApiUrl;
if (localSignalRApiUrl == null) {
localSignalRApiUrl = signalRApiUrl = apiUrl.replace("/api", "") + "/ocd5notification";
}
return localSignalRApiUrl;
}
}

View File

@ -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;
}
}

View File

@ -23,9 +23,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* @author Christian Kittel - Initial contribution
*/
@NonNullByDefault
public class Day {
public class DayModel {
public int weekDayGrpNo;
public List<Event> events = new ArrayList<>();
public List<EventModel> events = new ArrayList<>();
}

View File

@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* @author Christian Kittel - Initial contribution
*/
@NonNullByDefault
public class Event {
public class EventModel {
public int scheduleType;

View File

@ -17,7 +17,7 @@ import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ojelectronics.internal.models.Thermostat;
import org.openhab.binding.ojelectronics.internal.models.thermostat.ThermostatModel;
/**
* Model for content of a group
@ -25,7 +25,7 @@ import org.openhab.binding.ojelectronics.internal.models.Thermostat;
* @author Christian Kittel - Initial contribution
*/
@NonNullByDefault
public class GroupContent {
public class GroupContentModel {
public int action;
@ -33,11 +33,11 @@ public class GroupContent {
public String groupName = "";
public List<Thermostat> thermostats = new ArrayList<Thermostat>();
public List<ThermostatModel> thermostats = new ArrayList<ThermostatModel>();
public int regulationMode;
public @Nullable Schedule schedule;
public @Nullable ScheduleModel schedule;
public int comfortSetpoint;

View 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>();
}

View File

@ -23,9 +23,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
* @author Christian Kittel - Initial contribution
*/
@NonNullByDefault
public class Schedule {
public class ScheduleModel {
public List<Day> days = new ArrayList<Day>();
public List<DayModel> days = new ArrayList<DayModel>();
public boolean modifiedDueToVerification;
}

View File

@ -10,13 +10,13 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.ojelectronics.internal.models;
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.Schedule;
import org.openhab.binding.ojelectronics.internal.models.groups.ScheduleModel;
import com.google.gson.annotations.SerializedName;
@ -26,14 +26,12 @@ import com.google.gson.annotations.SerializedName;
* @author Christian Kittel - Initial contribution
*/
@NonNullByDefault
public class Thermostat {
public class ThermostatModel extends ThermostatModelBase {
public int id;
public int action;
public String serialNumber = "";
public String groupName = "";
public int groupId;
@ -53,7 +51,7 @@ public class Thermostat {
public int regulationMode;
public @Nullable Schedule schedule;
public @Nullable ScheduleModel schedule;
public int comfortSetpoint;

View File

@ -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 = "";
}

View File

@ -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;
}

View 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;
}

View 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));

View File

@ -12,12 +12,13 @@
*/
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();
}
}

View File

@ -12,10 +12,9 @@
*/
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();
}
}

View File

@ -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);
}
}

View 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;
}
});

View 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) {
things.stream().filter(thing -> thing.getHandler() instanceof ThermostatHandler)
.map(thing -> (ThermostatHandler) thing.getHandler())
.map(handler -> handler.tryHandleAndGetUpdatedThermostat()).forEach(this::updateThermostat);
new SignInService(configuration, httpClient).signIn((sessionId) -> updateAllThermostats(things, sessionId),
connectionLost, unauthorized);
}
private void updateThermostat(@Nullable Thermostat thermostat) {
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((thermostat) -> updateThermostat(thermostat, sessionId));
}
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);
}
}
}

View 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

View File

@ -22,16 +22,10 @@
</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>