added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
38
bundles/org.openhab.binding.foobot/.classpath
Normal file
38
bundles/org.openhab.binding.foobot/.classpath
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.foobot/.project
Normal file
23
bundles/org.openhab.binding.foobot/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.foobot</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.foobot/NOTICE
Normal file
13
bundles/org.openhab.binding.foobot/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
71
bundles/org.openhab.binding.foobot/README.md
Normal file
71
bundles/org.openhab.binding.foobot/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Foobot Binding
|
||||
|
||||
This binding fetches the Indoor Air Quality data of each of your Foobot devices from the Foobot cloud service.
|
||||
|
||||
To use this binding, you first need to [register and get your API key](https://api.foobot.io/apidoc/index.html).
|
||||
The api is rate limited to 200 calls per day. If you need a higher rate limit please contact Foobot.
|
||||
|
||||
## Supported Things
|
||||
|
||||
The binding supports the following things:
|
||||
|
||||
| Thing type | Name
|
||||
|-------------|------------------------------------------
|
||||
| account | The bridge with connection configuration
|
||||
| device | The sensor thing
|
||||
|
||||
## Discovery
|
||||
|
||||
The binding requires you to have a Foobot account and an API key.
|
||||
The discovery process is able to automatically discover all devices associated with your Foobot account.
|
||||
|
||||
## Bridge Configuration
|
||||
|
||||
Bridge has the following configuration parameters:
|
||||
|
||||
| Parameter | Description | Required
|
||||
|------------------|-------------------------------------------------------|----------
|
||||
| apikey | API Key from https://api.foobot.io/apidoc/index.html | Mandatory
|
||||
| username | The e-mail address used to log into the Foobot App | Mandatory
|
||||
| refreshInterval | Refresh interval in minutes, minimal 5 minutes | Optional, the default value is 8 minutes.
|
||||
|
||||
The minimal refresh rate is 5 minutes because the device only sends data every 5 minutes.
|
||||
The default is 8 minutes. This will get you through the day with the default rate limit of 200 calls per day.
|
||||
|
||||
## Channels
|
||||
|
||||
The bridge has one channel:
|
||||
|
||||
| Channel ID | Item Type | Description
|
||||
|----------------------|-----------|-----------------------------------------------
|
||||
| apiKeyLimitRemaining | Number | The remaining number of API requests for today
|
||||
|
||||
|
||||
The AirQuality sensors information that is retrieved is available as these channels:
|
||||
|
||||
| Channel ID | Item Type | Description
|
||||
|-------------------|----------------------|---------------------------------------------
|
||||
| time | DateTime | Last time the sensor data was send to Foobot
|
||||
| pm | Number:Density | Particulate Matter level (ug/m3)
|
||||
| temperature | Number:Temperature | Temperature in Celsius or Fahrenheit
|
||||
| humidity | Number:Dimensionless | Humidity level (%)
|
||||
| co2 | Number:Dimensionless | Carbon diOxide level (ppm)
|
||||
| voc | Number:Dimensionless | Volatile Organic Compounds level (ppb)
|
||||
| gpi | Number:Dimensionless | Global Pollution index (%)
|
||||
|
||||
## Full Example
|
||||
|
||||
demo.things:
|
||||
|
||||
```
|
||||
// Bridge configuration:
|
||||
Bridge foobot:account:myfoobotaccount "Foobot Account" [apiKey="XXXXXX", username="XXXXXX", refreshInterval=8] {
|
||||
Things:
|
||||
device myfoobot "Foobot sensor" [uuid="XXXXXXXXXXXXXXXX"]
|
||||
```
|
||||
|
||||
demo.items:
|
||||
|
||||
```
|
||||
Number:Temperature Temperature "Temperature" <temperature> { channel="foobot:myfoobotaccount:device:myfoobot:temperature" }
|
||||
```
|
||||
17
bundles/org.openhab.binding.foobot/pom.xml
Normal file
17
bundles/org.openhab.binding.foobot/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.foobot</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Foobot Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.foobot-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-foobot" description="Foobot Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.foobot/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal;
|
||||
|
||||
import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotDevice;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotJsonData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Connector class communicating with Foobot api and parsing returned json.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotApiConnector {
|
||||
|
||||
public static final String API_RATE_LIMIT_EXCEEDED_MESSAGE = "Api rate limit exceeded";
|
||||
public static final int API_RATE_LIMIT_EXCEEDED = -2;
|
||||
|
||||
private static final int UNKNOWN_REMAINING = -1;
|
||||
private static final String HEADER_X_API_KEY_TOKEN = "X-API-KEY-TOKEN";
|
||||
private static final String HEADER_X_API_KEY_LIMIT_REMAINING = "x-api-key-limit-remaining";
|
||||
private static final int REQUEST_TIMEOUT_SECONDS = 3;
|
||||
private static final Gson GSON = new Gson();
|
||||
private static final Type FOOTBOT_DEVICE_LIST_TYPE = new TypeToken<ArrayList<FoobotDevice>>() {
|
||||
}.getType();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FoobotApiConnector.class);
|
||||
|
||||
private @Nullable HttpClient httpClient;
|
||||
private String apiKey = "";
|
||||
private int apiKeyLimitRemaining = UNKNOWN_REMAINING;
|
||||
|
||||
public void setHttpClient(@Nullable HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the last known api remaining limit or -1 if not known.
|
||||
*/
|
||||
public int getApiKeyLimitRemaining() {
|
||||
return apiKeyLimitRemaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of associated devices with the given username from the foobot api.
|
||||
*
|
||||
* @param username to get the associated devices for
|
||||
* @return List of devices
|
||||
* @throws FoobotApiException in case there was a problem communicating or parsing the response
|
||||
*/
|
||||
public synchronized List<FoobotDevice> getAssociatedDevices(String username) throws FoobotApiException {
|
||||
try {
|
||||
final String url = URL_TO_FETCH_DEVICES.replace("%username%",
|
||||
URLEncoder.encode(username, StandardCharsets.UTF_8.toString()));
|
||||
logger.debug("URL = {}", url);
|
||||
|
||||
return GSON.fromJson(request(url, apiKey), FOOTBOT_DEVICE_LIST_TYPE);
|
||||
} catch (JsonParseException | UnsupportedEncodingException e) {
|
||||
throw new FoobotApiException(0, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sensor data for the device with the given uuid from the foobot api.
|
||||
*
|
||||
* @param uuid of the device to get the sensor data for
|
||||
* @return sensor data of the device
|
||||
* @throws FoobotApiException in case there was a problem communicating or parsing the response
|
||||
*/
|
||||
public synchronized @Nullable FoobotJsonData getSensorData(String uuid) throws FoobotApiException {
|
||||
try {
|
||||
final String url = URL_TO_FETCH_SENSOR_DATA.replace("%uuid%",
|
||||
URLEncoder.encode(uuid, StandardCharsets.UTF_8.toString()));
|
||||
logger.debug("URL = {}", url);
|
||||
|
||||
return GSON.fromJson(request(url, apiKey), FoobotJsonData.class);
|
||||
} catch (JsonParseException | UnsupportedEncodingException e) {
|
||||
throw new FoobotApiException(0, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected String request(String url, String apiKey) throws FoobotApiException {
|
||||
apiKeyLimitRemaining = UNKNOWN_REMAINING;
|
||||
if (httpClient == null) {
|
||||
logger.debug("No http connection possible: httpClient == null");
|
||||
throw new FoobotApiException(0, "No http connection possible");
|
||||
}
|
||||
final Request request = httpClient.newRequest(url).timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
request.header(HttpHeader.ACCEPT, "application/json");
|
||||
request.header(HttpHeader.ACCEPT_ENCODING, StandardCharsets.UTF_8.name());
|
||||
request.header(HEADER_X_API_KEY_TOKEN, apiKey);
|
||||
final ContentResponse response;
|
||||
|
||||
try {
|
||||
response = request.send();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FoobotApiException(0, e.getMessage());
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new FoobotApiException(0, e.getMessage());
|
||||
}
|
||||
final String content = response.getContentAsString();
|
||||
|
||||
logger.trace("Foobot content = {}", content);
|
||||
logger.debug("Foobot response = {}", response);
|
||||
setApiKeyLimitRemaining(response);
|
||||
switch (response.getStatus()) {
|
||||
case HttpStatus.FORBIDDEN_403:
|
||||
throw new FoobotApiException(response.getStatus(),
|
||||
"Access denied. Did you set the correct api-key and/or username?");
|
||||
case HttpStatus.TOO_MANY_REQUESTS_429:
|
||||
apiKeyLimitRemaining = API_RATE_LIMIT_EXCEEDED;
|
||||
throw new FoobotApiException(response.getStatus(), API_RATE_LIMIT_EXCEEDED_MESSAGE);
|
||||
case HttpStatus.OK_200:
|
||||
if (StringUtils.trimToNull(content) == null) {
|
||||
throw new FoobotApiException(0, "No data returned");
|
||||
}
|
||||
return content;
|
||||
default:
|
||||
logger.trace("Foobot returned status '{}', reason: {}, content = {}", response.getStatus(),
|
||||
response.getReason(), content);
|
||||
throw new FoobotApiException(response.getStatus(), response.getReason());
|
||||
}
|
||||
}
|
||||
|
||||
private void setApiKeyLimitRemaining(ContentResponse response) {
|
||||
final HttpField field = response.getHeaders().getField(HEADER_X_API_KEY_LIMIT_REMAINING);
|
||||
|
||||
if (field != null) {
|
||||
apiKeyLimitRemaining = field.getIntValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception thrown when problems occur with obtaining data for the foobot api.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotApiException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public FoobotApiException(final int status, final String message) {
|
||||
super(String.format("%s (code: %s)", message, status));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link FoobotBinding} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Divya Chauhan - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotBindingConstants {
|
||||
|
||||
// List Foobot URLs
|
||||
private static final String URL_FOOBOT_API_V2 = "https://api.foobot.io/v2/";
|
||||
public static final String URL_TO_FETCH_DEVICES = URL_FOOBOT_API_V2 + "owner/%username%/device/";
|
||||
public static final String URL_TO_FETCH_SENSOR_DATA = URL_FOOBOT_API_V2 + "device/%uuid%/datapoint/0/last/0/";
|
||||
|
||||
private static final String BINDING_ID = "foobot";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID BRIDGE_TYPE_FOOBOTACCOUNT = new ThingTypeUID(BINDING_ID, "account");
|
||||
public static final ThingTypeUID THING_TYPE_FOOBOT = new ThingTypeUID(BINDING_ID, "device");
|
||||
|
||||
// Bridge channel
|
||||
public static final String CHANNEL_APIKEY_LIMIT_REMAINING = "apiKeyLimitRemaining";
|
||||
|
||||
// List Foobot configuration attributes
|
||||
public static final String CONFIG_APIKEY = "apiKey";
|
||||
public static final String CONFIG_UUID = "uuid";
|
||||
public static final String CONFIG_MAC = "mac";
|
||||
|
||||
public static final String PROPERTY_NAME = "name";
|
||||
|
||||
public static final int MINIMUM_REFRESH_PERIOD_MINUTES = 5;
|
||||
public static final int DEFAULT_REFRESH_PERIOD_MINUTES = 8;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal;
|
||||
|
||||
import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.foobot.internal.handler.FoobotAccountHandler;
|
||||
import org.openhab.binding.foobot.internal.handler.FoobotDeviceHandler;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link FoobotHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Divya Chauhan - Initial contribution
|
||||
* @author George Katsis - Add Bridge thing type
|
||||
* @author Hilbrand Bouwkamp - Completed implementation
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.foobot")
|
||||
@NonNullByDefault
|
||||
public class FoobotHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections
|
||||
.unmodifiableSet(Stream.of(BRIDGE_TYPE_FOOBOTACCOUNT, THING_TYPE_FOOBOT).collect(Collectors.toSet()));
|
||||
|
||||
public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_FOOBOT);
|
||||
|
||||
private final FoobotApiConnector connector = new FoobotApiConnector();
|
||||
|
||||
@Activate
|
||||
public FoobotHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
|
||||
connector.setHttpClient(httpClientFactory.getCommonHttpClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_FOOBOT)) {
|
||||
return new FoobotDeviceHandler(thing, connector);
|
||||
} else if (thingTypeUID.equals(BRIDGE_TYPE_FOOBOTACCOUNT)) {
|
||||
return new FoobotAccountHandler((Bridge) thing, connector);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link FoobotAccountConfiguration} class contains fields mapping bridge configuration parameters.
|
||||
*
|
||||
* @author George Katsis - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotAccountConfiguration {
|
||||
|
||||
public String apiKey = "";
|
||||
public String username = "";
|
||||
public int refreshInterval;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.discovery;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiException;
|
||||
import org.openhab.binding.foobot.internal.FoobotBindingConstants;
|
||||
import org.openhab.binding.foobot.internal.FoobotHandlerFactory;
|
||||
import org.openhab.binding.foobot.internal.handler.FoobotAccountHandler;
|
||||
import org.openhab.binding.foobot.internal.handler.FoobotDeviceHandler;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotDevice;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FoobotAccountDiscoveryService} is responsible for starting the discovery procedure
|
||||
* that retrieves Foobot account and imports all registered Foobot devices.
|
||||
*
|
||||
* @author George Katsis - Initial contribution
|
||||
* @author Hilbrand Bouwkamp - Completed implementation
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotAccountDiscoveryService extends AbstractDiscoveryService
|
||||
implements DiscoveryService, ThingHandlerService {
|
||||
|
||||
private static final int TIMEOUT_SECONDS = 5;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FoobotAccountDiscoveryService.class);
|
||||
|
||||
private @Nullable FoobotAccountHandler handler;
|
||||
private @NonNullByDefault({}) ThingUID bridgeUID;
|
||||
|
||||
public FoobotAccountDiscoveryService() {
|
||||
super(FoobotHandlerFactory.DISCOVERABLE_THING_TYPE_UIDS, TIMEOUT_SECONDS, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
scheduler.execute(this::retrieveFoobots);
|
||||
}
|
||||
|
||||
private void retrieveFoobots() {
|
||||
if (handler == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final List<FoobotDeviceHandler> footbotHandlers = handler.getFootbotHandlers();
|
||||
|
||||
handler.getDeviceList().stream()
|
||||
.filter(d -> !footbotHandlers.stream().anyMatch(h -> h.getUuid().equals(d.getUuid())))
|
||||
.forEach(this::addThing);
|
||||
} catch (final FoobotApiException e) {
|
||||
logger.debug("Footbot Api connection failed: {}", e.getMessage(), e);
|
||||
logger.warn("Discovering new footbot devices failed: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
private void addThing(final FoobotDevice foobot) {
|
||||
logger.debug("Adding new Foobot '{}' with uuid: {}", foobot.getName(), foobot.getUuid());
|
||||
|
||||
final ThingUID thingUID = new ThingUID(FoobotBindingConstants.THING_TYPE_FOOBOT, bridgeUID, foobot.getUuid());
|
||||
final Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(Thing.PROPERTY_SERIAL_NUMBER, foobot.getUuid());
|
||||
properties.put(FoobotBindingConstants.CONFIG_UUID, foobot.getUuid());
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, foobot.getMac());
|
||||
properties.put(FoobotBindingConstants.PROPERTY_NAME, foobot.getName());
|
||||
|
||||
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withBridge(bridgeUID).withProperties(properties)
|
||||
.withLabel(foobot.getName()).withRepresentationProperty(foobot.getUuid()).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable final ThingHandler handler) {
|
||||
if (handler instanceof FoobotAccountHandler) {
|
||||
this.handler = (FoobotAccountHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.handler;
|
||||
|
||||
import static org.openhab.binding.foobot.internal.FoobotBindingConstants.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiConnector;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiException;
|
||||
import org.openhab.binding.foobot.internal.FoobotBindingConstants;
|
||||
import org.openhab.binding.foobot.internal.config.FoobotAccountConfiguration;
|
||||
import org.openhab.binding.foobot.internal.discovery.FoobotAccountDiscoveryService;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotDevice;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Bridge handler to manage Foobot Account
|
||||
*
|
||||
* @author George Katsis - Initial contribution
|
||||
* @author Hilbrand Bouwkamp - Completed implementation
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotAccountHandler extends BaseBridgeHandler {
|
||||
|
||||
/*
|
||||
* Set the exact interval a little lower to compensate for the time it takes to get the new data.
|
||||
*/
|
||||
private static final long DEVICES_INTERVAL_MINUTES = Duration.ofDays(1).minus(Duration.ofMinutes(1)).toMinutes();
|
||||
private static final Duration SENSOR_INTERVAL_OFFSET_SECONDS = Duration.ofSeconds(15);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FoobotAccountHandler.class);
|
||||
|
||||
private final FoobotApiConnector connector;
|
||||
|
||||
private String username = "";
|
||||
private int refreshInterval;
|
||||
private @Nullable ScheduledFuture<?> refreshDeviceListJob;
|
||||
private @Nullable ScheduledFuture<?> refreshSensorsJob;
|
||||
private @NonNullByDefault({}) ExpiringCache<List<FoobotDeviceHandler>> dataCache;
|
||||
|
||||
public FoobotAccountHandler(Bridge bridge, FoobotApiConnector connector) {
|
||||
super(bridge);
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(FoobotAccountDiscoveryService.class);
|
||||
}
|
||||
|
||||
public List<FoobotDevice> getDeviceList() throws FoobotApiException {
|
||||
return connector.getAssociatedDevices(username);
|
||||
}
|
||||
|
||||
public int getRefreshInterval() {
|
||||
return refreshInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
final FoobotAccountConfiguration accountConfig = getConfigAs(FoobotAccountConfiguration.class);
|
||||
final List<String> missingParams = new ArrayList<>();
|
||||
|
||||
if (StringUtils.trimToNull(accountConfig.apiKey) == null) {
|
||||
missingParams.add("'apikey'");
|
||||
}
|
||||
if (StringUtils.trimToNull(accountConfig.username) == null) {
|
||||
missingParams.add("'username'");
|
||||
}
|
||||
|
||||
if (!missingParams.isEmpty()) {
|
||||
final boolean oneParam = missingParams.size() == 1;
|
||||
final String errorMsg = String.format(
|
||||
"Parameter%s [%s] %s mandatory and must be configured and not be empty", oneParam ? "" : "s",
|
||||
StringUtils.join(missingParams, ", "), oneParam ? "is" : "are");
|
||||
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
|
||||
return;
|
||||
}
|
||||
username = accountConfig.username;
|
||||
connector.setApiKey(accountConfig.apiKey);
|
||||
refreshInterval = accountConfig.refreshInterval;
|
||||
if (this.refreshInterval < MINIMUM_REFRESH_PERIOD_MINUTES) {
|
||||
logger.warn(
|
||||
"Refresh interval time [{}] is not valid. Refresh interval time must be at least {} minutes. Setting to {} minutes",
|
||||
accountConfig.refreshInterval, MINIMUM_REFRESH_PERIOD_MINUTES, DEFAULT_REFRESH_PERIOD_MINUTES);
|
||||
refreshInterval = DEFAULT_REFRESH_PERIOD_MINUTES;
|
||||
}
|
||||
logger.debug("Foobot Account bridge starting... user: {}, refreshInterval: {}", accountConfig.username,
|
||||
refreshInterval);
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, "Wait to get associated devices");
|
||||
|
||||
dataCache = new ExpiringCache<>(Duration.ofMinutes(refreshInterval), this::retrieveDeviceList);
|
||||
this.refreshDeviceListJob = scheduler.scheduleWithFixedDelay(this::refreshDeviceList, 0,
|
||||
DEVICES_INTERVAL_MINUTES, TimeUnit.MINUTES);
|
||||
this.refreshSensorsJob = scheduler.scheduleWithFixedDelay(this::refreshSensors, 0,
|
||||
Duration.ofMinutes(refreshInterval).minus(SENSOR_INTERVAL_OFFSET_SECONDS).getSeconds(),
|
||||
TimeUnit.SECONDS);
|
||||
|
||||
logger.debug("Foobot account bridge handler started.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
if (command instanceof RefreshType) {
|
||||
refreshDeviceList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Dispose {}", getThing().getUID());
|
||||
|
||||
final ScheduledFuture<?> refreshDeviceListJob = this.refreshDeviceListJob;
|
||||
if (refreshDeviceListJob != null) {
|
||||
refreshDeviceListJob.cancel(true);
|
||||
this.refreshDeviceListJob = null;
|
||||
}
|
||||
final ScheduledFuture<?> refreshSensorsJob = this.refreshSensorsJob;
|
||||
if (refreshSensorsJob != null) {
|
||||
refreshSensorsJob.cancel(true);
|
||||
this.refreshSensorsJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of devices and updates the properties of the devices. This method is called by the cache to
|
||||
* update the cache data.
|
||||
*
|
||||
* @return List of retrieved devices
|
||||
*/
|
||||
private List<FoobotDeviceHandler> retrieveDeviceList() {
|
||||
logger.debug("Refreshing sensors for {}", getThing().getUID());
|
||||
final List<FoobotDeviceHandler> footbotHandlers = getFootbotHandlers();
|
||||
|
||||
try {
|
||||
getDeviceList().stream().forEach(d -> {
|
||||
footbotHandlers.stream().filter(h -> h.getUuid().equals(d.getUuid())).findAny()
|
||||
.ifPresent(fh -> fh.handleUpdateProperties(d));
|
||||
});
|
||||
} catch (FoobotApiException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
return footbotHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the devices list
|
||||
*/
|
||||
private void refreshDeviceList() {
|
||||
// This getValue() return value not used here. But if the cache is expired it refreshes the cache.
|
||||
dataCache.getValue();
|
||||
updateRemainingLimitStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
|
||||
if (childHandler instanceof FoobotDeviceHandler) {
|
||||
final String uuid = ((FoobotDeviceHandler) childHandler).getUuid();
|
||||
|
||||
try {
|
||||
getDeviceList().stream().filter(d -> d.getUuid().equals(uuid)).findAny()
|
||||
.ifPresent(fd -> ((FoobotDeviceHandler) childHandler).handleUpdateProperties(fd));
|
||||
} catch (FoobotApiException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the list of associated footbot devices with this bridge.
|
||||
*/
|
||||
public List<FoobotDeviceHandler> getFootbotHandlers() {
|
||||
return getThing().getThings().stream().map(Thing::getHandler).filter(FoobotDeviceHandler.class::isInstance)
|
||||
.map(FoobotDeviceHandler.class::cast).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void refreshSensors() {
|
||||
logger.debug("Refreshing sensors for {}", getThing().getUID());
|
||||
logger.debug("handlers: {}", getFootbotHandlers().size());
|
||||
try {
|
||||
for (FoobotDeviceHandler handler : getFootbotHandlers()) {
|
||||
logger.debug("handler: {}", handler.getUuid());
|
||||
handler.refreshSensors();
|
||||
}
|
||||
if (connector.getApiKeyLimitRemaining() == FoobotApiConnector.API_RATE_LIMIT_EXCEEDED) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
|
||||
FoobotApiConnector.API_RATE_LIMIT_EXCEEDED_MESSAGE);
|
||||
} else if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Error updating sensor data ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRemainingLimitStatus() {
|
||||
final int remaining = connector.getApiKeyLimitRemaining();
|
||||
|
||||
updateState(FoobotBindingConstants.CHANNEL_APIKEY_LIMIT_REMAINING,
|
||||
remaining < 0 ? UnDefType.UNDEF : new DecimalType(remaining));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.handler;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiConnector;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiException;
|
||||
import org.openhab.binding.foobot.internal.FoobotBindingConstants;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotDevice;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotJsonData;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotSensor;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link FoobotDeviceHandler} is responsible for handling commands, which are sent to one of the channels.
|
||||
*
|
||||
* @author Divya Chauhan - Initial contribution
|
||||
* @author George Katsis - Add Bridge thing type
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FoobotDeviceHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FoobotDeviceHandler.class);
|
||||
private final FoobotApiConnector connector;
|
||||
|
||||
private @NonNullByDefault({}) ExpiringCache<FoobotJsonData> dataCache;
|
||||
private String uuid = "";
|
||||
|
||||
public FoobotDeviceHandler(final Thing thing, final FoobotApiConnector connector) {
|
||||
super(thing);
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
final FoobotJsonData sensorData = dataCache.getValue();
|
||||
|
||||
if (sensorData != null) {
|
||||
updateState(channelUID, sensorDataToState(channelUID.getId(), sensorData));
|
||||
}
|
||||
} else {
|
||||
logger.debug("The Foobot binding is read-only and can not handle command {}", command);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the uuid associated with this device.
|
||||
*/
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("Initializing Foobot handler.");
|
||||
uuid = (String) getConfig().get(FoobotBindingConstants.CONFIG_UUID);
|
||||
|
||||
if (StringUtils.trimToNull(uuid) == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Parameter 'uuid' is mandatory and must be configured");
|
||||
return;
|
||||
}
|
||||
final FoobotAccountHandler bridgeHandler = getBridgeHandler();
|
||||
final int refreshInterval = bridgeHandler == null ? FoobotBindingConstants.DEFAULT_REFRESH_PERIOD_MINUTES
|
||||
: bridgeHandler.getRefreshInterval();
|
||||
|
||||
dataCache = new ExpiringCache<>(Duration.ofMinutes(refreshInterval), this::retrieveSensorData);
|
||||
scheduler.execute(this::refreshSensors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the thing properties as retrieved by the bridge.
|
||||
*
|
||||
* @param foobot device parameters.
|
||||
*/
|
||||
public void handleUpdateProperties(final FoobotDevice foobot) {
|
||||
final Map<String, String> properties = editProperties();
|
||||
|
||||
properties.put(Thing.PROPERTY_MAC_ADDRESS, foobot.getMac());
|
||||
properties.put(FoobotBindingConstants.PROPERTY_NAME, foobot.getName());
|
||||
updateProperties(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the footbot api to retrieve the sensor data. Sets thing offline in case of errors.
|
||||
*
|
||||
* @return returns the retrieved sensor data or null if no data or an error occurred.
|
||||
*/
|
||||
private @Nullable FoobotJsonData retrieveSensorData() {
|
||||
logger.debug("Refresh sensor data for: {}", uuid);
|
||||
FoobotJsonData sensorData = null;
|
||||
|
||||
try {
|
||||
sensorData = connector.getSensorData(uuid);
|
||||
if (sensorData == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "No sensor data received");
|
||||
return sensorData;
|
||||
}
|
||||
} catch (FoobotApiException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return null;
|
||||
} catch (RuntimeException e) {
|
||||
logger.debug("Error requesting sensor data: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
if (getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
final FoobotAccountHandler bridgeHandler = getBridgeHandler();
|
||||
|
||||
if (bridgeHandler != null) {
|
||||
bridgeHandler.updateRemainingLimitStatus();
|
||||
}
|
||||
return sensorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the device channels.
|
||||
*/
|
||||
public void refreshSensors() {
|
||||
final FoobotJsonData sensorData = dataCache.getValue();
|
||||
|
||||
if (sensorData != null) {
|
||||
for (final Channel channel : getThing().getChannels()) {
|
||||
final ChannelUID channelUid = channel.getUID();
|
||||
|
||||
updateState(channelUid, sensorDataToState(channelUid.getId(), sensorData));
|
||||
}
|
||||
updateTime(sensorData);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTime(final FoobotJsonData sensorData) {
|
||||
final State lastTime = sensorDataToState(FoobotSensor.TIME.getChannelId(), sensorData);
|
||||
|
||||
if (lastTime instanceof DecimalType) {
|
||||
((DecimalType) lastTime).intValue();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Disposing the Foobot handler.");
|
||||
}
|
||||
|
||||
protected State sensorDataToState(final String channelId, final FoobotJsonData data) {
|
||||
final FoobotSensor sensor = FoobotSensor.findSensorByChannelId(channelId);
|
||||
|
||||
if (sensor == null || data.getSensors() == null || data.getDatapoints() == null
|
||||
|| data.getDatapoints().isEmpty()) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
final int sensorIndex = data.getSensors().indexOf(sensor.getDataKey());
|
||||
|
||||
if (sensorIndex == -1) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
final String value = data.getDatapoints().get(0).get(sensorIndex);
|
||||
final String unit = data.getUnits().get(sensorIndex);
|
||||
|
||||
if (value == null) {
|
||||
return UnDefType.UNDEF;
|
||||
} else {
|
||||
final Unit<?> stateUnit = sensor.getUnit(unit);
|
||||
if (sensor == FoobotSensor.TIME) {
|
||||
return new DateTimeType(
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(value)), ZoneId.systemDefault()));
|
||||
} else if (stateUnit == null) {
|
||||
return new DecimalType(value);
|
||||
} else {
|
||||
return new QuantityType(new BigDecimal(value), stateUnit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable FoobotAccountHandler getBridgeHandler() {
|
||||
return getBridge() != null && getBridge().getHandler() instanceof FoobotAccountHandler
|
||||
? (FoobotAccountHandler) getBridge().getHandler()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.json;
|
||||
|
||||
/**
|
||||
* The {@link FoobotDevice} is the Java class used to map the JSON response to the foobot.io request.
|
||||
*
|
||||
* @author Divya Chauhan - Initial contribution
|
||||
* @author George Katsis - Code refactor
|
||||
*/
|
||||
public class FoobotDevice {
|
||||
|
||||
private String uuid;
|
||||
private String mac;
|
||||
private String name;
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public String getMac() {
|
||||
return mac;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.json;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The {@link FoobotJsonData} is responsible for storing the "datapoints" from the foobot.io JSON response
|
||||
*
|
||||
* @author Divya Chauhan - Initial contribution
|
||||
*/
|
||||
public class FoobotJsonData {
|
||||
|
||||
private String uuid;
|
||||
private long start;
|
||||
private long end;
|
||||
private List<String> sensors;
|
||||
private List<String> units;
|
||||
private List<List<String>> datapoints;
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public long getEnd() {
|
||||
return end;
|
||||
}
|
||||
|
||||
public List<String> getSensors() {
|
||||
return sensors;
|
||||
}
|
||||
|
||||
public List<String> getUnits() {
|
||||
return units;
|
||||
}
|
||||
|
||||
public List<List<String>> getDatapoints() {
|
||||
return datapoints;
|
||||
}
|
||||
|
||||
public void setDatapoints(List<List<String>> datapoints) {
|
||||
this.datapoints = datapoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.json;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
|
||||
/**
|
||||
* Enum for all specific sensor data returned by the Foobot device.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum FoobotSensor {
|
||||
TIME("time", "time", null),
|
||||
PM("pm", "pm", SmartHomeUnits.MICROGRAM_PER_CUBICMETRE),
|
||||
TEMPERATURE("temperature", "tmp", "C", SIUnits.CELSIUS, ImperialUnits.FAHRENHEIT),
|
||||
HUMIDITY("humidity", "hum", null),
|
||||
CO2("co2", "co2", SmartHomeUnits.PARTS_PER_MILLION),
|
||||
VOC("voc", "voc", null),
|
||||
GPI("gpi", "allpollu", null);
|
||||
|
||||
private final String channelId;
|
||||
private final String dataKey;
|
||||
private final @Nullable String matchUnit;
|
||||
private final @Nullable Unit<?> unit;
|
||||
private final @Nullable Unit<?> alternativeUnit;
|
||||
|
||||
private static final Map<String, FoobotSensor> CHANNEL_ID_MAP = Stream.of(values())
|
||||
.collect(Collectors.toMap(FoobotSensor::getChannelId, Function.identity()));
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param channelId Id of the thing channel
|
||||
* @param dataKey key of the sensor data in the foobot sensor json data
|
||||
* @param unit Unit of the sensor data or null if no unit specified
|
||||
*/
|
||||
private FoobotSensor(String channelId, String dataKey, @Nullable Unit<?> unit) {
|
||||
this(channelId, dataKey, null, unit, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param channelId Id of the thing channel
|
||||
* @param dataKey key of the sensor data in the foobot sensor json data
|
||||
* @param matchUnit unit string to be matched with the foobot returned unit
|
||||
* @param unit Unit of the sensor data or null if no unit specified
|
||||
* @param alternativeUnit if foobot api unit doesn't match this unit is returned
|
||||
*/
|
||||
private FoobotSensor(String channelId, String dataKey, @Nullable String matchUnit, @Nullable Unit<?> unit,
|
||||
@Nullable Unit<?> alternativeUnit) {
|
||||
this.channelId = channelId;
|
||||
this.dataKey = dataKey;
|
||||
this.matchUnit = matchUnit;
|
||||
this.unit = unit;
|
||||
this.alternativeUnit = alternativeUnit;
|
||||
}
|
||||
|
||||
public static @Nullable FoobotSensor findSensorByChannelId(String channelId) {
|
||||
return CHANNEL_ID_MAP.get(channelId);
|
||||
}
|
||||
|
||||
public String getChannelId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the key of the sensor type as returned by the foobot api
|
||||
*/
|
||||
public String getDataKey() {
|
||||
return dataKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Unit of this sensor data type or null if no unit specified.
|
||||
*
|
||||
* @param unitToMath match the returned unit by the foobot api with the Unit to be returned
|
||||
* @return Unit or null if no unit available for the sensor
|
||||
*/
|
||||
public @Nullable Unit<?> getUnit(String unitToMath) {
|
||||
return matchUnit == null ? unit : (matchUnit.equals(unitToMath) ? unit : alternativeUnit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="foobot" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Foobot</name>
|
||||
<description>Foobot binding allow users to connect to their foobots in home or office and get real-time updates on the
|
||||
Air Quality.</description>
|
||||
<author>Divya Chauhan and George Katsis</author>
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="foobot"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Foobot Account -->
|
||||
<bridge-type id="account">
|
||||
<label>Foobot Account</label>
|
||||
<description>Your Foobot account.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="apiKeyLimitRemaining" typeId="api-key-limit-remaining"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="apiKey" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>API Key</label>
|
||||
<description>You can request your API Key from https://api.foobot.io/apidoc/index.html</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="username" type="text" required="true">
|
||||
<label>Username</label>
|
||||
<description>The e-mail address you use to login to your Foobot account</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="refreshInterval" type="integer" min="5" unit="m">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Specifies the refresh interval in minutes.</description>
|
||||
<default>8</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<!-- Foobot Device -->
|
||||
<thing-type id="device">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="account"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Foobot</label>
|
||||
<description>A Foobot device.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="time" typeId="time"/>
|
||||
<channel id="pm" typeId="pm"/>
|
||||
<channel id="temperature" typeId="temperature"/>
|
||||
<channel id="humidity" typeId="humidity"/>
|
||||
<channel id="co2" typeId="co2"/>
|
||||
<channel id="voc" typeId="voc"/>
|
||||
<channel id="gpi" typeId="gpi"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>uuid</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="uuid" type="text" required="true">
|
||||
<label>UUID</label>
|
||||
<description>The device UUID</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="time">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Readout</label>
|
||||
<description>The last time the sensor data was uploaded to Foobot</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="pm">
|
||||
<item-type>Number:Density</item-type>
|
||||
<label>Particulate Matter</label>
|
||||
<description>Particulate Matter Level</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="humidity">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<description>Humidity Level</description>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" pattern="%d %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="co2">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Carbon Dioxide</label>
|
||||
<description>Carbon dioxide Level</description>
|
||||
<category>CarbonDioxide</category>
|
||||
<state readOnly="true" pattern="%d ppm"/>
|
||||
</channel-type>
|
||||
<channel-type id="voc">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Volatile Compounds</label>
|
||||
<description>Volatile Organic Compounds Level</description>
|
||||
<state readOnly="true" pattern="%d ppb"/>
|
||||
</channel-type>
|
||||
<channel-type id="gpi">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Pollution Index</label>
|
||||
<description>Global Pollution Index Level</description>
|
||||
<state readOnly="true" pattern="%.0f %%"/>
|
||||
</channel-type>
|
||||
<channel-type id="api-key-limit-remaining" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Remaining Api Limit</label>
|
||||
<description>The remaining number of calls that can be made to the api today</description>
|
||||
<state readOnly="true" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.handler;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiConnector;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiException;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotDevice;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FoobotAccountHandler}.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
public class FoobotAccountHandlerTest {
|
||||
|
||||
private @Mock Bridge bridge;
|
||||
private final FoobotApiConnector connector = new FoobotApiConnector() {
|
||||
@Override
|
||||
protected String request(String url, String apiKey) throws FoobotApiException {
|
||||
try (InputStream stream = getClass().getResourceAsStream("../devices.json")) {
|
||||
return IOUtils.toString(stream);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e.getMessage());
|
||||
}
|
||||
};
|
||||
};
|
||||
private final FoobotAccountHandler handler = new FoobotAccountHandler(bridge, connector);
|
||||
|
||||
@Test
|
||||
public void testSensorDataToState() throws IOException, FoobotApiException {
|
||||
final List<FoobotDevice> deviceList = handler.getDeviceList();
|
||||
|
||||
assertFalse("Device list should not return empty", deviceList.isEmpty());
|
||||
assertEquals("1234567890ABCDEF", deviceList.get(0).getUuid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.foobot.internal.handler;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiConnector;
|
||||
import org.openhab.binding.foobot.internal.FoobotApiException;
|
||||
import org.openhab.binding.foobot.internal.json.FoobotJsonData;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.thing.Thing;
|
||||
|
||||
/**
|
||||
* Unit test for {@link FoobotDeviceHandler}.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
public class FoobotDeviceHandlerTest {
|
||||
|
||||
private @Mock Thing thing;
|
||||
private final FoobotApiConnector connector = new FoobotApiConnector() {
|
||||
@Override
|
||||
protected String request(String url, String apiKey) throws FoobotApiException {
|
||||
try (InputStream stream = getClass().getResourceAsStream("../sensors.json")) {
|
||||
return IOUtils.toString(stream);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e.getMessage());
|
||||
}
|
||||
};
|
||||
};
|
||||
private final FoobotDeviceHandler handler = new FoobotDeviceHandler(thing, connector);
|
||||
|
||||
@Test
|
||||
public void testSensorDataToState() throws IOException, FoobotApiException {
|
||||
final FoobotJsonData sensorData = connector.getSensorData("1234");
|
||||
|
||||
assertNotNull("No sensor data read", sensorData);
|
||||
assertEquals(handler.sensorDataToState("temperature", sensorData), new QuantityType(12.345, SIUnits.CELSIUS));
|
||||
assertEquals(handler.sensorDataToState("gpi", sensorData), new DecimalType(5.6789012));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"uuid": "1234567890ABCDEF",
|
||||
"userId": 12345,
|
||||
"mac": "AABBCCDDEEFF",
|
||||
"name": "Foobot 1"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"uuid": "0123456789ABCDEF",
|
||||
"start": 1234567890,
|
||||
"end": 1234567890,
|
||||
"sensors": [
|
||||
"time",
|
||||
"pm",
|
||||
"tmp",
|
||||
"hum",
|
||||
"co2",
|
||||
"voc",
|
||||
"allpollu"
|
||||
],
|
||||
"units": [
|
||||
"s",
|
||||
"ugm3",
|
||||
"C",
|
||||
"pc",
|
||||
"ppm",
|
||||
"ppb",
|
||||
"%"
|
||||
],
|
||||
"datapoints": [
|
||||
[
|
||||
1234567890,
|
||||
1.2345678,
|
||||
12.345,
|
||||
23.456,
|
||||
345,
|
||||
456,
|
||||
5.6789012
|
||||
]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user