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,32 @@
<?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 kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.unifi</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,158 @@
# UniFi Binding
This binding integrates with [Ubiquiti UniFi Networks](https://www.ubnt.com/products/#unifi) allowing for presence detection of network clients.
## Supported Things
* `controller` - An instance of the UniFi controller software
* `wirelessClient` - Any wireless client connected to a UniFi wireless network
## Discovery
Discovery is currently not supported.
## Binding Configuration
The binding has no configuration options, all configuration is done at the Bridge and Thing levels.
## Bridge Configuration
You need at least one UniFi Controller (Bridge) for this binding to work. It requires a network accessible instance of the [Ubiquiti Networks Controller Software](https://www.ubnt.com/download/unifi).
The following table describes the Bridge configuration parameters:
| Parameter | Description | Config | Default |
| ------------------------ | ---------------------------------------------- |--------- | ------- |
| host | Hostname of IP address of the UniFi Controller | Required | - |
| port | Port of the UniFi Controller | Required | - |
| username | The username to access the UniFi Controller | Required | - |
| password | The password to access the UniFi Controller | Required | - |
| refresh | Refresh interval in seconds | Optional | 10 |
## Thing Configuration
You must define a UniFi Controller (Bridge) before defining UniFi Clients (Things) for this binding to work.
The following table describes the Thing configuration parameters:
| Parameter | Description | Config | Default |
| ------------ | -------------------------------------------------------------|--------- | ------- |
| cid | The MAC address, IP address, hostname or alias of the client | Required | - |
| site | The site where the client should be found | Optional | - |
| considerHome | The interval in seconds to consider the client as home | Optional | 180 |
Here's some additional notes regarding the thing configuration parameters:
##### `cid`
The `cid` parameter is a universal "client identifier". It accepts the following values:
1. MAC address [highest priority]
1. IP address
1. Hostname (as show by the controller)
1. Alias (as defined by you in the controller UI) [lowest priority]
The priority essentially means the binding attempts to lookup by MAC address, then by IP address, then by hostname and finally by alias. Once it finds a matching client, it short circuits and stops searching. Most of the time, you will simply use the MAC address.
##### `site`
The `site` parameter is optional. If you leave it blank, the client will appear `ONLINE` if found in *any* site defined on the controller.
You may use the `site` parameter as a filter if you only want the client to appear home if it is found in the site defined in the `site` parameter.
Additionally, you may use friendly site names as they appear in the controller UI.
##### `considerHome`
The `considerHome` parameter allows you to control how quickly the binding marks a client as away. For example, using the default of `180` (seconds), the binding will report a client away as soon as `lastSeen` + `180` (seconds) < `now`
## Channels
The Wireless Client information that is retrieved is available as these channels:
| Channel ID | Item Type | Description | Permissions |
|------------|-----------|--------------------------------------------------------------------- | ----------- |
| online | Switch | Online status of the client | Read |
| site | String | Site name (from the controller web UI) the client is associated with | Read |
| macAddress | String | MAC address of the client | Read |
| ipAddress | String | IP address of the client | Read |
| ap | String | Access point (AP) the client is connected to | Read |
| essid | String | Network name (ESSID) the client is connected to | Read |
| rssi | Number | Received signal strength indicator (RSSI) of the client | Read |
| uptime | Number | Uptime of the wireless client (in seconds) | Read |
| lastSeen | DateTime | Date and Time the wireless client was last seen | Read |
| blocked | Switch | Blocked status of the client | Read, Write |
| reconnect | Switch | Force the client to be reconnect | Write |
_Note: All channels with the Write permission require administrator credentials as defined in the controller._
##### `blocked`
The `blocked` channel allows you to block / unblock a client via the controller.
##### `reconnect`
The `reconnect` channel allows you to force a client to reconnect. Sending `ON` to this channel will trigger a reconnect via the controller.
## Full Example
things/unifi.things
```
Bridge unifi:controller:home "UniFi Controller" [ host="unifi", port=8443, username="$username", password="$password", refresh=10 ] {
Thing wirelessClient matthewsPhone "Matthew's iPhone" [ cid="$cid", site="default", considerHome=180 ]
}
```
Replace `$user`, `$password` and `$cid` accordingly.
items/unifi.items
```
Switch MatthewsPhone "Matthew's iPhone [MAP(unifi.map):%s]" { channel="unifi:wirelessClient:home:matthewsPhone:online" }
String MatthewsPhoneSite "Matthew's iPhone: Site [%s]" { channel="unifi:wirelessClient:home:matthewsPhone:site" }
String MatthewsPhoneMAC "Matthew's iPhone: MAC [%s]" { channel="unifi:wirelessClient:home:matthewsPhone:macAddress" }
String MatthewsPhoneIP "Matthew's iPhone: IP [%s]" { channel="unifi:wirelessClient:home:matthewsPhone:ipAddress" }
String MatthewsPhoneAP "Matthew's iPhone: AP [%s]" { channel="unifi:wirelessClient:home:matthewsPhone:ap" }
String MatthewsPhoneESSID "Matthew's iPhone: ESSID [%s]" { channel="unifi:wirelessClient:home:matthewsPhone:essid" }
Number MatthewsPhoneRSSI "Matthew's iPhone: RSSI [%d]" { channel="unifi:wirelessClient:home:matthewsPhone:rssi" }
Number MatthewsPhoneUptime "Matthew's iPhone: Uptime [%d]" { channel="unifi:wirelessClient:home:matthewsPhone:uptime" }
DateTime MatthewsPhoneLastSeen "Matthew's iPhone: Last Seen [%1$tH:%1$tM:%1$tS]" { channel="unifi:wirelessClient:home:matthewsPhone:lastSeen" }
Switch MatthewsPhoneBlocked "Matthew's iPhone: Blocked" { channel="unifi:wirelessClient:home:matthewsPhone:blocked" }
Switch MatthewsPhoneReconnect "Matthew's iPhone: Reconnect" { channel="unifi:wirelessClient:home:matthewsPhone:reconnect" }
```
transform/unifi.map
```
ON=Home
OFF=Away
```
sitemaps/unifi.sitemap
```
sitemap unifi label="UniFi Binding"
{
Frame {
Text item=MatthewsPhone
Text item=MatthewsPhoneSite
Text item=MatthewsPhoneMAC
Text item=MatthewsPhoneIP
Text item=MatthewsPhoneAP
Text item=MatthewsPhoneESSID
Text item=MatthewsPhoneRSSI
Text item=MatthewsPhoneUptime
Text item=MatthewsPhoneLastSeen
Switch item=MatthewsPhoneBlocked
Switch item=MatthewsPhoneReconnect
}
}
```

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-v4_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.unifi</artifactId>
<name>openHAB Add-ons :: Bundles :: UniFi Binding</name>
</project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.unifi-${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-unifi" description="UniFi Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.unifi/${project.version}</bundle>
</feature>
</features>

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.unifi.internal;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link UniFiBindingConstants} class defines common constants, which are
* used across the UniFi binding.
*
* @author Matthew Bowman - Initial contribution
* @author Patrik Wimnell - Blocking / Unblocking client support
*/
public class UniFiBindingConstants {
public static final String BINDING_ID = "unifi";
// List of all Thing Types
public static final ThingTypeUID THING_TYPE_CONTROLLER = new ThingTypeUID(BINDING_ID, "controller");
public static final ThingTypeUID THING_TYPE_WIRED_CLIENT = new ThingTypeUID(BINDING_ID, "wiredClient");
public static final ThingTypeUID THING_TYPE_WIRELESS_CLIENT = new ThingTypeUID(BINDING_ID, "wirelessClient");
// List of common wired + wireless client channels
public static final String CHANNEL_ONLINE = "online";
public static final String CHANNEL_SITE = "site";
public static final String CHANNEL_MAC_ADDRESS = "macAddress";
public static final String CHANNEL_IP_ADDRESS = "ipAddress";
public static final String CHANNEL_UPTIME = "uptime";
public static final String CHANNEL_LAST_SEEN = "lastSeen";
public static final String CHANNEL_BLOCKED = "blocked";
public static final String CHANNEL_RECONNECT = "reconnect";
// List of additional wired client channels
// ..coming soon..
// List of additional wireless client channels
public static final String CHANNEL_AP = "ap";
public static final String CHANNEL_ESSID = "essid";
public static final String CHANNEL_RSSI = "rssi";
// List of all Parameters
public static final String PARAMETER_HOST = "host";
public static final String PARAMETER_PORT = "port";
public static final String PARAMETER_USERNAME = "username";
public static final String PARAMETER_PASSWORD = "password";
public static final String PARAMETER_SITE = "site";
public static final String PARAMETER_CID = "cid";
}

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.unifi.internal;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.unifi.internal.handler.UniFiClientThingHandler;
/**
* The {@link UniFiClientThingConfig} encapsulates all the configuration options for an instance of the
* {@link UniFiClientThingHandler}.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiClientThingConfig {
private String cid = "";
private String site = "";
private int considerHome = 180;
public String getClientID() {
return cid;
}
public String getSite() {
return site;
}
public int getConsiderHome() {
return considerHome;
}
public UniFiClientThingConfig tidy() {
cid = StringUtils.lowerCase(StringUtils.strip(cid));
site = StringUtils.lowerCase(StringUtils.strip(site));
return this;
}
public boolean isValid() {
return StringUtils.isNotBlank(cid);
}
@Override
public String toString() {
return String.format("UniFiClientConfig{cid: '%s', site: '%s', considerHome: %d}", cid, site, considerHome);
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.unifi.internal;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler;
/**
* The {@link UniFiControllerThingConfig} encapsulates all the configuration options for an instance of the
* {@link UniFiControllerThingHandler}.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiControllerThingConfig {
private String host = "unifi";
private int port = 8443;
private String username = "";
private String password = "";
private int refresh = 10;
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public int getRefresh() {
return refresh;
}
public boolean isValid() {
return StringUtils.isNotBlank(host) && StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password);
}
@Override
public String toString() {
return "UniFiControllerConfig{host = " + host + ", port = " + port + ", username = " + username
+ ", password = *****, refresh = " + refresh + "}";
}
}

View File

@@ -0,0 +1,73 @@
/**
* 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.unifi.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.unifi.internal.handler.UniFiClientThingHandler;
import org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.io.net.http.HttpClientInitializationException;
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 UniFiThingHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Matthew Bowman - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.unifi")
@NonNullByDefault
public class UniFiThingHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
@Activate
public UniFiThingHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
// [wip] mgb: disabled due to missing common name attributes with certs
// this.httpClient = httpClientFactory.getCommonHttpClient();
httpClient = new HttpClient(new SslContextFactory(true));
try {
httpClient.start();
} catch (Exception e) {
throw new HttpClientInitializationException("Could not start HttpClient", e);
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return UniFiControllerThingHandler.supportsThingType(thingTypeUID)
|| UniFiClientThingHandler.supportsThingType(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (UniFiControllerThingHandler.supportsThingType(thingTypeUID)) {
return new UniFiControllerThingHandler((Bridge) thing, httpClient);
} else if (UniFiClientThingHandler.supportsThingType(thingTypeUID)) {
return new UniFiClientThingHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiCommunicationException} signals there was a problem communicating with the controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiCommunicationException extends UniFiException {
private static final long serialVersionUID = -7261308872245069364L;
public UniFiCommunicationException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiException} represents a binding specific {@link Exception}.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiException extends Exception {
private static final long serialVersionUID = -7422254981644510570L;
public UniFiException(String message) {
super(message);
}
public UniFiException(String message, Throwable cause) {
super(message, cause);
}
public UniFiException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiExpiredSessionException} signals the session with the controller has expired.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiExpiredSessionException extends UniFiException {
private static final long serialVersionUID = -2002650048964514035L;
public UniFiExpiredSessionException(String message) {
super(message);
}
}

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.unifi.internal.api;
/**
* The {@link UniFiInvalidCredentialsException} signals the credentials used to authenticate with the controller are
* invalid.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiInvalidCredentialsException extends UniFiException {
private static final long serialVersionUID = -7159360851783088458L;
public UniFiInvalidCredentialsException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiInvalidHostException} signals there was a problem with the hostname of the controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiInvalidHostException extends UniFiException {
private static final long serialVersionUID = -7261308872245069364L;
public UniFiInvalidHostException(String message) {
super(message);
}
public UniFiInvalidHostException(String message, Throwable cause) {
super(message, cause);
}
public UniFiInvalidHostException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiNotAuthorizedException} signals the controller denied a request due to non-admin credentials.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiNotAuthorizedException extends UniFiException {
private static final long serialVersionUID = 1379973398415636322L;
public UniFiNotAuthorizedException(String message) {
super(message);
}
public UniFiNotAuthorizedException(String message, Throwable cause) {
super(message, cause);
}
public UniFiNotAuthorizedException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.unifi.internal.api;
/**
* The {@link UniFiSSLException} signals a failure establishing an SSL connection with the controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiSSLException extends UniFiException {
private static final long serialVersionUID = 4688857482270932413L;
public UniFiSSLException(String message) {
super(message);
}
public UniFiSSLException(String message, Throwable cause) {
super(message, cause);
}
public UniFiSSLException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.unifi.internal.api.cache;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UniFiCache} is a specialised lookup table that stores objects using multiple keys in the form
* <code>prefix:suffix</code>. Each implementation is responsible for providing a list of supported prefixes and must
* implement {@link #getSuffix(Object, String)} to provide a value specific suffix derived from the prefix.
*
* Objects are then retrieved simply by using the <code>suffix</code> key component and all combinations of
* <code>prefix:suffix</code> are searched in the order of their priority.
*
* @author Matthew Bowman - Initial contribution
*/
public abstract class UniFiCache<T> {
private static final String SEPARATOR = ":";
public static final String PREFIX_ALIAS = "alias";
public static final String PREFIX_DESC = "desc";
public static final String PREFIX_HOSTNAME = "hostname";
public static final String PREFIX_ID = "id";
public static final String PREFIX_IP = "ip";
public static final String PREFIX_MAC = "mac";
public static final String PREFIX_NAME = "name";
private final Logger logger = LoggerFactory.getLogger(getClass());
private Map<String, T> map = new HashMap<>();
private String[] prefixes;
protected UniFiCache(String... prefixes) {
this.prefixes = prefixes;
}
public final T get(Object id) {
T value = null;
for (String prefix : prefixes) {
String key = prefix + SEPARATOR + id;
if (map.containsKey(key)) {
value = map.get(key);
logger.trace("Cache HIT : '{}' -> {}", key, value);
break;
} else {
logger.trace("Cache MISS : '{}'", key);
}
}
return value;
}
public final void put(T value) {
for (String prefix : prefixes) {
String suffix = getSuffix(value, prefix);
if (StringUtils.isNotBlank(suffix)) {
String key = prefix + SEPARATOR + suffix;
map.put(key, value);
}
}
}
public final void putAll(UniFiCache<T> cache) {
map.putAll(cache.map);
}
public final Collection<T> values() {
return map.values().stream().distinct().collect(Collectors.toList());
}
protected abstract String getSuffix(T value, String prefix);
}

View File

@@ -0,0 +1,46 @@
/**
* 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.unifi.internal.api.cache;
import org.openhab.binding.unifi.internal.api.model.UniFiClient;
/**
* The {@link UniFiClientCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
* {@link UniFiClient} instances.
*
* The cache uses the following prefixes: <code>mac</code>, <code>ip</code>, <code>hostname</code>, and
* <code>alias</code>
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiClientCache extends UniFiCache<UniFiClient> {
public UniFiClientCache() {
super(PREFIX_MAC, PREFIX_IP, PREFIX_HOSTNAME, PREFIX_ALIAS);
}
@Override
protected String getSuffix(UniFiClient client, String prefix) {
switch (prefix) {
case PREFIX_MAC:
return client.getMac();
case PREFIX_IP:
return client.getIp();
case PREFIX_HOSTNAME:
return client.getHostname();
case PREFIX_ALIAS:
return client.getAlias();
}
return null;
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.unifi.internal.api.cache;
import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
/**
* The {@link UniFiDeviceCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
* {@link UniFiDevice} instances.
*
* The cache uses the following prefixes: <code>mac</code>
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiDeviceCache extends UniFiCache<UniFiDevice> {
public UniFiDeviceCache() {
super(PREFIX_MAC);
}
@Override
protected String getSuffix(UniFiDevice device, String prefix) {
switch (prefix) {
case PREFIX_MAC:
return device.getMac();
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.unifi.internal.api.cache;
import org.openhab.binding.unifi.internal.api.model.UniFiSite;
/**
* The {@link UniFiSiteCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
* {@link UniFiSite} instances.
*
* The cache uses the following prefixes: <code>id</code>, <code>name</code>, <code>desc</code>
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiSiteCache extends UniFiCache<UniFiSite> {
public UniFiSiteCache() {
super(PREFIX_ID, PREFIX_NAME, PREFIX_DESC);
}
@Override
protected String getSuffix(UniFiSite site, String prefix) {
switch (prefix) {
case PREFIX_ID:
return site.getId();
case PREFIX_NAME:
return site.getName();
case PREFIX_DESC:
return site.getDescription();
}
return null;
}
}

View File

@@ -0,0 +1,127 @@
/**
* 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.unifi.internal.api.model;
import java.util.Calendar;
import org.apache.commons.lang.BooleanUtils;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.util.UniFiTidyLowerCaseStringDeserializer;
import org.openhab.binding.unifi.internal.api.util.UniFiTimestampDeserializer;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UniFiClient} is the base data model for any (wired or wireless) connected to a UniFi network.
*
* @author Matthew Bowman - Initial contribution
* @author Patrik Wimnell - Blocking / Unblocking client support
*/
public abstract class UniFiClient {
protected final transient UniFiController controller;
@SerializedName("_id")
protected String id;
protected String siteId;
@JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
protected String mac;
protected String ip;
@JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
protected String hostname;
@SerializedName("name")
@JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
protected String alias;
protected Integer uptime;
@JsonAdapter(UniFiTimestampDeserializer.class)
protected Calendar lastSeen;
protected boolean blocked;
protected UniFiClient(UniFiController controller) {
this.controller = controller;
}
public String getId() {
return id;
}
public String getMac() {
return mac;
}
public String getIp() {
return this.ip;
}
public String getHostname() {
return hostname;
}
public String getAlias() {
return alias;
}
public Integer getUptime() {
return uptime;
}
public Calendar getLastSeen() {
return lastSeen;
}
public boolean isBlocked() {
return blocked;
}
public abstract Boolean isWired();
public final Boolean isWireless() {
return BooleanUtils.negate(isWired());
}
protected abstract String getDeviceMac();
public UniFiSite getSite() {
return controller.getSite(siteId);
}
public UniFiDevice getDevice() {
return controller.getDevice(getDeviceMac());
}
// Functional API
public void block(boolean blocked) throws UniFiException {
controller.block(this, blocked);
}
public void reconnect() throws UniFiException {
controller.reconnect(this);
}
@Override
public String toString() {
return String.format(
"UniFiClient{mac: '%s', ip: '%s', hostname: '%s', alias: '%s', wired: %b, blocked: %b, device: %s}",
mac, ip, hostname, alias, isWired(), blocked, getDevice());
}
}

View File

@@ -0,0 +1,306 @@
/**
* 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.unifi.internal.api.model;
import java.util.Collection;
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.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.UniFiExpiredSessionException;
import org.openhab.binding.unifi.internal.api.UniFiNotAuthorizedException;
import org.openhab.binding.unifi.internal.api.cache.UniFiClientCache;
import org.openhab.binding.unifi.internal.api.cache.UniFiDeviceCache;
import org.openhab.binding.unifi.internal.api.cache.UniFiSiteCache;
import org.openhab.binding.unifi.internal.api.util.UniFiClientDeserializer;
import org.openhab.binding.unifi.internal.api.util.UniFiClientInstanceCreator;
import org.openhab.binding.unifi.internal.api.util.UniFiDeviceInstanceCreator;
import org.openhab.binding.unifi.internal.api.util.UniFiSiteInstanceCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* The {@link UniFiController} is the main communication point with an external instance of the Ubiquiti Networks
* Controller Software.
*
* @author Matthew Bowman - Initial contribution
* @author Patrik Wimnell - Blocking / Unblocking client support
*/
@NonNullByDefault
public class UniFiController {
private final Logger logger = LoggerFactory.getLogger(UniFiController.class);
private UniFiSiteCache sitesCache = new UniFiSiteCache();
private UniFiDeviceCache devicesCache = new UniFiDeviceCache();
private UniFiClientCache clientsCache = new UniFiClientCache();
private UniFiClientCache insightsCache = new UniFiClientCache();
private final HttpClient httpClient;
private final String host;
private final int port;
private final String username;
private final String password;
private final Gson gson;
public UniFiController(HttpClient httpClient, String host, int port, String username, String password) {
this.httpClient = httpClient;
this.host = host;
this.port = port;
this.username = username;
this.password = password;
UniFiSiteInstanceCreator siteInstanceCreator = new UniFiSiteInstanceCreator(this);
UniFiDeviceInstanceCreator deviceInstanceCreator = new UniFiDeviceInstanceCreator(this);
UniFiClientInstanceCreator clientInstanceCreator = new UniFiClientInstanceCreator(this);
this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(UniFiSite.class, siteInstanceCreator)
.registerTypeAdapter(UniFiDevice.class, deviceInstanceCreator)
.registerTypeAdapter(UniFiClient.class, new UniFiClientDeserializer())
.registerTypeAdapter(UniFiUnknownClient.class, clientInstanceCreator)
.registerTypeAdapter(UniFiWiredClient.class, clientInstanceCreator)
.registerTypeAdapter(UniFiWirelessClient.class, clientInstanceCreator).create();
}
// Public API
public void start() throws UniFiException {
login();
}
public void stop() throws UniFiException {
logout();
}
public void login() throws UniFiException {
UniFiControllerRequest<Void> req = newRequest(Void.class);
req.setPath("/api/login");
req.setBodyParameter("username", username);
req.setBodyParameter("password", password);
// scurb: Changed strict = false to make blocking feature work
req.setBodyParameter("strict", false);
req.setBodyParameter("remember", false);
executeRequest(req);
}
public void logout() throws UniFiException {
UniFiControllerRequest<Void> req = newRequest(Void.class);
req.setPath("/logout");
executeRequest(req);
}
public void refresh() throws UniFiException {
synchronized (this) {
sitesCache = getSites();
devicesCache = getDevices();
clientsCache = getClients();
insightsCache = getInsights();
}
}
// Site API
public @Nullable UniFiSite getSite(@Nullable String id) {
UniFiSite site = null;
if (StringUtils.isNotBlank(id)) {
synchronized (this) {
site = sitesCache.get(id);
}
if (site == null) {
logger.debug("Could not find a matching site for id = '{}'", id);
}
}
return site;
}
// Device API
public @Nullable UniFiDevice getDevice(@Nullable String id) {
UniFiDevice device = null;
if (StringUtils.isNotBlank(id)) {
synchronized (this) {
device = devicesCache.get(id);
}
if (device == null) {
logger.debug("Could not find a matching device for id = '{}'", id);
}
}
return device;
}
// Client API
public @Nullable UniFiClient getClient(@Nullable String id) {
UniFiClient client = null;
if (StringUtils.isNotBlank(id)) {
synchronized (this) {
// mgb: first check active clients and fallback to insights if not found
client = clientsCache.get(id);
if (client == null) {
client = insightsCache.get(id);
}
}
if (client == null) {
logger.debug("Could not find a matching client for id = {}", id);
}
}
return client;
}
protected void block(UniFiClient client, boolean blocked) throws UniFiException {
UniFiControllerRequest<Void> req = newRequest(Void.class);
req.setPath("/api/s/" + client.getSite().getName() + "/cmd/stamgr");
req.setBodyParameter("cmd", blocked ? "block-sta" : "unblock-sta");
req.setBodyParameter("mac", client.getMac());
executeRequest(req);
}
protected void reconnect(UniFiClient client) throws UniFiException {
UniFiControllerRequest<Void> req = newRequest(Void.class);
req.setPath("/api/s/" + client.getSite().getName() + "/cmd/stamgr");
req.setBodyParameter("cmd", "kick-sta");
req.setBodyParameter("mac", client.getMac());
executeRequest(req);
}
// Internal API
private <T> UniFiControllerRequest<T> newRequest(Class<T> responseType) {
return new UniFiControllerRequest<>(responseType, gson, httpClient, host, port);
}
private <T> @Nullable T executeRequest(UniFiControllerRequest<T> request) throws UniFiException {
T result;
try {
result = request.execute();
} catch (UniFiExpiredSessionException e) {
login();
result = executeRequest(request);
} catch (UniFiNotAuthorizedException e) {
logger.warn("Not Authorized! Please make sure your controller credentials have administrator rights");
result = null;
}
return result;
}
private UniFiSiteCache getSites() throws UniFiException {
UniFiControllerRequest<UniFiSite[]> req = newRequest(UniFiSite[].class);
req.setPath("/api/self/sites");
UniFiSite[] sites = executeRequest(req);
UniFiSiteCache cache = new UniFiSiteCache();
if (sites != null) {
logger.debug("Found {} UniFi Site(s): {}", sites.length, lazyFormatAsList(sites));
for (UniFiSite site : sites) {
cache.put(site);
}
}
return cache;
}
private UniFiDeviceCache getDevices() throws UniFiException {
UniFiDeviceCache cache = new UniFiDeviceCache();
Collection<UniFiSite> sites = sitesCache.values();
for (UniFiSite site : sites) {
cache.putAll(getDevices(site));
}
return cache;
}
private UniFiDeviceCache getDevices(UniFiSite site) throws UniFiException {
UniFiControllerRequest<UniFiDevice[]> req = newRequest(UniFiDevice[].class);
req.setPath("/api/s/" + site.getName() + "/stat/device");
UniFiDevice[] devices = executeRequest(req);
UniFiDeviceCache cache = new UniFiDeviceCache();
if (devices != null) {
logger.debug("Found {} UniFi Device(s): {}", devices.length, lazyFormatAsList(devices));
for (UniFiDevice device : devices) {
cache.put(device);
}
}
return cache;
}
private UniFiClientCache getClients() throws UniFiException {
UniFiClientCache cache = new UniFiClientCache();
Collection<UniFiSite> sites = sitesCache.values();
for (UniFiSite site : sites) {
cache.putAll(getClients(site));
}
return cache;
}
private UniFiClientCache getClients(UniFiSite site) throws UniFiException {
UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class);
req.setPath("/api/s/" + site.getName() + "/stat/sta");
UniFiClient[] clients = executeRequest(req);
UniFiClientCache cache = new UniFiClientCache();
if (clients != null) {
logger.debug("Found {} UniFi Client(s): {}", clients.length, lazyFormatAsList(clients));
for (UniFiClient client : clients) {
cache.put(client);
}
}
return cache;
}
private UniFiClientCache getInsights() throws UniFiException {
UniFiClientCache cache = new UniFiClientCache();
Collection<UniFiSite> sites = sitesCache.values();
for (UniFiSite site : sites) {
cache.putAll(getInsights(site));
}
return cache;
}
private UniFiClientCache getInsights(UniFiSite site) throws UniFiException {
UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class);
req.setPath("/api/s/" + site.getName() + "/stat/alluser");
req.setQueryParameter("within", 168); // scurb: Changed to 7 days.
UniFiClient[] clients = executeRequest(req);
UniFiClientCache cache = new UniFiClientCache();
if (clients != null) {
logger.debug("Found {} UniFi Insights(s): {}", clients.length, lazyFormatAsList(clients));
for (UniFiClient client : clients) {
cache.put(client);
}
}
return cache;
}
private static Object lazyFormatAsList(Object[] arr) {
return new Object() {
@Override
public String toString() {
String value = "";
for (Object o : arr) {
value += "\n - " + o.toString();
}
return value;
}
};
}
}

View File

@@ -0,0 +1,225 @@
/**
* 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.unifi.internal.api.model;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.SSLException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MimeTypes;
import org.openhab.binding.unifi.internal.api.UniFiCommunicationException;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.UniFiExpiredSessionException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidCredentialsException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidHostException;
import org.openhab.binding.unifi.internal.api.UniFiNotAuthorizedException;
import org.openhab.binding.unifi.internal.api.UniFiSSLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
/**
* The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
*
* @author Matthew Bowman - Initial contribution
*
* @param <T> The response type expected as a result of the request's execution
*/
@NonNullByDefault
public class UniFiControllerRequest<T> {
private static final String CONTENT_TYPE_APPLICATION_JSON = MimeTypes.Type.APPLICATION_JSON.asString();
private static final long TIMEOUT_SECONDS = 5;
private static final String PROPERTY_DATA = "data";
private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
private final Gson gson;
private final HttpClient httpClient;
private final String host;
private final int port;
private String path = "/";
private Map<String, String> queryParameters = new HashMap<>();
private Map<String, String> bodyParameters = new HashMap<>();
private final Class<T> resultType;
// Public API
public UniFiControllerRequest(Class<T> resultType, Gson gson, HttpClient httpClient, String host, int port) {
this.resultType = resultType;
this.gson = gson;
this.httpClient = httpClient;
this.host = host;
this.port = port;
}
public void setPath(String path) {
this.path = path;
}
public void setBodyParameter(String key, Object value) {
this.bodyParameters.put(key, String.valueOf(value));
}
public void setQueryParameter(String key, Object value) {
this.queryParameters.put(key, String.valueOf(value));
}
public @Nullable T execute() throws UniFiException {
T result = null;
String json = getContent();
// mgb: only try and unmarshall non-void result types
if (!Void.class.equals(resultType)) {
JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject();
if (jsonObject.has(PROPERTY_DATA) && jsonObject.get(PROPERTY_DATA).isJsonArray()) {
result = gson.fromJson(jsonObject.getAsJsonArray(PROPERTY_DATA), resultType);
}
}
return result;
}
// Private API
private String getContent() throws UniFiException {
String content;
ContentResponse response = getContentResponse();
int status = response.getStatus();
switch (status) {
case HttpStatus.OK_200:
content = response.getContentAsString();
if (logger.isTraceEnabled()) {
logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
}
break;
case HttpStatus.BAD_REQUEST_400:
throw new UniFiInvalidCredentialsException("Invalid Credentials");
case HttpStatus.UNAUTHORIZED_401:
throw new UniFiExpiredSessionException("Expired Credentials");
case HttpStatus.FORBIDDEN_403:
throw new UniFiNotAuthorizedException("Unauthorized Access");
default:
throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
}
return content;
}
private ContentResponse getContentResponse() throws UniFiException {
Request request = newRequest();
logger.trace(">> {} {}", request.getMethod(), request.getURI());
ContentResponse response;
try {
response = request.send();
} catch (TimeoutException | InterruptedException e) {
throw new UniFiCommunicationException(e);
} catch (ExecutionException e) {
// mgb: unwrap the cause and try to cleanly handle it
Throwable cause = e.getCause();
if (cause instanceof UnknownHostException) {
// invalid hostname
throw new UniFiInvalidHostException(cause);
} else if (cause instanceof ConnectException) {
// cannot connect
throw new UniFiCommunicationException(cause);
} else if (cause instanceof SSLException) {
// cannot establish ssl connection
throw new UniFiSSLException(cause);
} else if (cause instanceof HttpResponseException
&& ((HttpResponseException) cause).getResponse() instanceof ContentResponse) {
// the UniFi controller violates the HTTP protocol
// - it returns 401 UNAUTHORIZED without the WWW-Authenticate response header
// - this causes an ExceptionException to be thrown
// - we unwrap the response from the exception for proper handling of the 401 status code
response = (ContentResponse) ((HttpResponseException) cause).getResponse();
} else {
// catch all
throw new UniFiException(cause);
}
}
return response;
}
private Request newRequest() {
HttpMethod method = bodyParameters.isEmpty() ? HttpMethod.GET : HttpMethod.POST;
HttpURI uri = new HttpURI(HttpScheme.HTTPS.asString(), host, port, path);
Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.method(method);
for (Entry<String, String> entry : queryParameters.entrySet()) {
request.param(entry.getKey(), entry.getValue());
}
if (!bodyParameters.isEmpty()) {
String jsonBody = getRequestBodyAsJson();
ContentProvider content = new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON, jsonBody,
StandardCharsets.UTF_8);
request = request.content(content);
}
return request;
}
private String getRequestBodyAsJson() {
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = new JsonObject();
JsonElement jsonElement = null;
for (Entry<String, String> entry : bodyParameters.entrySet()) {
try {
jsonElement = jsonParser.parse(entry.getValue());
} catch (JsonSyntaxException e) {
jsonElement = new JsonPrimitive(entry.getValue());
}
jsonObject.add(entry.getKey(), jsonElement);
}
return jsonObject.toString();
}
private static String prettyPrintJson(String content) {
JsonParser parser = new JsonParser();
JsonObject json = parser.parse(content).getAsJsonObject();
Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
return prettyGson.toJson(json);
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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.unifi.internal.api.model;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.unifi.internal.api.util.UniFiTidyLowerCaseStringDeserializer;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UniFiDevice} represents the data model of a UniFi Wireless Device
* (better known as an Access Point).
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiDevice {
protected final transient UniFiController controller;
@SerializedName("_id")
private String id;
@JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
private String mac;
private String model;
private String name;
private String siteId;
public UniFiDevice(UniFiController controller) {
this.controller = controller;
}
public String getId() {
return id;
}
public String getModel() {
return model;
}
public String getName() {
return StringUtils.defaultIfBlank(name, mac);
}
public String getMac() {
return mac;
}
public UniFiSite getSite() {
return controller.getSite(siteId);
}
@Override
public String toString() {
return String.format("UniFiDevice{mac: '%s', name: '%s', model: '%s', site: %s}", mac, name, model, getSite());
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.unifi.internal.api.model;
import org.apache.commons.lang.StringUtils;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UniFiSite} represents the data model of a UniFi site.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiSite {
private final transient UniFiController controller;
public UniFiSite(UniFiController controller) {
this.controller = controller;
}
@SerializedName("_id")
private String id;
private String name;
private String desc;
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getDescription() {
return desc;
}
public boolean matchesName(String siteName) {
return StringUtils.equalsIgnoreCase(desc, siteName) || StringUtils.equalsIgnoreCase(name, siteName)
|| StringUtils.equalsIgnoreCase(id, siteName);
}
@Override
public String toString() {
return String.format("UniFiSite{name: '%s', desc: '%s'}", name, desc);
}
}

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.unifi.internal.api.model;
/**
* A {@link UniFiUnknownClient} represents an unknown {@link UniFiClient}.
*
* An unknown client is neither a {@link UniFiWiredClient} nor a {@link UniFiWirelessClient}
* because the <code>is_wired</code> property was missing from the JSON response of the controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiUnknownClient extends UniFiClient {
public UniFiUnknownClient(UniFiController controller) {
super(controller);
}
@Override
public Boolean isWired() {
return null; // mgb: no is_wired property in the json
}
@Override
public String getDeviceMac() {
return null; // mgb: no device mac in the json
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.unifi.internal.api.model;
/**
* A {@link UniFiWiredClient} represents a wired {@link UniFiClient}.
*
* A wired client is physically connected to the network - typically it is connected via an Ethernet cable.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiWiredClient extends UniFiClient {
private String swMac;
public UniFiWiredClient(UniFiController controller) {
super(controller);
}
@Override
public Boolean isWired() {
return true;
}
@Override
public String getDeviceMac() {
return swMac;
}
}

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.unifi.internal.api.model;
import org.openhab.binding.unifi.internal.api.util.UniFiTidyLowerCaseStringDeserializer;
import com.google.gson.annotations.JsonAdapter;
/**
* A {@link UniFiWirelessClient} represents a wireless {@link UniFiClient}.
*
* A wireless client is not physically connected to the network - typically it is connected via a Wi-Fi adapter.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiWirelessClient extends UniFiClient {
@JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
private String apMac;
private String essid;
private Integer rssi;
public UniFiWirelessClient(UniFiController controller) {
super(controller);
}
@Override
public Boolean isWired() {
return false;
}
@Override
public String getDeviceMac() {
return apMac;
}
public String getEssid() {
return essid;
}
public Integer getRssi() {
return rssi;
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.unifi.internal.api.util;
import java.lang.reflect.Type;
import org.openhab.binding.unifi.internal.api.model.UniFiClient;
import org.openhab.binding.unifi.internal.api.model.UniFiUnknownClient;
import org.openhab.binding.unifi.internal.api.model.UniFiWiredClient;
import org.openhab.binding.unifi.internal.api.model.UniFiWirelessClient;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
/**
*
* The {@link UniFiClientDeserializer} is an implementation of {@link JsonDeserializer} that deserializes
* {@link UniFiClient} instances based on each client's <code>is_wired</code> property contained in the JSON output of
* the controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiClientDeserializer implements JsonDeserializer<UniFiClient> {
private static final String PROPERTY_IS_WIRED = "is_wired";
@Override
public UniFiClient deserialize(JsonElement json, Type type, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
JsonElement isWiredElement = jsonObject.get(PROPERTY_IS_WIRED);
// mgb: if the "is_wired "property is missing, the client is unknown
if (isWiredElement == null) {
return context.deserialize(json, UniFiUnknownClient.class);
}
boolean isWired = isWiredElement.getAsBoolean();
if (isWired) {
return context.deserialize(json, UniFiWiredClient.class);
}
return context.deserialize(json, UniFiWirelessClient.class);
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.unifi.internal.api.util;
import java.lang.reflect.Type;
import org.openhab.binding.unifi.internal.api.model.UniFiClient;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.binding.unifi.internal.api.model.UniFiUnknownClient;
import org.openhab.binding.unifi.internal.api.model.UniFiWiredClient;
import org.openhab.binding.unifi.internal.api.model.UniFiWirelessClient;
import com.google.gson.InstanceCreator;
/**
* The {@link UniFiClientInstanceCreator} creates instances of {@link UniFiClient}s during the JSON unmarshalling of
* controller responses.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiClientInstanceCreator implements InstanceCreator<UniFiClient> {
private final UniFiController controller;
public UniFiClientInstanceCreator(UniFiController controller) {
this.controller = controller;
}
@Override
public UniFiClient createInstance(Type type) {
if (UniFiUnknownClient.class.equals(type)) {
return new UniFiUnknownClient(controller);
}
if (UniFiWirelessClient.class.equals(type)) {
return new UniFiWirelessClient(controller);
}
if (UniFiWiredClient.class.equals(type)) {
return new UniFiWiredClient(controller);
}
return null;
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.unifi.internal.api.util;
import java.lang.reflect.Type;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
import com.google.gson.InstanceCreator;
/**
*
* The {@link UniFiDeviceInstanceCreator} creates instances of {@link UniFiDevice}s during the JSON unmarshalling of
* controller responses.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiDeviceInstanceCreator implements InstanceCreator<UniFiDevice> {
private final UniFiController controller;
public UniFiDeviceInstanceCreator(UniFiController controller) {
this.controller = controller;
}
@Override
public UniFiDevice createInstance(Type type) {
return new UniFiDevice(controller);
}
}

View File

@@ -0,0 +1,41 @@
/**
* 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.unifi.internal.api.util;
import java.lang.reflect.Type;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.binding.unifi.internal.api.model.UniFiSite;
import com.google.gson.InstanceCreator;
/**
*
* The {@link UniFiSiteInstanceCreator} creates instances of {@link UniFiSite}s during the JSON unmarshalling of
* controller responses.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiSiteInstanceCreator implements InstanceCreator<UniFiSite> {
private final UniFiController controller;
public UniFiSiteInstanceCreator(UniFiController controller) {
this.controller = controller;
}
@Override
public UniFiSite createInstance(Type type) {
return new UniFiSite(controller);
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.unifi.internal.api.util;
import java.lang.reflect.Type;
import org.apache.commons.lang.StringUtils;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
*
* The {@link UniFiTidyLowerCaseStringDeserializer} is an implementation of {@link JsonDeserializer} that deserializes
* strings in a tidy lower case format.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiTidyLowerCaseStringDeserializer implements JsonDeserializer<String> {
@Override
public String deserialize(JsonElement json, Type type, JsonDeserializationContext context)
throws JsonParseException {
String s = json.getAsJsonPrimitive().getAsString();
return StringUtils.lowerCase(StringUtils.strip(s));
}
}

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.unifi.internal.api.util;
import java.lang.reflect.Type;
import java.util.Calendar;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
/**
* The {@link UniFiTimestampDeserializer} is an implementation of {@link JsonDeserializer} that deserializes timestamps
* returned in the JSON responses of the UniFi controller.
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiTimestampDeserializer implements JsonDeserializer<Calendar> {
@Override
public Calendar deserialize(JsonElement json, Type type, JsonDeserializationContext context) {
String text = json.getAsJsonPrimitive().getAsString();
long millis = Long.valueOf(text) * 1000;
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(millis);
return cal;
}
}

View File

@@ -0,0 +1,132 @@
/**
* 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.unifi.internal.handler;
import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.types.RefreshType.REFRESH;
import java.lang.reflect.ParameterizedType;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Matthew Bowman - Initial contribution
*
* @param <E> entity - the UniFi entity class used by this thing handler
* @param <C> config - the UniFi config class used by this thing handler
*/
@NonNullByDefault
public abstract class UniFiBaseThingHandler<E, C> extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(UniFiBaseThingHandler.class);
public UniFiBaseThingHandler(Thing thing) {
super(thing);
}
@Override
@SuppressWarnings("unchecked")
public final void initialize() {
Bridge bridge = getBridge();
if (bridge == null || bridge.getHandler() == null
|| !(bridge.getHandler() instanceof UniFiControllerThingHandler)) {
updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"You must choose a UniFi Controller for this thing.");
return;
}
if (bridge.getStatus() == OFFLINE) {
updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "The UniFi Controller is currently offline.");
}
// mgb: derive the config class from the generic type
Class<?> clazz = (Class<?>) (((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[1]);
C config = (C) getConfigAs(clazz);
initialize(config);
}
/**
* Utility method to access the {@link UniFiController} instance associated with this thing.
*
* @return
*/
@SuppressWarnings("null")
private final @Nullable UniFiController getController() {
Bridge bridge = getBridge();
if (bridge != null && bridge.getHandler() != null
&& (bridge.getHandler() instanceof UniFiControllerThingHandler)) {
return ((UniFiControllerThingHandler) bridge.getHandler()).getController();
}
return null;
}
@Override
public final void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling command = {} for channel = {}", command, channelUID);
// mgb: only handle commands if we're ONLINE
if (getThing().getStatus() == ONLINE) {
UniFiController controller = getController();
if (controller != null) {
E entity = getEntity(controller);
if (entity != null) {
if (command == REFRESH) {
refreshChannel(entity, channelUID);
} else {
try {
handleCommand(entity, channelUID, command);
} catch (UniFiException e) {
logger.warn("Unexpected error handling command = {} for channel = {} : {}", command,
channelUID, e.getMessage());
}
}
}
}
}
}
protected final void refresh() {
// mgb: only refresh if we're ONLINE
if (getThing().getStatus() == ONLINE) {
UniFiController controller = getController();
if (controller != null) {
E entity = getEntity(controller);
if (entity != null) {
for (Channel channel : getThing().getChannels()) {
ChannelUID channelUID = channel.getUID();
refreshChannel(entity, channelUID);
}
}
}
}
}
protected abstract void initialize(@NonNull C config);
protected abstract @Nullable E getEntity(UniFiController controller);
protected abstract void refreshChannel(E entity, ChannelUID channelUID);
protected abstract void handleCommand(E entity, ChannelUID channelUID, Command command) throws UniFiException;
}

View File

@@ -0,0 +1,302 @@
/**
* 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.unifi.internal.handler;
import static org.openhab.binding.unifi.internal.UniFiBindingConstants.*;
import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.unifi.internal.UniFiBindingConstants;
import org.openhab.binding.unifi.internal.UniFiClientThingConfig;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.model.UniFiClient;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
import org.openhab.binding.unifi.internal.api.model.UniFiSite;
import org.openhab.binding.unifi.internal.api.model.UniFiWiredClient;
import org.openhab.binding.unifi.internal.api.model.UniFiWirelessClient;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UniFiClientThingHandler} is responsible for handling commands and status
* updates for UniFi Wireless Devices.
*
* @author Matthew Bowman - Initial contribution
* @author Patrik Wimnell - Blocking / Unblocking client support
*/
@NonNullByDefault
public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient, UniFiClientThingConfig> {
public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
return UniFiBindingConstants.THING_TYPE_WIRELESS_CLIENT.equals(thingTypeUID);
}
private final Logger logger = LoggerFactory.getLogger(UniFiClientThingHandler.class);
private UniFiClientThingConfig config = new UniFiClientThingConfig();
public UniFiClientThingHandler(Thing thing) {
super(thing);
}
@Override
protected synchronized void initialize(UniFiClientThingConfig config) {
// mgb: called when the config changes
if (thing.getStatus() == INITIALIZING) {
logger.debug("Initializing the UniFi Client Handler with config = {}", config);
if (!config.isValid()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR,
"You must define a MAC address, IP address, hostname or alias for this thing.");
return;
}
this.config = config;
updateStatus(ONLINE);
}
}
private static boolean belongsToSite(UniFiClient client, String siteName) {
boolean result = true; // mgb: assume true = proof by contradiction
if (StringUtils.isNotEmpty(siteName)) {
UniFiSite site = client.getSite();
// mgb: if the 'site' can't be found or the name doesn't match...
if (site == null || !site.matchesName(siteName)) {
// mgb: ... then the client doesn't belong to this thing's configured 'site' and we 'filter' it
result = false;
}
}
return result;
}
@Override
protected synchronized @Nullable UniFiClient getEntity(UniFiController controller) {
UniFiClient client = controller.getClient(config.getClientID());
// mgb: short circuit
if (client == null || !belongsToSite(client, config.getSite())) {
return null;
}
return client;
}
private State getDefaultState(String channelID, boolean clientHome) {
State state = UnDefType.NULL;
switch (channelID) {
case CHANNEL_ONLINE:
case CHANNEL_SITE:
case CHANNEL_AP:
case CHANNEL_ESSID:
case CHANNEL_RSSI:
case CHANNEL_MAC_ADDRESS:
case CHANNEL_IP_ADDRESS:
case CHANNEL_BLOCKED:
state = (clientHome ? UnDefType.NULL : UnDefType.UNDEF); // skip the update if the client is home
break;
case CHANNEL_UPTIME:
// mgb: uptime should default to 0 seconds
state = (clientHome ? UnDefType.NULL : new DecimalType(0)); // skip the update if the client is home
break;
case CHANNEL_LAST_SEEN:
// mgb: lastSeen should keep the last state no matter what
state = UnDefType.NULL;
break;
case CHANNEL_RECONNECT:
state = OnOffType.OFF;
break;
}
return state;
}
private synchronized boolean isClientHome(UniFiClient client) {
boolean online = false;
if (client != null) {
Calendar lastSeen = client.getLastSeen();
if (lastSeen == null) {
logger.warn("Could not determine if client is online: cid = {}, lastSeen = null", config.getClientID());
} else {
Calendar considerHome = (Calendar) lastSeen.clone();
considerHome.add(Calendar.SECOND, config.getConsiderHome());
Calendar now = Calendar.getInstance();
online = (now.compareTo(considerHome) < 0);
}
}
return online;
}
@Override
protected void refreshChannel(UniFiClient client, ChannelUID channelUID) {
boolean clientHome = isClientHome(client);
UniFiDevice device = client.getDevice();
UniFiSite site = (device == null ? null : device.getSite());
String channelID = channelUID.getIdWithoutGroup();
State state = getDefaultState(channelID, clientHome);
switch (channelID) {
// mgb: common wired + wireless client channels
// :online
case CHANNEL_ONLINE:
state = OnOffType.from(clientHome);
break;
// :site
case CHANNEL_SITE:
if (clientHome && site != null && StringUtils.isNotBlank(site.getDescription())) {
state = StringType.valueOf(site.getDescription());
}
break;
// :macAddress
case CHANNEL_MAC_ADDRESS:
if (clientHome && StringUtils.isNotBlank(client.getMac())) {
state = StringType.valueOf(client.getMac());
}
break;
// :ipAddress
case CHANNEL_IP_ADDRESS:
if (clientHome && StringUtils.isNotBlank(client.getIp())) {
state = StringType.valueOf(client.getIp());
}
break;
// :uptime
case CHANNEL_UPTIME:
if (clientHome && client.getUptime() != null) {
state = new DecimalType(client.getUptime());
}
break;
// :lastSeen
case CHANNEL_LAST_SEEN:
// mgb: we don't check clientOnline as lastSeen is also included in the Insights data
if (client.getLastSeen() != null) {
state = new DateTimeType(
ZonedDateTime.ofInstant(client.getLastSeen().toInstant(), ZoneId.systemDefault()));
}
break;
// :blocked
case CHANNEL_BLOCKED:
state = OnOffType.from(client.isBlocked());
break;
default:
// mgb: additional wired client channels
if (client.isWired() && (client instanceof UniFiWiredClient)) {
state = getWiredChannelState((UniFiWiredClient) client, clientHome, channelID);
}
// mgb: additional wireless client channels
else if (client.isWireless() && (client instanceof UniFiWirelessClient)) {
state = getWirelessChannelState((UniFiWirelessClient) client, clientHome, channelID);
}
break;
}
// mgb: only non null states get updates
if (state != UnDefType.NULL) {
updateState(channelID, state);
}
}
private State getWiredChannelState(UniFiWiredClient client, boolean clientHome, String channelID) {
State state = UnDefType.NULL;
return state;
}
private State getWirelessChannelState(UniFiWirelessClient client, boolean clientHome, String channelID) {
State state = UnDefType.NULL;
switch (channelID) {
// :ap
case CHANNEL_AP:
UniFiDevice device = client.getDevice();
if (clientHome && device != null && StringUtils.isNotBlank(device.getName())) {
state = StringType.valueOf(device.getName());
}
break;
// :essid
case CHANNEL_ESSID:
if (clientHome && StringUtils.isNotBlank(client.getEssid())) {
state = StringType.valueOf(client.getEssid());
}
break;
// :rssi
case CHANNEL_RSSI:
if (clientHome && client.getRssi() != null) {
state = new DecimalType(client.getRssi());
}
break;
// :reconnect
case CHANNEL_RECONNECT:
// nop - read-only channel
break;
}
return state;
}
@Override
protected void handleCommand(UniFiClient client, ChannelUID channelUID, Command command) throws UniFiException {
String channelID = channelUID.getIdWithoutGroup();
switch (channelID) {
case CHANNEL_BLOCKED:
handleBlockedCommand(client, channelUID, command);
break;
case CHANNEL_RECONNECT:
handleReconnectCommand(client, channelUID, command);
break;
default:
logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
}
}
private void handleBlockedCommand(UniFiClient client, ChannelUID channelUID, Command command)
throws UniFiException {
if (command instanceof OnOffType) {
client.block(command == OnOffType.ON);
} else {
logger.warn("Ignoring unsupported command = {} for channel = {} - valid commands types are: OnOffType",
command, channelUID);
}
}
private void handleReconnectCommand(UniFiClient client, ChannelUID channelUID, Command command)
throws UniFiException {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
client.reconnect();
updateState(channelUID, OnOffType.OFF);
}
} else {
logger.warn("Ignoring unsupported command = {} for channel = {} - valid commands types are: OnOffType",
command, channelUID);
}
}
}

View File

@@ -0,0 +1,199 @@
/**
* 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.unifi.internal.handler;
import static org.openhab.core.thing.ThingStatus.OFFLINE;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.thing.ThingStatusDetail.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.unifi.internal.UniFiBindingConstants;
import org.openhab.binding.unifi.internal.UniFiControllerThingConfig;
import org.openhab.binding.unifi.internal.api.UniFiCommunicationException;
import org.openhab.binding.unifi.internal.api.UniFiException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidCredentialsException;
import org.openhab.binding.unifi.internal.api.UniFiInvalidHostException;
import org.openhab.binding.unifi.internal.api.UniFiSSLException;
import org.openhab.binding.unifi.internal.api.model.UniFiController;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link UniFiControllerThingHandler} is responsible for handling commands and status
* updates for the UniFi Controller.
*
* @author Matthew Bowman - Initial contribution
*/
@NonNullByDefault
public class UniFiControllerThingHandler extends BaseBridgeHandler {
public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
return UniFiBindingConstants.THING_TYPE_CONTROLLER.equals(thingTypeUID);
}
private static final String STATUS_DESCRIPTION_COMMUNICATION_ERROR = "Error communicating with the UniFi controller";
private static final String STATUS_DESCRIPTION_SSL_ERROR = "Error establishing an SSL connection with the UniFi controller";
private static final String STATUS_DESCRIPTION_INVALID_CREDENTIALS = "Invalid username and/or password - please double-check your configuration";
private static final String STATUS_DESCRIPTION_INVALID_HOSTNAME = "Invalid hostname - please double-check your configuration";
private final Logger logger = LoggerFactory.getLogger(UniFiControllerThingHandler.class);
private UniFiControllerThingConfig config = new UniFiControllerThingConfig();
private @Nullable volatile UniFiController controller; /* mgb: volatile because accessed from multiple threads */
private @Nullable ScheduledFuture<?> refreshJob;
private final HttpClient httpClient;
public UniFiControllerThingHandler(Bridge bridge, HttpClient httpClient) {
super(bridge);
this.httpClient = httpClient;
}
// Public API
@Override
public void initialize() {
// mgb: called when the config changes
cancelRefreshJob();
config = getConfig().as(UniFiControllerThingConfig.class);
logger.debug("Initializing the UniFi Controller Handler with config = {}", config);
try {
controller = new UniFiController(httpClient, config.getHost(), config.getPort(), config.getUsername(),
config.getPassword());
controller.start();
updateStatus(ONLINE);
} catch (UniFiInvalidHostException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_HOSTNAME);
} catch (UniFiCommunicationException e) {
updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
} catch (UniFiSSLException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_SSL_ERROR);
} catch (UniFiInvalidCredentialsException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
} catch (UniFiException e) {
logger.error("Unknown error while configuring the UniFi Controller", e);
updateStatus(OFFLINE, CONFIGURATION_ERROR, e.getMessage());
}
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
if (status == ONLINE || (status == OFFLINE && statusDetail == COMMUNICATION_ERROR)) {
scheduleRefreshJob();
} else if (status == OFFLINE && statusDetail == CONFIGURATION_ERROR) {
cancelRefreshJob();
}
// mgb: update the status only if it's changed
ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
.build();
if (!statusInfo.equals(getThing().getStatusInfo())) {
super.updateStatus(status, statusDetail, description);
}
}
@Override
public void dispose() {
cancelRefreshJob();
if (controller != null) {
try {
controller.stop();
} catch (UniFiException e) {
// mgb: nop as we're in dispose
}
controller = null;
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// nop - read-only binding
logger.warn("Ignoring command = {} for channel = {} - the UniFi binding is read-only!", command, channelUID);
}
public @Nullable UniFiController getController() {
return controller;
}
public int getRefreshInterval() {
return config.getRefresh();
}
// Private API
private void scheduleRefreshJob() {
synchronized (this) {
if (refreshJob == null) {
logger.debug("Scheduling refresh job every {}s", config.getRefresh());
refreshJob = scheduler.scheduleWithFixedDelay(this::run, 0, config.getRefresh(), TimeUnit.SECONDS);
}
}
}
private void cancelRefreshJob() {
synchronized (this) {
if (refreshJob != null) {
logger.debug("Cancelling refresh job");
refreshJob.cancel(true);
refreshJob = null;
}
}
}
private void run() {
try {
logger.trace("Executing refresh job");
refresh();
updateStatus(ONLINE);
} catch (UniFiCommunicationException e) {
updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
} catch (UniFiInvalidCredentialsException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
} catch (Exception e) {
logger.warn("Unhandled exception while refreshing the UniFi Controller {} - {}", getThing().getUID(),
e.getMessage());
updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
}
}
private void refresh() throws UniFiException {
if (controller != null) {
logger.debug("Refreshing the UniFi Controller {}", getThing().getUID());
controller.refresh();
// mgb: then refresh all the client things
getThing().getThings().forEach((thing) -> {
if (thing.getHandler() instanceof UniFiBaseThingHandler) {
((UniFiBaseThingHandler) thing.getHandler()).refresh();
}
});
}
}
}

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.unifi.internal.ssl;
import java.net.Socket;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509ExtendedTrustManager;
/**
*
* The {@link UniFiTrustManager} is a "trust all" implementation of {@link X509ExtendedTrustManager}.
*
* @see {@link UniFiTrustManagerProvider}
*
* @author Matthew Bowman - Initial contribution
*/
public class UniFiTrustManager extends X509ExtendedTrustManager {
private static UniFiTrustManager instance = new UniFiTrustManager();
public static UniFiTrustManager getInstance() {
return instance;
}
/**
* private construction - singleton
*/
private UniFiTrustManager() {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
throws CertificateException {
}
}

View File

@@ -0,0 +1,39 @@
/**
* 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.unifi.internal.ssl;
import javax.net.ssl.X509ExtendedTrustManager;
import org.openhab.core.io.net.http.TlsTrustManagerProvider;
/**
*
* The {@link UniFiTrustManagerProvider} is an implementation of {@link TlsTrustManagerProvider} which provides an
* instance of {@link UniFiTrustManagerProvider} for all servers that use a certificate with the common name equal to
* "UniFi" (<code>CN=UniFi</code>).
*
* @author Matthew Bowman - Initial contribution
*/
// @Component // [wip] mgb: disabled due to issues with service order loading
public class UniFiTrustManagerProvider implements TlsTrustManagerProvider {
@Override
public String getHostName() {
return "UniFi";
}
@Override
public X509ExtendedTrustManager getTrustManager() {
return UniFiTrustManager.getInstance();
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="unifi" 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>UniFi Binding</name>
<description>The UniFi binding integrates the UniFi controller from Ubiquiti Networks to facilitate tracking of Wi-Fi
clients.</description>
<author>Matthew Bowman</author>
</binding:binding>

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="unifi"
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">
<bridge-type id="controller">
<label>UniFi Controller</label>
<description>A UniFi controller.</description>
<config-description>
<parameter name="host" type="text" required="true">
<label>Hostname</label>
<description>Hostname of IP address of the UniFi Controller</description>
<default>unifi</default>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer" max="65535" min="1" required="false">
<label>Port</label>
<description>Port of the UniFi Controller</description>
<default>8443</default>
</parameter>
<parameter name="username" type="text" required="true">
<label>Username</label>
<description>The username to access the UniFi Controller.</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>Password</label>
<description>The password to access the UniFi Controller.</description>
<context>password</context>
</parameter>
<parameter name="refresh" type="integer" required="false">
<label>Refresh Interval</label>
<description>The refresh interval in seconds to poll the UniFi controller</description>
<default>10</default>
</parameter>
</config-description>
</bridge-type>
<!-- <thing-type id="wiredClient"> .. coming soon .. </thing-type> -->
<thing-type id="wirelessClient">
<supported-bridge-type-refs>
<bridge-type-ref id="controller"/>
</supported-bridge-type-refs>
<label>UniFi Wireless Client</label>
<description>A wireless client connected to a UniFi wireless network</description>
<channels>
<!-- common wired + wireless client channels -->
<channel id="online" typeId="online"/>
<channel id="site" typeId="site"/>
<channel id="macAddress" typeId="macAddress"/>
<channel id="ipAddress" typeId="ipAddress"/>
<channel id="uptime" typeId="uptime"/>
<channel id="lastSeen" typeId="lastSeen"/>
<channel id="blocked" typeId="blocked"/>
<!-- additional wireless client channels -->
<channel id="ap" typeId="ap"/>
<channel id="essid" typeId="essid"/>
<channel id="rssi" typeId="rssi"/>
<channel id="reconnect" typeId="reconnect"/>
</channels>
<representation-property>cid</representation-property>
<config-description>
<parameter name="cid" type="text" required="true">
<label>Client ID</label>
<description>The MAC address, IP address, hostname or alias of the client</description>
</parameter>
<parameter name="site" type="text" required="false">
<label>Site</label>
<description>The site where the client should be found (optional)</description>
</parameter>
<parameter name="considerHome" type="integer" required="false">
<label>Consider Home Interval</label>
<description>The interval in seconds to consider the client as home</description>
<default>180</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="online">
<item-type>Switch</item-type>
<label>Online</label>
<description>Online status of the wireless client</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="site">
<item-type>String</item-type>
<label>Site Name</label>
<description>UniFi Site the client is associated with</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="macAddress">
<item-type>String</item-type>
<label>MAC Address</label>
<description>MAC address of the client</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="ipAddress">
<item-type>String</item-type>
<label>IP Address</label>
<description>IP address of the client</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="uptime">
<item-type>Number</item-type>
<label>Uptime</label>
<description>Uptime of the client (in seconds)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="lastSeen">
<item-type>DateTime</item-type>
<label>Last Seen</label>
<description>Timestamp of when the client was last seen</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="ap">
<item-type>String</item-type>
<label>Access Point</label>
<description>Access Point the wireless client is connected to</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="essid">
<item-type>String</item-type>
<label>Wireless Network</label>
<description>Wireless Network (ESSID) the wireless client is connected to</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="rssi">
<item-type>Number</item-type>
<label>Received Signal Strength Indicator</label>
<description>Received Signal Strength Indicator (RSSI) of the wireless client</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="blocked">
<item-type>Switch</item-type>
<label>Blocked</label>
<description>Is device blocked</description>
</channel-type>
<channel-type id="reconnect">
<item-type>Switch</item-type>
<label>Reconnect</label>
<description>Forces a client to reconnect</description>
</channel-type>
</thing:thing-descriptions>