6 Commits

Author SHA1 Message Date
zyphlar
fb34fd73bc Add compilation prerequisites to README
I compiled the JAR on the raspi (distributed image) itself and needed to add maven, libpigpio.so.1, and JDK in order to complete it (in addition to pigpio and Java runtime which I'm sure are required for running but preinstalled on the image.)
2023-02-25 14:14:06 -08:00
Mark Milligan
95a554422d Synchronize around the wait call that keeps the service running. 2022-11-17 17:21:36 -06:00
Mark Milligan
bf648f069c Prevent an NPE if apple SSO is not configured on a self-hosted server. 2022-11-06 18:50:09 -06:00
Mark Milligan
07765061bd Add config flag for hubs to post to self-hosted server with a self-signed SSL certificate.
Support Raspberry Pi 2B by handling missing BLE controller and supporting 32-bit pigpio.
2022-11-06 18:21:54 -06:00
Mark Milligan
a8236cebc1 Merge branch 'main' of https://github.com/MarkBryanMilligan/LanternPowerMonitor into main 2022-10-22 14:36:35 -05:00
Mark Milligan
b14e6a086d Update BOM for out of stock parts 2022-10-22 14:35:40 -05:00
15 changed files with 194 additions and 44 deletions

View File

@@ -68,7 +68,7 @@ When you add the hub to your configuration via the app, you can change where the
1. After that second restart, you should start seeing data. If you're not getting data in your app, the hub probably was not able to get a wifi connection. You can pull the last 10 lines of the log file and network details from the hub via bluetooth on the hub config page to troubleshoot. 1. After that second restart, you should start seeing data. If you're not getting data in your app, the hub probably was not able to get a wifi connection. You can pull the last 10 lines of the log file and network details from the hub via bluetooth on the hub config page to troubleshoot.
# Ok, but I don't like doing things the easy way. # Ok, but I don't like doing things the easy way.
First, you and I will get along just fine. Second, do a reactor build from the root folder: First, you and I will get along just fine. Second, do a reactor build from the root folder. You may need to install `maven libpigpio-dev default-jdk` or similar depending on your OS if you don't already have them. Then:
``` ```
~/LanternPowerMonitor$ mvn clean install ~/LanternPowerMonitor$ mvn clean install
``` ```

Binary file not shown.

10
bom/LCSC_BOM.csv Normal file
View File

@@ -0,0 +1,10 @@
LCSC Part Number,Quantity
C2977589,1
C43846,1
C43840,1
C385441,2
C385449,1
C385460,1
C385498,1
C433508,1
C397337,16
1 LCSC Part Number Quantity
2 C2977589 1
3 C43846 1
4 C43840 1
5 C385441 2
6 C385449 1
7 C385460 1
8 C385498 1
9 C433508 1
10 C397337 16

View File

@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>lantern-currentmonitor</artifactId> <artifactId>lantern-currentmonitor</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>1.1.1</version> <version>1.1.3</version>
<name>lantern-currentmonitor</name> <name>lantern-currentmonitor</name>
<parent> <parent>

View File

@@ -7,27 +7,38 @@ import com.lanternsoftware.currentmonitor.bluetooth.BleHelper;
import com.lanternsoftware.currentmonitor.bluetooth.BleService; import com.lanternsoftware.currentmonitor.bluetooth.BleService;
import com.lanternsoftware.datamodel.currentmonitor.HubConfigService; import com.lanternsoftware.datamodel.currentmonitor.HubConfigService;
import com.lanternsoftware.util.CollectionUtils; import com.lanternsoftware.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
public class BluetoothConfig { public class BluetoothConfig {
private static final Logger LOG = LoggerFactory.getLogger(BluetoothConfig.class);
private final BleApplication app; private final BleApplication app;
public BluetoothConfig(String _hubName, BleCharacteristicListener _listener) { public BluetoothConfig(String _hubName, BleCharacteristicListener _listener) {
BleHelper.getAdapter().setPowered(true); BleApplication a = null;
BleHelper.requestBusName("com.lanternsoftware"); try {
BleHelper.setBasePath("/com/lanternsoftware"); BleHelper.getAdapter().setPowered(true);
HubConfigService service = new HubConfigService(); BleHelper.requestBusName("com.lanternsoftware");
List<BleCharacteristic> chars = CollectionUtils.transform(service.getCharacteristics(), _c->new BleCharacteristic("HubConfig", _c.getUUID(), _c.name(), _c.getFlags())); BleHelper.setBasePath("/com/lanternsoftware");
chars.forEach(_c->_c.setListener(_listener)); List<BleCharacteristic> chars = CollectionUtils.transform(HubConfigService.getCharacteristics(), _c -> new BleCharacteristic("HubConfig", _c.getUUID(), _c.name(), _c.getFlags()));
app = new BleApplication("Lantern", _hubName, new BleService("HubConfig", service.getServiceUUID(), chars)); chars.forEach(_c -> _c.setListener(_listener));
a = new BleApplication("Lantern", _hubName, new BleService("HubConfig", HubConfigService.getServiceUUID(), chars));
}
catch (Throwable _t) {
LOG.error("Failed to initialize BLE service", _t);
}
app = a;
} }
public void start() { public void start() {
app.start(); if (app != null)
app.start();
} }
public void stop() { public void stop() {
app.stop(); if (app != null)
app.stop();
} }
} }

View File

@@ -219,7 +219,7 @@ public class MonitorApp {
config = new MonitorConfig(); config = new MonitorConfig();
ResourceLoader.writeFile(WORKING_DIR + "config.json", DaoSerializer.toJson(config)); ResourceLoader.writeFile(WORKING_DIR + "config.json", DaoSerializer.toJson(config));
} }
pool = new HttpPool(10, 10, config.getSocketTimeout(), config.getConnectTimeout(), config.getSocketTimeout()); pool = HttpPool.builder().withValidateSSLCertificates(!config.isAcceptSelfSignedCertificates()).build();
if (NullUtils.isNotEmpty(config.getHost())) if (NullUtils.isNotEmpty(config.getHost()))
host = NullUtils.terminateWith(config.getHost(), "/"); host = NullUtils.terminateWith(config.getHost(), "/");
monitor.setDebug(config.isDebug()); monitor.setDebug(config.isDebug());
@@ -301,10 +301,12 @@ public class MonitorApp {
monitor.stop(); monitor.stop();
pool.shutdown(); pool.shutdown();
}, "Monitor Shutdown")); }, "Monitor Shutdown"));
try { synchronized (monitor) {
monitor.wait(); try {
} catch (InterruptedException _e) { monitor.wait();
LOG.error("Interrupted, shutting down", _e); } catch (InterruptedException _e) {
LOG.error("Interrupted, shutting down", _e);
}
} }
} }

View File

@@ -19,6 +19,7 @@ public class MonitorConfig {
private int socketTimeout; private int socketTimeout;
private boolean postSamples = false; private boolean postSamples = false;
private boolean needsCalibration = true; private boolean needsCalibration = true;
private boolean acceptSelfSignedCertificates = false;
private String mqttBrokerUrl; private String mqttBrokerUrl;
private String mqttUserName; private String mqttUserName;
private String mqttPassword; private String mqttPassword;
@@ -115,6 +116,14 @@ public class MonitorConfig {
needsCalibration = _needsCalibration; needsCalibration = _needsCalibration;
} }
public boolean isAcceptSelfSignedCertificates() {
return acceptSelfSignedCertificates;
}
public void setAcceptSelfSignedCertificates(boolean _acceptSelfSignedCertificates) {
acceptSelfSignedCertificates = _acceptSelfSignedCertificates;
}
public String getMqttBrokerUrl() { public String getMqttBrokerUrl() {
return mqttBrokerUrl; return mqttBrokerUrl;
} }

View File

@@ -36,6 +36,7 @@ public class MonitorConfigSerializer extends AbstractDaoSerializer<MonitorConfig
d.put("socket_timeout", _o.getSocketTimeout()); d.put("socket_timeout", _o.getSocketTimeout());
d.put("post_samples", _o.isPostSamples()); d.put("post_samples", _o.isPostSamples());
d.put("needs_calibration", _o.isNeedsCalibration()); d.put("needs_calibration", _o.isNeedsCalibration());
d.put("accept_self_signed_certificates", _o.isAcceptSelfSignedCertificates());
d.put("mqtt_broker_url", _o.getMqttBrokerUrl()); d.put("mqtt_broker_url", _o.getMqttBrokerUrl());
d.put("mqtt_user_name", _o.getMqttUserName()); d.put("mqtt_user_name", _o.getMqttUserName());
d.put("mqtt_password", _o.getMqttPassword()); d.put("mqtt_password", _o.getMqttPassword());
@@ -60,6 +61,7 @@ public class MonitorConfigSerializer extends AbstractDaoSerializer<MonitorConfig
o.setSocketTimeout(DaoSerializer.getInteger(_d, "socket_timeout")); o.setSocketTimeout(DaoSerializer.getInteger(_d, "socket_timeout"));
o.setPostSamples(DaoSerializer.getBoolean(_d, "post_samples")); o.setPostSamples(DaoSerializer.getBoolean(_d, "post_samples"));
o.setNeedsCalibration(DaoSerializer.getBoolean(_d, "needs_calibration")); o.setNeedsCalibration(DaoSerializer.getBoolean(_d, "needs_calibration"));
o.setAcceptSelfSignedCertificates(DaoSerializer.getBoolean(_d, "accept_self_signed_certificates"));
o.setMqttBrokerUrl(DaoSerializer.getString(_d, "mqtt_broker_url")); o.setMqttBrokerUrl(DaoSerializer.getString(_d, "mqtt_broker_url"));
o.setMqttUserName(DaoSerializer.getString(_d, "mqtt_user_name")); o.setMqttUserName(DaoSerializer.getString(_d, "mqtt_user_name"));
o.setMqttPassword(DaoSerializer.getString(_d, "mqtt_password")); o.setMqttPassword(DaoSerializer.getString(_d, "mqtt_password"));

View File

@@ -662,7 +662,7 @@ public class MongoCurrentMonitorDao implements CurrentMonitorDao {
if (NullUtils.isEmpty(_username) || NullUtils.isEmpty(_password)) if (NullUtils.isEmpty(_username) || NullUtils.isEmpty(_password))
return null; return null;
Account acct = proxy.queryOne(Account.class, new DaoQuery("username", _username.toLowerCase().trim())); Account acct = proxy.queryOne(Account.class, new DaoQuery("username", _username.toLowerCase().trim()));
if ((acct == null) || !BCrypt.checkpw(_password, acct.getPassword())) if ((acct == null) || !BCrypt.checkpw(_password, NullUtils.makeNotNull(acct.getPassword())))
return null; return null;
return toAuthCode(acct.getId(), acct.getAuxiliaryAccountIds()); return toAuthCode(acct.getId(), acct.getAuxiliaryAccountIds());
} }

View File

@@ -8,7 +8,7 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
public class HubConfigService { public abstract class HubConfigService {
public static final UUIDFormatter uuidFormat = new UUIDFormatter("c5650001-d50f-49af-b906-cada0dc17937"); public static final UUIDFormatter uuidFormat = new UUIDFormatter("c5650001-d50f-49af-b906-cada0dc17937");
private static final AESTool aes = new AESTool(37320708309265127L,-8068168662055796771L,-4867793276337148572L,4425609941731230765L); private static final AESTool aes = new AESTool(37320708309265127L,-8068168662055796771L,-4867793276337148572L,4425609941731230765L);
private static final UUID serviceUUID = uuidFormat.format(1); private static final UUID serviceUUID = uuidFormat.format(1);
@@ -20,7 +20,7 @@ public class HubConfigService {
return serviceUUID; return serviceUUID;
} }
public List<HubConfigCharacteristic> getCharacteristics() { public static List<HubConfigCharacteristic> getCharacteristics() {
return Arrays.asList(HubConfigCharacteristic.values()); return Arrays.asList(HubConfigCharacteristic.values());
} }

View File

@@ -28,7 +28,7 @@ public class AppleSSO {
private final String audience; private final String audience;
public AppleSSO(String _credentialsPath) { public AppleSSO(String _credentialsPath) {
audience = ResourceLoader.loadFileAsString(_credentialsPath).trim(); audience = NullUtils.trim(ResourceLoader.loadFileAsString(_credentialsPath));
} }
public String getEmailFromIdToken(String _idToken) { public String getEmailFromIdToken(String _idToken) {

View File

@@ -1,27 +1,36 @@
package com.lanternsoftware.util.http; package com.lanternsoftware.util.http;
import java.io.IOException; import com.lanternsoftware.util.NullUtils;
import java.io.InputStream;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.CookieStore; import org.apache.http.client.CookieStore;
import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.lanternsoftware.util.NullUtils; import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
public class HttpPool { public class HttpPool {
private static final Logger LOG = LoggerFactory.getLogger(HttpPool.class); private static final Logger LOG = LoggerFactory.getLogger(HttpPool.class);
@@ -32,17 +41,43 @@ public class HttpPool {
private final PoolingHttpClientConnectionManager connectionManager; private final PoolingHttpClientConnectionManager connectionManager;
public HttpPool(int _maxTotalConnections, int _maxPerRoute) { public HttpPool(int _maxTotalConnections, int _maxPerRoute) {
this(_maxTotalConnections, _maxPerRoute, 10000, 5000, 10000); this(_maxTotalConnections, _maxPerRoute, null, null);
}
public HttpPool(int _maxTotalConnections, int _maxPerRoute, KeyStore _keystore, String _keystorePassword) {
this(_maxTotalConnections, _maxPerRoute, 10000, 5000, 10000, _keystore, _keystorePassword, true);
} }
public HttpPool(int _maxTotalConnections, int _maxPerRoute, int _socketTimeoutMs, int _connectTimeoutMs, int _connectionRequestTimeoutMs) { public HttpPool(int _maxTotalConnections, int _maxPerRoute, int _socketTimeoutMs, int _connectTimeoutMs, int _connectionRequestTimeoutMs) {
this(_maxTotalConnections, _maxPerRoute, _socketTimeoutMs, _connectTimeoutMs, _connectionRequestTimeoutMs, null, null, true);
}
public HttpPool(int _maxTotalConnections, int _maxPerRoute, int _socketTimeoutMs, int _connectTimeoutMs, int _connectionRequestTimeoutMs, KeyStore _keystore, String _keystorePassword, boolean _validateSSLCertificates) {
requestConfig = RequestConfig.custom().setSocketTimeout(_socketTimeoutMs).setConnectTimeout(_connectTimeoutMs).setConnectionRequestTimeout(_connectionRequestTimeoutMs).setCookieSpec(CookieSpecs.STANDARD).build(); requestConfig = RequestConfig.custom().setSocketTimeout(_socketTimeoutMs).setConnectTimeout(_connectTimeoutMs).setConnectionRequestTimeout(_connectionRequestTimeoutMs).setCookieSpec(CookieSpecs.STANDARD).build();
keepAliveStrategy = (HttpResponse response, HttpContext context) -> 0; keepAliveStrategy = (HttpResponse response, HttpContext context) -> 0;
connectionManager = new PoolingHttpClientConnectionManager(); Registry<ConnectionSocketFactory> registry = null;
if ((_keystore != null) || !_validateSSLCertificates) {
try {
SSLContextBuilder contextBuilder = SSLContexts.custom();
if (_keystore != null)
contextBuilder.loadKeyMaterial(_keystore, _keystorePassword.toCharArray());
if (!_validateSSLCertificates)
contextBuilder.loadTrustMaterial(null, (x509CertChain, authType) -> true);
SSLConnectionSocketFactory socketFactory = _validateSSLCertificates ? new SSLConnectionSocketFactory(contextBuilder.build()) : new SSLConnectionSocketFactory(contextBuilder.build(), NoopHostnameVerifier.INSTANCE);
registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", socketFactory).register("http", new PlainConnectionSocketFactory()).build();
} catch (Exception _e) {
LOG.error("Failed to load SSL keystore", _e);
}
}
connectionManager = registry != null ? new PoolingHttpClientConnectionManager(registry) : new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(_maxTotalConnections); connectionManager.setMaxTotal(_maxTotalConnections);
connectionManager.setDefaultMaxPerRoute(_maxPerRoute); connectionManager.setDefaultMaxPerRoute(_maxPerRoute);
} }
public static Builder builder() {
return new Builder();
}
public void shutdown() { public void shutdown() {
connectionManager.shutdown(); connectionManager.shutdown();
} }
@@ -76,26 +111,28 @@ public class HttpPool {
} }
public byte[] executeToByteArray(HttpUriRequest _request) { public byte[] executeToByteArray(HttpUriRequest _request) {
return executeToPayload(_request).getPayload();
}
public HttpResponsePayload executeToPayload(HttpUriRequest _request) {
InputStream is = null; InputStream is = null;
CloseableHttpResponse resp = null; CloseableHttpResponse resp = null;
try { try {
resp = execute(_request); resp = execute(_request);
if (resp == null) if (resp == null)
return null; return new HttpResponsePayload(HttpStatus.SC_INTERNAL_SERVER_ERROR, null);
if ((resp.getStatusLine().getStatusCode() < 200) || (resp.getStatusLine().getStatusCode() >= 300)) { if ((resp.getStatusLine().getStatusCode() < 200) || (resp.getStatusLine().getStatusCode() >= 300))
LOG.error("Failed to make http request to " + _request.getURI().toString() + ". Status code: " + resp.getStatusLine().getStatusCode()); LOG.error("Failed to make http request to " + _request.getURI().toString() + ". Status code: " + resp.getStatusLine().getStatusCode());
return null;
}
HttpEntity entity = resp.getEntity(); HttpEntity entity = resp.getEntity();
if (entity != null) { if (entity != null) {
is = entity.getContent(); is = entity.getContent();
return IOUtils.toByteArray(is); return new HttpResponsePayload(resp.getStatusLine().getStatusCode(), IOUtils.toByteArray(is));
} }
return null; return new HttpResponsePayload(resp.getStatusLine().getStatusCode(), null);
} }
catch (Exception _e) { catch (Exception _e) {
LOG.error("Failed to make http request to " + _request.getURI().toString(), _e); LOG.error("Failed to make http request to " + _request.getURI().toString(), _e);
return null; return new HttpResponsePayload(HttpStatus.SC_INTERNAL_SERVER_ERROR, null);
} }
finally { finally {
IOUtils.closeQuietly(is); IOUtils.closeQuietly(is);
@@ -106,4 +143,57 @@ public class HttpPool {
public static void addBasicAuthHeader(HttpUriRequest _request, String _username, String _password) { public static void addBasicAuthHeader(HttpUriRequest _request, String _username, String _password) {
_request.addHeader("Authorization", "Basic " + Base64.encodeBase64String(NullUtils.toByteArray(_username + ":" + _password))); _request.addHeader("Authorization", "Basic " + Base64.encodeBase64String(NullUtils.toByteArray(_username + ":" + _password)));
} }
public static final class Builder {
private int maxTotalConnections = 10;
private int maxPerRoute = 10;
private int socketTimeoutMs = 10000;
private int connectTimeoutMs = 5000;
private int connectionRequestTimeoutMs = 10000;
private KeyStore keystore;
private String keystorePassword;
private boolean validateSSLCertificates = true;
private Builder() {
}
public Builder withMaxTotalConnections(int val) {
maxTotalConnections = val;
return this;
}
public Builder withMaxPerRoute(int val) {
maxPerRoute = val;
return this;
}
public Builder withSocketTimeoutMs(int val) {
socketTimeoutMs = val;
return this;
}
public Builder withConnectTimeoutMs(int val) {
connectTimeoutMs = val;
return this;
}
public Builder withConnectionRequestTimeoutMs(int val) {
connectionRequestTimeoutMs = val;
return this;
}
public Builder withKeystore(KeyStore _keystore, String _password) {
keystore = _keystore;
keystorePassword = _password;
return this;
}
public Builder withValidateSSLCertificates(boolean val) {
validateSSLCertificates = val;
return this;
}
public HttpPool build() {
return new HttpPool(maxTotalConnections, maxPerRoute, socketTimeoutMs, connectTimeoutMs, connectionRequestTimeoutMs, keystore, keystorePassword, validateSSLCertificates);
}
}
} }

View File

@@ -0,0 +1,29 @@
package com.lanternsoftware.util.http;
import com.lanternsoftware.util.NullUtils;
public class HttpResponsePayload {
private final int status;
private final byte[] payload;
public HttpResponsePayload(int _status, byte[] _payload) {
status = _status;
payload = _payload;
}
public int getStatus() {
return status;
}
public byte[] getPayload() {
return payload;
}
public boolean isSuccess() {
return (status >= 200) && (status < 300);
}
public String asString() {
return NullUtils.toString(payload);
}
}

View File

@@ -76,7 +76,6 @@ public class ZWaveApp {
private HttpPool pool; private HttpPool pool;
private SwitchScheduleTask nextScheduleTask; private SwitchScheduleTask nextScheduleTask;
private final Map<Integer, Double> sensors = new HashMap<>(); private final Map<Integer, Double> sensors = new HashMap<>();
private final Object ZWAVE_MUTEX = new Object();
private ExecutorService executor = null; private ExecutorService executor = null;
public void start() { public void start() {
@@ -559,17 +558,15 @@ public class ZWaveApp {
if (_sw.isSourceUrlValid()) if (_sw.isSourceUrlValid())
return DaoSerializer.getDouble(DaoSerializer.parse(pool.executeToString(new HttpGet(_sw.getSourceUrl()))), "temp"); return DaoSerializer.getDouble(DaoSerializer.parse(pool.executeToString(new HttpGet(_sw.getSourceUrl()))), "temp");
else if (_sw.isZWaveThermostat() && config.isMySwitch(_sw)) { else if (_sw.isZWaveThermostat() && config.isMySwitch(_sw)) {
synchronized (ZWAVE_MUTEX) { synchronized (sensors) {
synchronized (sensors) { controller.send(new MultilevelSensorGetRequest((byte) _sw.getNodeId()));
controller.send(new MultilevelSensorGetRequest((byte) _sw.getNodeId())); try {
try { sensors.wait(3000);
sensors.wait(3000); } catch (InterruptedException _e) {
} catch (InterruptedException _e) { _e.printStackTrace();
_e.printStackTrace();
}
Double temp = sensors.get(_sw.getNodeId());
return (temp == null) ? 0.0 : temp;
} }
Double temp = sensors.get(_sw.getNodeId());
return (temp == null) ? 0.0 : temp;
} }
} }
return 0.0; return 0.0;