mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[airq] Improve error handling (#16694)
Signed-off-by: Fabian Wolter <github@fabian-wolter.de> Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
parent
ee9a0a31bf
commit
803178ee87
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.airq.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for handling an empty response.
|
||||||
|
*
|
||||||
|
* @author Fabian Wolter - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AirqEmptyResonseException extends AirqException {
|
||||||
|
private static final long serialVersionUID = 1423144673651821622L;
|
||||||
|
|
||||||
|
public AirqEmptyResonseException() {
|
||||||
|
super("Device sent an empty response");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.airq.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General exception for this binding.
|
||||||
|
*
|
||||||
|
* @author Fabian Wolter - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AirqException extends Exception {
|
||||||
|
private static final long serialVersionUID = 8255154215873928896L;
|
||||||
|
|
||||||
|
public AirqException() {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirqException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirqException(Exception exception) {
|
||||||
|
super(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AirqException(String message, Exception exception) {
|
||||||
|
super(message, exception);
|
||||||
|
}
|
||||||
|
}
|
@ -65,16 +65,17 @@ import com.google.gson.Gson;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link AirqHandler} is responsible for retrieving all information from the air-Q device
|
* The {@link AirqHandler} is responsible for retrieving all information from the air-Q device
|
||||||
* and change properties and channels accordingly.
|
* and change properties and channels accordingly.
|
||||||
*
|
*
|
||||||
* @author Aurelio Caliaro - Initial contribution
|
* @author Aurelio Caliaro - Initial contribution
|
||||||
|
* @author Fabian Wolter - Improve error handling
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class AirqHandler extends BaseThingHandler {
|
public class AirqHandler extends BaseThingHandler {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(AirqHandler.class);
|
private final Logger logger = LoggerFactory.getLogger(AirqHandler.class);
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
private @Nullable ScheduledFuture<?> pollingJob;
|
private @Nullable ScheduledFuture<?> pollingJob;
|
||||||
@ -112,7 +113,6 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
public AirqHandler(Thing thing, HttpClient httpClient) {
|
public AirqHandler(Thing thing, HttpClient httpClient) {
|
||||||
super(thing);
|
super(thing);
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
logger.warn("air-Q - airqHandler - constructor: httpClient={}", httpClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTimeFormat(String str) {
|
private boolean isTimeFormat(String str) {
|
||||||
@ -309,17 +309,7 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
config = getThing().getConfiguration().as(AirqConfiguration.class);
|
config = getThing().getConfiguration().as(AirqConfiguration.class);
|
||||||
updateStatus(ThingStatus.UNKNOWN);
|
updateStatus(ThingStatus.UNKNOWN);
|
||||||
// We don't have to test if ipAddress and password have been set because we have defined them
|
|
||||||
// as being 'required' in thing-types.xml and OpenHAB will only initialize the handler if both are set.
|
|
||||||
String data = getDecryptedContentString("http://" + config.ipAddress + "/data", "GET", null);
|
|
||||||
// we try if the device is reachable and the password is correct. Otherwise a corresponding message is
|
|
||||||
// thrown in Thing manager.
|
|
||||||
if (data == null) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
|
||||||
"Unable to retrieve get data from air-Q device. Probable cause: invalid password.");
|
|
||||||
} else {
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
|
||||||
}
|
|
||||||
pollingJob = scheduler.scheduleWithFixedDelay(this::pollData, 0, POLLING_PERIOD_DATA_MSEC,
|
pollingJob = scheduler.scheduleWithFixedDelay(this::pollData, 0, POLLING_PERIOD_DATA_MSEC,
|
||||||
TimeUnit.MILLISECONDS);
|
TimeUnit.MILLISECONDS);
|
||||||
getConfigDataJob = scheduler.scheduleWithFixedDelay(this::getConfigData, 0, POLLING_PERIOD_CONFIG,
|
getConfigDataJob = scheduler.scheduleWithFixedDelay(this::getConfigData, 0, POLLING_PERIOD_CONFIG,
|
||||||
@ -327,7 +317,7 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AES decoding based on this tutorial: https://www.javainterviewpoint.com/aes-256-encryption-and-decryption/
|
// AES decoding based on this tutorial: https://www.javainterviewpoint.com/aes-256-encryption-and-decryption/
|
||||||
public @Nullable String decrypt(byte[] base64text, String password) {
|
public String decrypt(byte[] base64text, String password) throws AirqException {
|
||||||
String content = "";
|
String content = "";
|
||||||
logger.trace("air-Q - airqHandler - decrypt(): content to decrypt: {}", base64text);
|
logger.trace("air-Q - airqHandler - decrypt(): content to decrypt: {}", base64text);
|
||||||
byte[] encodedtextwithIV = Base64.getDecoder().decode(base64text);
|
byte[] encodedtextwithIV = Base64.getDecoder().decode(base64text);
|
||||||
@ -345,16 +335,16 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
byte[] decryptedText = cipher.doFinal(ciphertext);
|
byte[] decryptedText = cipher.doFinal(ciphertext);
|
||||||
content = new String(decryptedText, StandardCharsets.UTF_8);
|
content = new String(decryptedText, StandardCharsets.UTF_8);
|
||||||
logger.trace("air-Q - airqHandler - decrypt(): Text decoded as String: {}", content);
|
logger.trace("air-Q - airqHandler - decrypt(): Text decoded as String: {}", content);
|
||||||
} catch (BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
return content;
|
||||||
|
} catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
|
| InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
|
||||||
logger.warn("Error while decrypting. Probably the provided password is wrong.");
|
throw new AirqException(exc);
|
||||||
return null;
|
} catch (BadPaddingException e) {
|
||||||
|
throw new AirqPasswordIncorrectException();
|
||||||
}
|
}
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String encrypt(byte[] toencode, String password) {
|
public String encrypt(byte[] toencode, String password) throws AirqException {
|
||||||
String content = "";
|
|
||||||
logger.trace("air-Q - airqHandler - encrypt(): text to encode: {}", new String(toencode));
|
logger.trace("air-Q - airqHandler - encrypt(): text to encode: {}", new String(toencode));
|
||||||
byte[] passkey = Arrays.copyOf(password.getBytes(StandardCharsets.UTF_8), 32);
|
byte[] passkey = Arrays.copyOf(password.getBytes(StandardCharsets.UTF_8), 32);
|
||||||
if (password.length() < 32) {
|
if (password.length() < 32) {
|
||||||
@ -375,45 +365,34 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
System.arraycopy(encryptedText, 0, totaltext, 16, encryptedText.length);
|
System.arraycopy(encryptedText, 0, totaltext, 16, encryptedText.length);
|
||||||
byte[] encodedcontent = Base64.getEncoder().encode(totaltext);
|
byte[] encodedcontent = Base64.getEncoder().encode(totaltext);
|
||||||
logger.trace("air-Q - airqHandler - encrypt(): encrypted text: {}", encodedcontent);
|
logger.trace("air-Q - airqHandler - encrypt(): encrypted text: {}", encodedcontent);
|
||||||
content = new String(encodedcontent);
|
return new String(encodedcontent);
|
||||||
} catch (Exception e) {
|
} catch (BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
|
||||||
logger.warn("air-Q - airqHandler - encrypt(): Error while encrypting: {}", e.toString());
|
| InvalidAlgorithmParameterException | IllegalBlockSizeException exc) {
|
||||||
|
throw new AirqException("Failed to encrypt data", exc);
|
||||||
}
|
}
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets the data after online/offline management and does the JSON work, or at least the first step.
|
// gets the data after online/offline management and does the JSON work, or at least the first step.
|
||||||
protected @Nullable String getDecryptedContentString(String url, String requestMethod, @Nullable String body) {
|
protected String getDecryptedContentString(String url, String requestMethod, @Nullable String body)
|
||||||
Result res = null;
|
throws AirqException {
|
||||||
String jsonAnswer = null;
|
Result res = getData(url, "GET", null);
|
||||||
res = getData(url, "GET", null);
|
String jsontext = res.getBody();
|
||||||
if (res != null) {
|
logger.trace("air-Q - airqHandler - getDecryptedContentString(): Result from getData() is {} with body={}", res,
|
||||||
String jsontext = res.getBody();
|
res.getBody());
|
||||||
logger.trace("air-Q - airqHandler - getDecryptedContentString(): Result from getData() is {} with body={}",
|
// Gson code based on https://riptutorial.com/de/gson
|
||||||
res, res.getBody());
|
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||||
// Gson code based on https://riptutorial.com/de/gson
|
if (ans == null) {
|
||||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
throw new AirqEmptyResonseException();
|
||||||
if (ans != null) {
|
|
||||||
JsonObject jsonObj = ans.getAsJsonObject();
|
|
||||||
jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
|
||||||
if (jsonAnswer == null) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
|
||||||
"Decryption not possible, probably wrong password");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"air-Q - airqHandler - getDecryptedContentString(): The air-Q data could not be extracted from this string: {}",
|
|
||||||
ans);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return jsonAnswer;
|
|
||||||
|
JsonObject jsonObj = ans.getAsJsonObject();
|
||||||
|
return decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// calls the networking job and in addition does additional tests for online/offline management
|
// calls the networking job and in addition does additional tests for online/offline management
|
||||||
protected @Nullable Result getData(String address, String requestMethod, @Nullable String body) {
|
protected Result getData(String address, String requestMethod, @Nullable String body) throws AirqException {
|
||||||
Result res = null;
|
|
||||||
int timeout = 10;
|
int timeout = 10;
|
||||||
logger.debug("air-Q - airqHandler - getData(): connecting to {} with method {} and body {}", address,
|
logger.trace("air-Q - airqHandler - getData(): connecting to {} with method {} and body {}", address,
|
||||||
requestMethod, body);
|
requestMethod, body);
|
||||||
Request request = httpClient.newRequest(address).timeout(timeout, TimeUnit.SECONDS).method(requestMethod);
|
Request request = httpClient.newRequest(address).timeout(timeout, TimeUnit.SECONDS).method(requestMethod);
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
@ -422,22 +401,10 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ContentResponse response = request.send();
|
ContentResponse response = request.send();
|
||||||
res = new Result(response.getContentAsString(), response.getStatus());
|
return new Result(response.getContentAsString(), response.getStatus());
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
|
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
|
||||||
logger.warn("air-Q - airqHandler - doNetwork(): Error while accessing air-Q: {}", exc.toString());
|
throw new AirqException("Error while accessing air-Q", exc);
|
||||||
}
|
}
|
||||||
if (res == null) {
|
|
||||||
if (getThing().getStatus() != ThingStatus.OFFLINE) {
|
|
||||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "air-Q device not reachable");
|
|
||||||
} else {
|
|
||||||
logger.warn("air-Q - airqHandler - getData(): retried but still cannot reach the air-Q device.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Result {
|
public static class Result {
|
||||||
@ -460,11 +427,14 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (pollingJob != null) {
|
ScheduledFuture<?> localPollingJob = pollingJob;
|
||||||
pollingJob.cancel(true);
|
if (localPollingJob != null) {
|
||||||
|
localPollingJob.cancel(true);
|
||||||
}
|
}
|
||||||
if (getConfigDataJob != null) {
|
|
||||||
getConfigDataJob.cancel(true);
|
ScheduledFuture<?> localGetConfigDataJob = getConfigDataJob;
|
||||||
|
if (localGetConfigDataJob != null) {
|
||||||
|
localGetConfigDataJob.cancel(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,60 +443,71 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
try {
|
try {
|
||||||
String url = "http://" + config.ipAddress + "/data";
|
String url = "http://" + config.ipAddress + "/data";
|
||||||
String jsonAnswer = getDecryptedContentString(url, "GET", null);
|
String jsonAnswer = getDecryptedContentString(url, "GET", null);
|
||||||
if (jsonAnswer != null) {
|
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
||||||
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
if (decEl == null) {
|
||||||
if (decEl != null) {
|
throw new AirqEmptyResonseException();
|
||||||
JsonObject decObj = decEl.getAsJsonObject();
|
|
||||||
logger.debug("air-Q - airqHandler - run(): decObj={}, jsonAnswer={}", decObj, jsonAnswer);
|
|
||||||
// 'bat' is a field that is already delivered by air-Q but as
|
|
||||||
// there are no air-Q devices which are powered with batteries
|
|
||||||
// it is obsolete at this moment. We implemented the code anyway
|
|
||||||
// to make it easier to add afterwords, but for the moment it is not applicable.
|
|
||||||
// processType(decObj, "bat", "battery", "pair");
|
|
||||||
processType(decObj, "cnt0_3", "fineDustCnt00_3", "pair");
|
|
||||||
processType(decObj, "cnt0_5", "fineDustCnt00_5", "pair");
|
|
||||||
processType(decObj, "cnt1", "fineDustCnt01", "pair");
|
|
||||||
processType(decObj, "cnt2_5", "fineDustCnt02_5", "pair");
|
|
||||||
processType(decObj, "cnt5", "fineDustCnt05", "pair");
|
|
||||||
processType(decObj, "cnt10", "fineDustCnt10", "pair");
|
|
||||||
processType(decObj, "co", "co", "pair");
|
|
||||||
processType(decObj, "co2", "co2", "pairPPM");
|
|
||||||
processType(decObj, "dewpt", "dewpt", "pair");
|
|
||||||
processType(decObj, "humidity", "humidityRelative", "pair");
|
|
||||||
processType(decObj, "humidity_abs", "humidityAbsolute", "pair");
|
|
||||||
processType(decObj, "no2", "no2", "pair");
|
|
||||||
processType(decObj, "o3", "o3", "pair");
|
|
||||||
processType(decObj, "oxygen", "o2", "pair");
|
|
||||||
processType(decObj, "pm1", "fineDustConc01", "pair");
|
|
||||||
processType(decObj, "pm2_5", "fineDustConc02_5", "pair");
|
|
||||||
processType(decObj, "pm10", "fineDustConc10", "pair");
|
|
||||||
processType(decObj, "pressure", "pressure", "pair");
|
|
||||||
processType(decObj, "so2", "so2", "pair");
|
|
||||||
processType(decObj, "sound", "sound", "pairDB");
|
|
||||||
processType(decObj, "temperature", "temperature", "pair");
|
|
||||||
// We have two places where the Device ID is delivered: with the measurement data and
|
|
||||||
// with the configuration.
|
|
||||||
// We take the info from the configuration and show it as a property, so we don't need
|
|
||||||
// something like processType(decObj, "DeviceID", "DeviceID", "string") at this moment. We leave
|
|
||||||
// this as a reminder in case for some reason it will be needed in future, e.g. when an air-Q
|
|
||||||
// device also sends data from other devices (then with another Device ID)
|
|
||||||
processType(decObj, "Status", "status", "string");
|
|
||||||
processType(decObj, "TypPS", "avgFineDustSize", "number");
|
|
||||||
processType(decObj, "dCO2dt", "dCO2dt", "number");
|
|
||||||
processType(decObj, "dHdt", "dHdt", "number");
|
|
||||||
processType(decObj, "door_event", "doorEvent", "number");
|
|
||||||
processType(decObj, "health", "health", "number");
|
|
||||||
processType(decObj, "measuretime", "measureTime", "number");
|
|
||||||
processType(decObj, "performance", "performance", "number");
|
|
||||||
processType(decObj, "timestamp", "timestamp", "datetime");
|
|
||||||
processType(decObj, "uptime", "uptime", "numberTimePeriod");
|
|
||||||
processType(decObj, "tvoc", "tvoc", "pairPPB");
|
|
||||||
} else {
|
|
||||||
logger.warn("The air-Q data could not be extracted from this string: {}", decEl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("air-Q - airqHandler - polldata.run(): Error while retrieving air-Q data: {}", toString());
|
JsonObject decObj = decEl.getAsJsonObject();
|
||||||
|
logger.trace("air-Q - airqHandler - run(): decObj={}, jsonAnswer={}", decObj, jsonAnswer);
|
||||||
|
// 'bat' is a field that is already delivered by air-Q but as
|
||||||
|
// there are no air-Q devices which are powered with batteries
|
||||||
|
// it is obsolete at this moment. We implemented the code anyway
|
||||||
|
// to make it easier to add afterwords, but for the moment it is not applicable.
|
||||||
|
// processType(decObj, "bat", "battery", "pair");
|
||||||
|
processType(decObj, "cnt0_3", "fineDustCnt00_3", "pair");
|
||||||
|
processType(decObj, "cnt0_5", "fineDustCnt00_5", "pair");
|
||||||
|
processType(decObj, "cnt1", "fineDustCnt01", "pair");
|
||||||
|
processType(decObj, "cnt2_5", "fineDustCnt02_5", "pair");
|
||||||
|
processType(decObj, "cnt5", "fineDustCnt05", "pair");
|
||||||
|
processType(decObj, "cnt10", "fineDustCnt10", "pair");
|
||||||
|
processType(decObj, "co", "co", "pair");
|
||||||
|
processType(decObj, "co2", "co2", "pairPPM");
|
||||||
|
processType(decObj, "dewpt", "dewpt", "pair");
|
||||||
|
processType(decObj, "humidity", "humidityRelative", "pair");
|
||||||
|
processType(decObj, "humidity_abs", "humidityAbsolute", "pair");
|
||||||
|
processType(decObj, "no2", "no2", "pair");
|
||||||
|
processType(decObj, "o3", "o3", "pair");
|
||||||
|
processType(decObj, "oxygen", "o2", "pair");
|
||||||
|
processType(decObj, "pm1", "fineDustConc01", "pair");
|
||||||
|
processType(decObj, "pm2_5", "fineDustConc02_5", "pair");
|
||||||
|
processType(decObj, "pm10", "fineDustConc10", "pair");
|
||||||
|
processType(decObj, "pressure", "pressure", "pair");
|
||||||
|
processType(decObj, "so2", "so2", "pair");
|
||||||
|
processType(decObj, "sound", "sound", "pairDB");
|
||||||
|
processType(decObj, "temperature", "temperature", "pair");
|
||||||
|
// We have two places where the Device ID is delivered: with the measurement data and
|
||||||
|
// with the configuration.
|
||||||
|
// We take the info from the configuration and show it as a property, so we don't need
|
||||||
|
// something like processType(decObj, "DeviceID", "DeviceID", "string") at this moment. We leave
|
||||||
|
// this as a reminder in case for some reason it will be needed in future, e.g. when an air-Q
|
||||||
|
// device also sends data from other devices (then with another Device ID)
|
||||||
|
processType(decObj, "Status", "status", "string");
|
||||||
|
processType(decObj, "TypPS", "avgFineDustSize", "number");
|
||||||
|
processType(decObj, "dCO2dt", "dCO2dt", "number");
|
||||||
|
processType(decObj, "dHdt", "dHdt", "number");
|
||||||
|
processType(decObj, "door_event", "doorEvent", "number");
|
||||||
|
processType(decObj, "health", "health", "number");
|
||||||
|
processType(decObj, "measuretime", "measureTime", "number");
|
||||||
|
processType(decObj, "performance", "performance", "number");
|
||||||
|
processType(decObj, "timestamp", "timestamp", "datetime");
|
||||||
|
processType(decObj, "uptime", "uptime", "numberTimePeriod");
|
||||||
|
processType(decObj, "tvoc", "tvoc", "pairPPB");
|
||||||
|
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
} catch (AirqPasswordIncorrectException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device password incorrect");
|
||||||
|
} catch (AirqException e) {
|
||||||
|
String causeMessage = "";
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
if (cause != null) {
|
||||||
|
causeMessage = cause.getClass().getSimpleName() + ": " + cause.getMessage() + ": ";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, causeMessage + e.getMessage());
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||||
|
"Syntax error while parsing response from device");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,67 +517,59 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
try {
|
try {
|
||||||
String url = "http://" + config.ipAddress + "/config";
|
String url = "http://" + config.ipAddress + "/config";
|
||||||
res = getData(url, "GET", null);
|
res = getData(url, "GET", null);
|
||||||
if (res != null) {
|
String jsontext = res.getBody();
|
||||||
String jsontext = res.getBody();
|
logger.trace("air-Q - airqHandler - getConfigData(): Result from getBody() is {} with body={}", res,
|
||||||
logger.trace("air-Q - airqHandler - getConfigData(): Result from getBody() is {} with body={}", res,
|
res.getBody());
|
||||||
res.getBody());
|
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
||||||
JsonElement ans = gson.fromJson(jsontext, JsonElement.class);
|
if (ans == null) {
|
||||||
if (ans != null) {
|
throw new AirqEmptyResonseException();
|
||||||
JsonObject jsonObj = ans.getAsJsonObject();
|
|
||||||
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
|
||||||
if (jsonAnswer != null) {
|
|
||||||
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
|
||||||
if (decEl != null) {
|
|
||||||
JsonObject decObj = decEl.getAsJsonObject();
|
|
||||||
logger.debug("air-Q - airqHandler - getConfigData(): decObj={}", decObj);
|
|
||||||
processType(decObj, "Wifi", "wifi", "boolean");
|
|
||||||
processType(decObj, "WLANssid", "ssid", "arr");
|
|
||||||
processType(decObj, "pass", "password", "string");
|
|
||||||
processType(decObj, "WifiInfo", "wifiInfo", "boolean");
|
|
||||||
processType(decObj, "TimeServer", "timeServer", "string");
|
|
||||||
processType(decObj, "geopos", "location", "coord");
|
|
||||||
processType(decObj, "NightMode", "", "nightmode");
|
|
||||||
processType(decObj, "devicename", "deviceName", "string");
|
|
||||||
processType(decObj, "RoomType", "roomType", "string");
|
|
||||||
processType(decObj, "logging", "logLevel", "string");
|
|
||||||
processType(decObj, "DeleteKey", "deleteKey", "string");
|
|
||||||
processType(decObj, "FireAlarm", "fireAlarm", "boolean");
|
|
||||||
processType(decObj, "air-Q-Hardware-Version", "hardwareVersion", "property");
|
|
||||||
processType(decObj, "WLAN config", "", "wlan");
|
|
||||||
processType(decObj, "cloudUpload", "cloudUpload", "boolean");
|
|
||||||
processType(decObj, "SecondsMeasurementDelay", "averagingRhythm", "number");
|
|
||||||
processType(decObj, "Rejection", "powerFreqSuppression", "string");
|
|
||||||
processType(decObj, "air-Q-Software-Version", "softwareVersion", "property");
|
|
||||||
processType(decObj, "sensors", "sensorList", "proparr");
|
|
||||||
processType(decObj, "AutoDriftCompensation", "autoDriftCompensation", "boolean");
|
|
||||||
processType(decObj, "AutoUpdate", "autoUpdate", "boolean");
|
|
||||||
processType(decObj, "AdvancedDataProcessing", "advancedDataProcessing", "boolean");
|
|
||||||
processType(decObj, "Industry", "Industry", "property");
|
|
||||||
processType(decObj, "ppm&ppb", "ppm_and_ppb", "boolean");
|
|
||||||
processType(decObj, "GasAlarm", "gasAlarm", "boolean");
|
|
||||||
processType(decObj, "id", "id", "property");
|
|
||||||
processType(decObj, "SoundInfo", "soundPressure", "boolean");
|
|
||||||
processType(decObj, "AlarmForwarding", "alarmForwarding", "boolean");
|
|
||||||
processType(decObj, "usercalib", "userCalib", "calib");
|
|
||||||
processType(decObj, "InitialCalFinished", "initialCalFinished", "boolean");
|
|
||||||
processType(decObj, "Averaging", "averaging", "boolean");
|
|
||||||
processType(decObj, "SensorInfo", "sensorInfo", "property");
|
|
||||||
processType(decObj, "ErrorBars", "errorBars", "boolean");
|
|
||||||
processType(decObj, "warmup-phase", "warmupPhase", "boolean");
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
|
|
||||||
decEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"air-Q - airqHandler - getConfigData(): The air-Q data could not be extracted from this string: {}",
|
|
||||||
ans);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("air-Q - airqHandler - getConfigData(): Error in processConfigData(): {}", e.toString());
|
JsonObject jsonObj = ans.getAsJsonObject();
|
||||||
|
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||||
|
JsonElement decEl = gson.fromJson(jsonAnswer, JsonElement.class);
|
||||||
|
if (decEl == null) {
|
||||||
|
throw new AirqEmptyResonseException();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject decObj = decEl.getAsJsonObject();
|
||||||
|
logger.trace("air-Q - airqHandler - getConfigData(): decObj={}", decObj);
|
||||||
|
processType(decObj, "Wifi", "wifi", "boolean");
|
||||||
|
processType(decObj, "WLANssid", "ssid", "arr");
|
||||||
|
processType(decObj, "pass", "password", "string");
|
||||||
|
processType(decObj, "WifiInfo", "wifiInfo", "boolean");
|
||||||
|
processType(decObj, "TimeServer", "timeServer", "string");
|
||||||
|
processType(decObj, "geopos", "location", "coord");
|
||||||
|
processType(decObj, "NightMode", "", "nightmode");
|
||||||
|
processType(decObj, "devicename", "deviceName", "string");
|
||||||
|
processType(decObj, "RoomType", "roomType", "string");
|
||||||
|
processType(decObj, "logging", "logLevel", "string");
|
||||||
|
processType(decObj, "DeleteKey", "deleteKey", "string");
|
||||||
|
processType(decObj, "FireAlarm", "fireAlarm", "boolean");
|
||||||
|
processType(decObj, "air-Q-Hardware-Version", "hardwareVersion", "property");
|
||||||
|
processType(decObj, "WLAN config", "", "wlan");
|
||||||
|
processType(decObj, "cloudUpload", "cloudUpload", "boolean");
|
||||||
|
processType(decObj, "SecondsMeasurementDelay", "averagingRhythm", "number");
|
||||||
|
processType(decObj, "Rejection", "powerFreqSuppression", "string");
|
||||||
|
processType(decObj, "air-Q-Software-Version", "softwareVersion", "property");
|
||||||
|
processType(decObj, "sensors", "sensorList", "proparr");
|
||||||
|
processType(decObj, "AutoDriftCompensation", "autoDriftCompensation", "boolean");
|
||||||
|
processType(decObj, "AutoUpdate", "autoUpdate", "boolean");
|
||||||
|
processType(decObj, "AdvancedDataProcessing", "advancedDataProcessing", "boolean");
|
||||||
|
processType(decObj, "Industry", "Industry", "property");
|
||||||
|
processType(decObj, "ppm&ppb", "ppm_and_ppb", "boolean");
|
||||||
|
processType(decObj, "GasAlarm", "gasAlarm", "boolean");
|
||||||
|
processType(decObj, "id", "id", "property");
|
||||||
|
processType(decObj, "SoundInfo", "soundPressure", "boolean");
|
||||||
|
processType(decObj, "AlarmForwarding", "alarmForwarding", "boolean");
|
||||||
|
processType(decObj, "usercalib", "userCalib", "calib");
|
||||||
|
processType(decObj, "InitialCalFinished", "initialCalFinished", "boolean");
|
||||||
|
processType(decObj, "Averaging", "averaging", "boolean");
|
||||||
|
processType(decObj, "SensorInfo", "sensorInfo", "property");
|
||||||
|
processType(decObj, "ErrorBars", "errorBars", "boolean");
|
||||||
|
processType(decObj, "warmup-phase", "warmupPhase", "boolean");
|
||||||
|
} catch (AirqException | JsonSyntaxException e) {
|
||||||
|
logger.warn("Failed to retrieve configuration: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -782,24 +755,28 @@ public class AirqHandler extends BaseThingHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void changeSettings(JsonObject jsonchange) {
|
private void changeSettings(JsonObject jsonchange) {
|
||||||
String jsoncmd = jsonchange.toString();
|
try {
|
||||||
logger.trace("air-Q - airqHandler - changeSettings(): called with jsoncmd={}", jsoncmd);
|
String jsoncmd = jsonchange.toString();
|
||||||
Result res = null;
|
logger.trace("air-Q - airqHandler - changeSettings(): called with jsoncmd={}", jsoncmd);
|
||||||
String url = "http://" + config.ipAddress + "/config";
|
Result res;
|
||||||
String jsonbody = encrypt(jsoncmd.getBytes(StandardCharsets.UTF_8), config.password);
|
String url = "http://" + config.ipAddress + "/config";
|
||||||
String fullbody = "request=" + jsonbody;
|
String jsonbody = encrypt(jsoncmd.getBytes(StandardCharsets.UTF_8), config.password);
|
||||||
logger.trace("air-Q - airqHandler - changeSettings(): doing call to url={}, method=POST, body={}", url,
|
String fullbody = "request=" + jsonbody;
|
||||||
fullbody);
|
logger.trace("air-Q - airqHandler - changeSettings(): doing call to url={}, method=POST, body={}", url,
|
||||||
res = getData(url, "POST", fullbody);
|
fullbody);
|
||||||
if (res != null) {
|
res = getData(url, "POST", fullbody);
|
||||||
JsonElement ans = gson.fromJson(res.getBody(), JsonElement.class);
|
JsonElement ans = gson.fromJson(res.getBody(), JsonElement.class);
|
||||||
if (ans != null) {
|
|
||||||
JsonObject jsonObj = ans.getAsJsonObject();
|
if (ans == null) {
|
||||||
String jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
throw new AirqEmptyResonseException();
|
||||||
logger.trace("air-Q - airqHandler - changeSettings(): call returned {}", jsonAnswer);
|
|
||||||
} else {
|
|
||||||
logger.warn("The air-Q data could not be extracted from this string: {}", ans);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonObject jsonObj = ans.getAsJsonObject();
|
||||||
|
String jsonAnswer;
|
||||||
|
jsonAnswer = decrypt(jsonObj.get("content").getAsString().getBytes(), config.password);
|
||||||
|
logger.trace("air-Q - airqHandler - changeSettings(): call returned {}", jsonAnswer);
|
||||||
|
} catch (AirqException e) {
|
||||||
|
logger.warn("Failed to change settings", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2024 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.airq.internal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for handling a wrong password.
|
||||||
|
*
|
||||||
|
* @author Fabian Wolter - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class AirqPasswordIncorrectException extends AirqException {
|
||||||
|
private static final long serialVersionUID = 1423144673651821622L;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user