added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

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

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

View 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

View 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" }
```

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
[
{
"uuid": "1234567890ABCDEF",
"userId": 12345,
"mac": "AABBCCDDEEFF",
"name": "Foobot 1"
}
]

View File

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