Skip to content

Commit

Permalink
[WlanThermo] Add support for new ESP32-powered devices [V3.x] (openha…
Browse files Browse the repository at this point in the history
…b#9579)

* Add support for ESP32 devices
* Add Unit Tests
Ensure Gson objects are NonNull
Generify Handlers
Generify Utils

Signed-off-by: Christian Schlipp <[email protected]>
Signed-off-by: John Marshall <[email protected]>
  • Loading branch information
CSchlipp authored and themillhousegroup committed May 10, 2021
1 parent d4bb284 commit 7b80823
Show file tree
Hide file tree
Showing 72 changed files with 5,155 additions and 1,403 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.binding.wlanthermo.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ThingTypeUID;

/**
Expand All @@ -27,11 +28,17 @@ public class WlanThermoBindingConstants {
private static final String BINDING_ID = "wlanthermo";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_WLANTHERMO_NANO = new ThingTypeUID(BINDING_ID, "nano");
public static final ThingTypeUID THING_TYPE_WLANTHERMO_NANO_V1 = new ThingTypeUID(BINDING_ID, "nano");
public static final ThingTypeUID THING_TYPE_WLANTHERMO_MINI = new ThingTypeUID(BINDING_ID, "mini");
public static final ThingTypeUID THING_TYPE_WLANTHERMO_ESP32 = new ThingTypeUID(BINDING_ID, "esp32");

// ThreadPool
public static final String WLANTHERMO_THREAD_POOL = "wlanthermo";
// Properties
public static final String PROPERTY_MODEL = "model";
public static final String PROPERTY_SERIAL = "serial";
public static final String PROPERTY_ESP32_BT_ENABLED = "esp32_bt_enabled";
public static final String PROPERTY_ESP32_PM_ENABLED = "esp32_pm_enabled";
public static final String PROPERTY_ESP32_TEMP_CHANNELS = "esp32_temp_channels";
public static final String PROPERTY_ESP32_PM_CHANNELS = "esp32_pm_channels";

// List of all Channel ids
// System Channels
Expand All @@ -43,16 +50,7 @@ public class WlanThermoBindingConstants {
public static final String SYSTEM_CPU_LOAD = "cpu_load";
public static final String SYSTEM_CPU_TEMP = "cpu_temp";

public static final String CHANNEL0 = "channel0";
public static final String CHANNEL1 = "channel1";
public static final String CHANNEL2 = "channel2";
public static final String CHANNEL3 = "channel3";
public static final String CHANNEL4 = "channel4";
public static final String CHANNEL5 = "channel5";
public static final String CHANNEL6 = "channel6";
public static final String CHANNEL7 = "channel7";
public static final String CHANNEL8 = "channel8";
public static final String CHANNEL9 = "channel9";
public static final String CHANNEL_PREFIX = "channel";

public static final String CHANNEL_NAME = "name";
public static final String CHANNEL_TYP = "typ";
Expand All @@ -67,6 +65,9 @@ public class WlanThermoBindingConstants {
public static final String CHANNEL_COLOR = "color";
public static final String CHANNEL_COLOR_NAME = "color_name";

public static final String CHANNEL_PITMASTER_PREFIX = "pit";
public static final String CHANNEL_PITMASTER_1 = "pit1";
public static final String CHANNEL_PITMASTER_2 = "pit2";
public static final String CHANNEL_PITMASTER_ENABLED = "enabled"; // Mini
public static final String CHANNEL_PITMASTER_CURRENT = "current"; // Mini
public static final String CHANNEL_PITMASTER_SETPOINT = "setpoint"; // Mini+Nano
Expand All @@ -76,7 +77,12 @@ public class WlanThermoBindingConstants {
public static final String CHANNEL_PITMASTER_STATE = "state"; // Nano
public static final String CHANNEL_PITMASTER_PIDPROFILE = "pid_id"; // Nano

public static final String TRIGGER_ALARM_OFF = "OFF";
public static final String TRIGGER_NONE = "";
public static final String TRIGGER_ALARM_MIN = "MIN";
public static final String TRIGGER_ALARM_MAX = "MAX";

public static final DecimalType SIGNAL_STRENGTH_4 = new DecimalType(4);
public static final DecimalType SIGNAL_STRENGTH_3 = new DecimalType(3);
public static final DecimalType SIGNAL_STRENGTH_2 = new DecimalType(2);
public static final DecimalType SIGNAL_STRENGTH_1 = new DecimalType(1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link WlanThermoMiniConfiguration} class contains fields mapping thing configuration parameters.
* The {@link WlanThermoConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Christian Schlipp - Initial contribution
*/
@NonNullByDefault
public class WlanThermoMiniConfiguration {
public class WlanThermoConfiguration {

/**
* IP Address of WlanThermo.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2021 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.wlanthermo.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link WlanThermoException} is thrown if an exception in WlanThermoBinding occurs.
*
* @author Christian Schlipp - Initial contribution
*/
@NonNullByDefault
public class WlanThermoException extends Exception {

static final long serialVersionUID = 1L;

public WlanThermoException(String reason) {
super(reason, null);
}

public WlanThermoException(String message, Throwable cause) {
super(message, cause);
}

public WlanThermoException(Throwable cause) {
super(cause);
}

public WlanThermoException() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2010-2021 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.wlanthermo.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;

/**
* The {@link WlanThermoExtendedConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Christian Schlipp - Initial contribution
*/
@NonNullByDefault
public class WlanThermoExtendedConfiguration extends WlanThermoConfiguration {

/**
* Username of WlanThermo user.
*/
private String username = "";

/**
* Password of WlanThermo user.
*/

private String password = "";

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* Copyright (c) 2010-2021 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.wlanthermo.internal;

import static org.openhab.binding.wlanthermo.internal.WlanThermoUtil.requireNonNull;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.util.DigestAuthentication;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.openhab.core.thing.*;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;

/**
* The {@link WlanThermoHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Christian Schlipp - Initial contribution
*/
@NonNullByDefault
public abstract class WlanThermoHandler extends BaseThingHandler {

private final boolean extendedConfig;
protected WlanThermoConfiguration config = new WlanThermoConfiguration();
protected final HttpClient httpClient;
protected final Logger logger = LoggerFactory.getLogger(WlanThermoHandler.class);
protected final Gson gson = new Gson();
protected @Nullable ScheduledFuture<?> pollingScheduler;

public WlanThermoHandler(Thing thing, HttpClient httpClient, boolean extendedConfig) {
super(thing);
this.httpClient = httpClient;
this.extendedConfig = extendedConfig;
}

@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
try {
if (extendedConfig) {
config = getConfigAs(WlanThermoExtendedConfiguration.class);
WlanThermoExtendedConfiguration extendedConfig = (WlanThermoExtendedConfiguration) config;
if (extendedConfig.getUsername().isEmpty() && !extendedConfig.getPassword().isEmpty()) {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
authStore.addAuthentication(new DigestAuthentication(config.getUri(), Authentication.ANY_REALM,
extendedConfig.getUsername(), extendedConfig.getPassword()));
}
} else {
config = getConfigAs(WlanThermoConfiguration.class);
}
pollingScheduler = scheduler.scheduleWithFixedDelay(this::checkConnectionAndUpdate, 0,
config.getPollingInterval(), TimeUnit.SECONDS);
} catch (URISyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Failed to initialize WlanThermo: " + e.getMessage());
}
}

@Override
public void dispose() {
ScheduledFuture<?> oldScheduler = pollingScheduler;
if (oldScheduler != null) {
boolean stopped = oldScheduler.cancel(true);
logger.debug("Stopped polling: {}", stopped);
}
pollingScheduler = null;
}

protected void checkConnectionAndUpdate() {
if (this.thing.getStatus() != ThingStatus.ONLINE) {
try {
if (httpClient.GET(config.getUri()).getStatus() == 200) {
updateStatus(ThingStatus.ONLINE);
// rerun immediately to update state
checkConnectionAndUpdate();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"WlanThermo not found under given address.");
}
} catch (URISyntaxException | ExecutionException | TimeoutException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Could not connect to WlanThermo at " + config.getIpAddress() + ": " + e.getMessage());
} catch (InterruptedException e) {
logger.debug("Connection check interrupted. {}", e.getMessage());
}
} else {
pull();
}
}

protected boolean doPost(String endpoint, String json) throws InterruptedException {
try {
URI uri = config.getUri(endpoint);
int status = httpClient.POST(uri).content(new StringContentProvider(json), "application/json")
.timeout(5, TimeUnit.SECONDS).send().getStatus();
if (status == 401) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"No or wrong login credentials provided. Please configure username/password for write access to WlanThermo!");
return false;
} else if (status != 200) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Failed to update channel on device, Statuscode " + status + " on URI " + uri.toString());
logger.debug("Payload sent: {}", json);
// Still continue to try next channel
return true;
} else {
updateStatus(ThingStatus.ONLINE);
return true;
}
} catch (TimeoutException | ExecutionException | URISyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Failed to update channel on device: " + e.getMessage());
return false;
}
}

protected <T> T doGet(String endpoint, Class<T> object) throws InterruptedException, WlanThermoException {
try {
String json = httpClient.GET(config.getUri(endpoint)).getContentAsString();
logger.debug("Received at {}: {}", endpoint, json);
return requireNonNull(gson.fromJson(json, object));
} catch (URISyntaxException | ExecutionException | TimeoutException | WlanThermoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Update failed: " + e.getMessage());
for (Channel channel : thing.getChannels()) {
updateState(channel.getUID(), UnDefType.UNDEF);
}
throw new WlanThermoException(e);
}
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
try {
State s = getState(channelUID);
updateState(channelUID, s);
} catch (WlanThermoException e) {
logger.debug("Could not handle command of type {} for channel {}!",
command.getClass().toGenericString(), channelUID.getId());
}
} else {
if (setState(channelUID, command) && thing.getStatus() == ThingStatus.ONLINE) {
logger.debug("Data updated, pushing changes");
scheduler.execute(this::push);
} else {
logger.debug("Could not handle command of type {} for channel {}!",
command.getClass().toGenericString(), channelUID.getId());
}
}
}

protected abstract void push();

protected abstract void pull();

protected abstract State getState(ChannelUID channelUID)
throws WlanThermoInputException, WlanThermoUnknownChannelException;

protected abstract boolean setState(ChannelUID channelUID, Command command);
}
Loading

0 comments on commit 7b80823

Please sign in to comment.