]> git.basschouten.com Git - openhab-addons.git/commitdiff
[unifi] New site, wlan, wiredClient, and poePort. Discovery support (#11959)
authorHilbrand Bouwkamp <hilbrand@h72.nl>
Mon, 16 May 2022 21:14:00 +0000 (23:14 +0200)
committerGitHub <noreply@github.com>
Mon, 16 May 2022 21:14:00 +0000 (23:14 +0200)
* [unifi] New wiredClient and poePort, Discovery support

This change adds the following changes:
- 2 new things: a wired client and POE port.
- Adds discovery of clients and poePort.
- Adds guest channel to client thing.
Also included some refactoring and bug fixes.

This change includes changes made by Matthew Bowman that he created on his own branch but were never completed.

Closes #9609: Implemented async http call, which should fix the buffer overflow.
Closes #10375: At least should avoid the stack overflow.
Closes #11964: cid will be handled in lower case.

* Removed type from UniFiCache constructor

It's redundant and only used for logging.

* Added UniFi Site and wLAN things

* Improved default state handling

Updated refresh/state update, to also update when no data available.
Simplified usage of cache: call cache directly instead of implicit via controller class.
Made getDefaultState generic to all things, and simplified passing channelId instead of channelUID to sub methods.

* Moved dto objects to dto package.

* Added support for client experience

* Made fields private

No need to have them protected.

* Added PoE power-cycle command

Also added wireless client as command as this better fits with the openHAB model to handle commands that are only one way and not have a state.

* Updated readme

* [unifi] Added client/guest count to wlan

* Fix QRcode construction and added hidden ssid support in qrcode string

Also-by: Matthew Bowman <mgb@otr.mx>
Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
Co-authored-by: Matthew Bowman <mgb@otr.mx>
64 files changed:
bundles/org.openhab.binding.unifi/README.md
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiBindingConstants.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiClientThingConfig.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiControllerThingConfig.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiPoePortThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiSiteThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiThingHandlerFactory.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiWlanThingConfig.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiCommunicationException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiController.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiControllerRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiExpiredSessionException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiInvalidCredentialsException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiInvalidHostException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiNotAuthorizedException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiSSLException.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiCache.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiClientCache.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiControllerCache.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiDeviceCache.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiSiteCache.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiWlanCache.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/HasId.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UnfiPortOverride.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiClient.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiDevice.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortOverrides.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortTable.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiSite.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiUnknownClient.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWiredClient.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWirelessClient.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWlan.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiClient.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiController.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiControllerRequest.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiDevice.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiSite.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiUnknownClient.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWiredClient.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWirelessClient.java [deleted file]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiClientDeserializer.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiClientInstanceCreator.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiDeviceInstanceCreator.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiSiteInstanceCreator.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiTidyLowerCaseStringDeserializer.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiTimestampDeserializer.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiWlanInstanceCreator.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiBaseThingHandler.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiClientThingHandler.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiControllerThingHandler.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiPoePortThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiSiteThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiThingDiscoveryService.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiWlanThingHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/ssl/UniFiTrustManager.java
bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/ssl/UniFiTrustManagerProvider.java
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi.properties
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi_de.properties
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi_hu.properties
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi_nl.properties [new file with mode: 0644]
bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/thing/thing-types.xml

index 2edd316713cf68ef98cf088c1190f1843b456915..3135eeed7ab1736e0c864d0edd9f891635eec8ae 100644 (file)
@@ -5,23 +5,26 @@ This binding integrates with [Ubiquiti UniFi Networks](https://www.ubnt.com/prod
 
 ## Supported Things
 
-* `controller` - An instance of the UniFi controller software 
+* `controller` - An instance of the UniFi controller software
+* `site` - A site thing with connection statistics
+* `wlan` - A wireless network thing. Control Wi-Fi network and easy access to access.
 * `wirelessClient` - Any wireless client connected to a UniFi wireless network
-
+* `wiredClient` - A wired client connected to the UniFi network
+* `poePort` - A PoE (Power over Ethernet) port on a UniFi switch
 
 ## Discovery
 
-Discovery is currently not supported.
-
+The binding supports discovery of things connected to a UniFi controller (Bridge).
+To discover things start the discovery process manually.
 
 ## 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).    
+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:
 
@@ -36,9 +39,28 @@ The following table describes the Bridge configuration parameters:
 
 ## Thing Configuration
 
-You must define a UniFi Controller (Bridge) before defining UniFi Clients (Things) for this binding to work.
+You must define a UniFi Controller (Bridge) before defining UniFi Things for this binding to work.
+
+### `site`
+
+The following table describes the `site` configuration parameters:
+
+| Parameter    | Description                                                  | Config   | Default |
+| ------------ | -------------------------------------------------------------|--------- | ------- |
+| sid          | The name, description or id of the site                      | Required | -       |
+
+### `wlan`
+
+The following table describes the `wlan` configuration parameters:
+
+| Parameter    | Description                                                  | Config   | Default |
+| ------------ | -------------------------------------------------------------|--------- | ------- |
+| wid          | The name or id of the WLAN                                   | Required | -       |
+
+### `wirelessClient` & `wiredClient`
+
+The following table describes the `wirelessClient` & `wiredClient` configuration parameters:
 
-The following table describes the Thing configuration parameters:
 
 | Parameter    | Description                                                  | Config   | Default |
 | ------------ | -------------------------------------------------------------|--------- | ------- |
@@ -56,8 +78,10 @@ The `cid` parameter is a universal "client identifier". It accepts the following
   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.   
+
+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`
 
@@ -69,37 +93,124 @@ Additionally, you may use friendly site names as they appear in the controller U
 
 ##### `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`
+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`.
+
+### `poePort`
+
+The following table describes the `poePort` configuration parameters:
+
+
+| Parameter  | Description                                               | Config   |
+|------------|-----------------------------------------------------------|----------|
+| portNumber | The port number as reported by the switch (starts with 1) | Required |
+| macAddress | The MAC address of the switch device the port is part of  | Required |
 
 
 ## 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       |
+### `site`
+
+The `site` information that is retrieved is available as these channels:
+
+| Channel ID      | Item Type | Description                          | Permissions |
+|-----------------|-----------|--------------------------------------|-------------|
+| totalClients    | Number    | Total number of clients connected    | Read        |
+| wirelessClients | Number    | Number of wireless clients connected | Read        |
+| wiredClients    | Number    | Number of wired clients connected    | Read        |
+| guestClients    | Number    | Number of guest clients connected    | Read        |
+
+### `wlan`
+
+The `wlan` information that is retrieved is available as these channels:
+
+| Channel ID      | Item Type | Description                                                                     | Permissions |
+|-----------------|-----------|---------------------------------------------------------------------------------|-------------|
+| enable          | Switch    | Enable status of the WLAN                                                       | Read, Write |
+| wirelessClients | Number    | Number of wireless clients connected                                            | Read        |
+| guestClients    | Number    | Number of guest clients connected                                               | Read        |
+| essid           | String    | Wireless Network (ESSID)                                                        | Read        |
+| site            | String    | UniFi Site the client is associated with                                        | Read        |
+| security        | String    | Security protocol of the Wi-Fi network                                          | Read        |
+| wlanBand        | String    | Wireless LAN band of the Wi-Fi network                                          | Read        |
+| wpaEnc          | String    | WPA Encoding of the Wi-Fi network                                               | Read        |
+| wpaMode         | String    | WPA Mode of the Wi-Fi network                                                   | Read        |
+| passphrase      | String    | Passphrase of the Wi-Fi network                                                 | Read        |
+| qrcodeEncoding  | String    | MECARD like encoding to generate a QR Code for easy access to the Wi-Fi network | Read        |
+
+::: warning Attention
+If you link an item to the `passphrase` or `qrcodeEncoding` channel your Wi-Fi password will be  exposed in openHAB.
+The password will also be visible in openHAB event log.
+:::
+
+The `qrcodeEncoding` channel can be used to easily create a QR Code to access, for example, a guest network.
+It contains a MECARD like representation of the access.
+This is the notation used in QR Codes that can be scanned by mobile phones.
+
+### `wirelessClient`
+
+The `wirelessClient` 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        |
+| guest      | Switch               | On if this is a guest 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 client (in seconds)                                    | Read        |
+| lastSeen   | DateTime             | Date and Time the client was last seen                               | Read        |
+| experience | Number:Dimensionless | Overall health indication of the client (in percentage)              | Read        |
+| blocked    | Switch               | Blocked status of the client                                         | Read, Write |
+| cmd        | String               | Command channel: `reconnect` to force the client to reconnect        | Write       |
+| reconnect  | Switch               | Force the client to reconnect                                        | Write       |
+
 
 _Note: All channels with the Write permission require administrator credentials as defined in the controller._
 
+### `wiredClient`
+
+The `wiredClient` 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        |
+| uptime     | Number               | Uptime of the client (in seconds)                                    | Read        |
+| lastSeen   | DateTime             | Date and Time the client was last seen                               | Read        |
+| experience | Number:Dimensionless | Overall health indication of the client (in percentage)              | Read        |
+| blocked    | Switch               | Blocked status of the client                                         | Read, Write |
+
 ##### `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.
+The `reconnect` channel allows you to force a client to reconnect.
+Sending `ON` to this channel will trigger a reconnect via the controller.
+
+### `poePort`
+
+The `poePort` information that is retrieved is available as these channels:
+
+| Channel ID | Item Type                | Description                                        | Permissions |
+|------------|--------------------------|----------------------------------------------------|-------------|
+| online     | Switch                   | Online status of the port                          | Read        |
+| mode       | Selection                | Select the PoE mode: off, auto, 24v or passthrough | Read, Write |
+| enable     | Switch                   | Enable Power over Ethernet                         | Read, Write |
+| cmd        | String                   | Command channel: `power-cycle`: Power Cycle port   | Write       |
+| power      | Number:Power             | Power consumption of the port in Watt              | Read        |
+| voltage    | Number:ElectricPotential | Voltage of the port in Volt                        | Read        |
+| current    | Number:ElectricCurrent   | Current used by the port in mA                     | Read        |
 
+The `enable` switch channel has a configuration parameter `mode` which is the value used to switch PoE on when the channel is switched to ON.
+The default mode value is `auto`.
 
 ## Full Example
 
@@ -126,7 +237,7 @@ String   MatthewsPhoneAP         "Matthew's iPhone: AP [%s]"
 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" } 
+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" }
 ```
index ed7ac82a856209fc1aac28923ef405d7f8f3ba99..5f994d3502ea2b2c7e86e2579ce671f4a4b895c5 100644 (file)
@@ -12,6 +12,9 @@
  */
 package org.openhab.binding.unifi.internal;
 
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.thing.ThingTypeUID;
 
 /**
@@ -20,15 +23,38 @@ import org.openhab.core.thing.ThingTypeUID;
  *
  * @author Matthew Bowman - Initial contribution
  * @author Patrik Wimnell - Blocking / Unblocking client support
+ * @author Hilbrand Bouwkamp - Added poePort
  */
-public class UniFiBindingConstants {
+@NonNullByDefault
+public final 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_SITE = new ThingTypeUID(BINDING_ID, "site");
+    public static final ThingTypeUID THING_TYPE_WLAN = new ThingTypeUID(BINDING_ID, "wlan");
     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");
+    public static final ThingTypeUID THING_TYPE_POE_PORT = new ThingTypeUID(BINDING_ID, "poePort");
+    public static final Set<ThingTypeUID> ALL_THING_TYPE_SUPPORTED = Set.of(THING_TYPE_CONTROLLER, THING_TYPE_SITE,
+            THING_TYPE_WLAN, THING_TYPE_WIRED_CLIENT, THING_TYPE_WIRELESS_CLIENT, THING_TYPE_POE_PORT);
+    public static final Set<ThingTypeUID> THING_TYPE_SUPPORTED = Set.of(THING_TYPE_SITE, THING_TYPE_WLAN,
+            THING_TYPE_WIRED_CLIENT, THING_TYPE_WIRELESS_CLIENT, THING_TYPE_POE_PORT);
+
+    // List of site channels
+    public static final String CHANNEL_TOTAL_CLIENTS = "totalClients";
+    public static final String CHANNEL_WIRELESS_CLIENTS = "wirelessClients";
+    public static final String CHANNEL_WIRED_CLIENTS = "wiredClients";
+    public static final String CHANNEL_GUEST_CLIENTS = "guestClients";
+
+    // List of wlan channels
+    public static final String CHANNEL_SECURITY = "security";
+    public static final String CHANNEL_WLANBAND = "wlanBand";
+    public static final String CHANNEL_WPAENC = "wpaEnc";
+    public static final String CHANNEL_WPAMODE = "wpaMode";
+    public static final String CHANNEL_PASSPHRASE = "passphrase";
+    public static final String CHANNEL_QRCODE_ENCODING = "qrcodeEncoding";
 
     // List of common wired + wireless client channels
     public static final String CHANNEL_ONLINE = "online";
@@ -37,17 +63,31 @@ public class UniFiBindingConstants {
     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_GUEST = "guest";
     public static final String CHANNEL_BLOCKED = "blocked";
     public static final String CHANNEL_RECONNECT = "reconnect";
-
-    // List of additional wired client channels
-    // ..coming soon..
+    public static final String CHANNEL_CMD = "cmd";
+    public static final String CHANNEL_CMD_RECONNECT = "reconnect";
+    public static final String CHANNEL_EXPERIENCE = "experience";
 
     // 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 switch port channels
+    public static final String CHANNEL_ENABLE = "enable";
+    public static final String CHANNEL_ENABLE_PARAMETER_MODE = "mode";
+    public static final String CHANNEL_ENABLE_PARAMETER_MODE_OFF = "off";
+    public static final String CHANNEL_ENABLE_PARAMETER_MODE_AUTO = "auto";
+    public static final String CHANNEL_PORT_POE_MODE = "mode";
+    public static final String CHANNEL_PORT_POE_CMD = "cmd";
+    public static final String CHANNEL_PORT_POE_CMD_POWER_CYCLE = "powercycle";
+    public static final String CHANNEL_PORT_POE_ENABLE = "enable";
+    public static final String CHANNEL_PORT_POE_POWER = "power";
+    public static final String CHANNEL_PORT_POE_VOLTAGE = "voltage";
+    public static final String CHANNEL_PORT_POE_CURRENT = "current";
+
     // List of all Parameters
     public static final String PARAMETER_HOST = "host";
     public static final String PARAMETER_PORT = "port";
@@ -56,4 +96,13 @@ public class UniFiBindingConstants {
     public static final String PARAMETER_UNIFIOS = "unifios";
     public static final String PARAMETER_SITE = "site";
     public static final String PARAMETER_CID = "cid";
+    public static final String PARAMETER_SID = "sid";
+    public static final String PARAMETER_WID = "wid";
+    public static final String PARAMETER_PORT_NUMBER = "portNumber";
+    public static final String PARAMETER_MAC_ADDRESS = "macAddress";
+    public static final String PARAMETER_WIFI_NAME = "wifi";
+
+    private UniFiBindingConstants() {
+        // Constants class
+    }
 }
index 7eecdcf823fdb2396fa0d17e64ed0b782b10e225..1ea54d467d66ebfa592cc311b204ca46420bfbf4 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.unifi.internal;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.unifi.internal.handler.UniFiClientThingHandler;
 
 /**
@@ -20,6 +21,8 @@ import org.openhab.binding.unifi.internal.handler.UniFiClientThingHandler;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
+@SuppressWarnings("unused")
 public class UniFiClientThingConfig {
 
     private String cid = "";
@@ -32,24 +35,33 @@ public class UniFiClientThingConfig {
         return cid;
     }
 
+    private void setCid(final String cid) {
+        // method to avoid ide auto format mark the field as final
+        this.cid = cid;
+    }
+
     public String getSite() {
         return site;
     }
 
-    public int getConsiderHome() {
-        return considerHome;
+    private void setSite(final String site) {
+        // method to avoid ide auto format mark the field as final
+        this.site = site;
     }
 
-    public UniFiClientThingConfig tidy() {
-        cid = cid.trim().toLowerCase();
-        site = site.trim().toLowerCase();
-        return this;
+    public int getConsiderHome() {
+        return considerHome;
     }
 
     public boolean isValid() {
         return !cid.isBlank();
     }
 
+    private void setConsiderHome(final int considerHome) {
+        // method to avoid ide auto format mark the field as final
+        this.considerHome = considerHome;
+    }
+
     @Override
     public String toString() {
         return String.format("UniFiClientConfig{cid: '%s', site: '%s', considerHome: %d}", cid, site, considerHome);
index ad5a136a199c848ffa2ee88a8101ec386d24d44a..d1ba889bf869a5e5af2676ea2e8237926ca7b151 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.unifi.internal;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler;
 
 /**
@@ -20,6 +21,8 @@ import org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
+@SuppressWarnings("unused")
 public class UniFiControllerThingConfig {
 
     private String host = "unifi";
@@ -38,26 +41,56 @@ public class UniFiControllerThingConfig {
         return host;
     }
 
+    private void setHost(final String host) {
+        // method to avoid ide auto format mark the field as final
+        this.host = host;
+    }
+
     public int getPort() {
         return port;
     }
 
+    private void setPort(final int port) {
+        // method to avoid ide auto format mark the field as final
+        this.port = port;
+    }
+
     public String getUsername() {
         return username;
     }
 
+    private void setUsername(final String username) {
+        // method to avoid ide auto format mark the field as final
+        this.username = username;
+    }
+
     public String getPassword() {
         return password;
     }
 
+    private void setPassword(final String password) {
+        // method to avoid ide auto format mark the field as final
+        this.password = password;
+    }
+
     public int getRefresh() {
         return refresh;
     }
 
+    private void setRefresh(final int refresh) {
+        // method to avoid ide auto format mark the field as final
+        this.refresh = refresh;
+    }
+
     public boolean isUniFiOS() {
         return unifios;
     }
 
+    private void setUnifiOS(final boolean unifios) {
+        // method to avoid ide auto format mark the field as final
+        this.unifios = unifios;
+    }
+
     public boolean isValid() {
         return !host.isBlank() && !username.isBlank() && !password.isBlank();
     }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiPoePortThingConfig.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiPoePortThingConfig.java
new file mode 100644 (file)
index 0000000..d631dae
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 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;
+
+/**
+ * The {@link UniFiPoeThingConfig} encapsulates all the configuration options for an instance of the
+ * {@link UniFiPoePortThingHandler}.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("unused")
+public class UniFiPoePortThingConfig {
+
+    private int portNumber;
+
+    private String macAddress = "";
+
+    public int getPortNumber() {
+        return portNumber;
+    }
+
+    public String getMacAddress() {
+        return macAddress;
+    }
+
+    private void setMacAddress(final String macAddress) {
+        // method to avoid ide auto format mark the field as final
+        this.macAddress = macAddress;
+    }
+
+    public boolean isValid() {
+        return !macAddress.isBlank();
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiSiteThingConfig.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiSiteThingConfig.java
new file mode 100644 (file)
index 0000000..24c3428
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 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.openhab.binding.unifi.internal.handler.UniFiSiteThingHandler;
+
+/**
+ * The {@link UniFiSiteThingConfig} encapsulates all the configuration options for an instance of the
+ * {@link UniFiSiteThingHandler}.
+ *
+ * @author Matthew Bowman - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("unused")
+public class UniFiSiteThingConfig {
+
+    private String sid = "";
+
+    public String getSiteID() {
+        return sid;
+    }
+
+    private void setSiteID(final String sid) {
+        // method to avoid ide auto format mark the field as final
+        this.sid = sid;
+    }
+
+    public boolean isValid() {
+        return !sid.isBlank();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UniFiSiteThingConfig{sid: '%s'}", sid);
+    }
+}
index b8ed6312442928f9edc8f1f8e42daee2ffdeb7df..020efec6dc43b367a8398b625cd29f3d635adedb 100644 (file)
  */
 package org.openhab.binding.unifi.internal;
 
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.ALL_THING_TYPE_SUPPORTED;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_CONTROLLER;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_POE_PORT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_SITE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_WIRED_CLIENT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_WIRELESS_CLIENT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.THING_TYPE_WLAN;
+
 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.binding.unifi.internal.handler.UniFiPoePortThingHandler;
+import org.openhab.binding.unifi.internal.handler.UniFiSiteThingHandler;
+import org.openhab.binding.unifi.internal.handler.UniFiWlanThingHandler;
 import org.openhab.core.io.net.http.HttpClientFactory;
 import org.openhab.core.io.net.http.HttpClientInitializationException;
 import org.openhab.core.thing.Bridge;
@@ -26,6 +37,7 @@ 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.ComponentContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
@@ -49,24 +61,40 @@ public class UniFiThingHandlerFactory extends BaseThingHandlerFactory {
         httpClient = new HttpClient(new SslContextFactory.Client(true));
         try {
             httpClient.start();
-        } catch (Exception e) {
+        } catch (final Exception e) {
             throw new HttpClientInitializationException("Could not start HttpClient", e);
         }
     }
 
     @Override
-    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
-        return UniFiControllerThingHandler.supportsThingType(thingTypeUID)
-                || UniFiClientThingHandler.supportsThingType(thingTypeUID);
+    protected void deactivate(final ComponentContext componentContext) {
+        try {
+            httpClient.stop();
+        } catch (final Exception e) {
+            // Eat http client stop exception.
+        } finally {
+            super.deactivate(componentContext);
+        }
+    }
+
+    @Override
+    public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
+        return ALL_THING_TYPE_SUPPORTED.contains(thingTypeUID);
     }
 
     @Override
-    protected @Nullable ThingHandler createHandler(Thing thing) {
-        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
-        if (UniFiControllerThingHandler.supportsThingType(thingTypeUID)) {
+    protected @Nullable ThingHandler createHandler(final Thing thing) {
+        final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+        if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
             return new UniFiControllerThingHandler((Bridge) thing, httpClient);
-        } else if (UniFiClientThingHandler.supportsThingType(thingTypeUID)) {
+        } else if (THING_TYPE_SITE.equals(thingTypeUID)) {
+            return new UniFiSiteThingHandler(thing);
+        } else if (THING_TYPE_WLAN.equals(thingTypeUID)) {
+            return new UniFiWlanThingHandler(thing);
+        } else if (THING_TYPE_WIRELESS_CLIENT.equals(thingTypeUID) || THING_TYPE_WIRED_CLIENT.equals(thingTypeUID)) {
             return new UniFiClientThingHandler(thing);
+        } else if (THING_TYPE_POE_PORT.equals(thingTypeUID)) {
+            return new UniFiPoePortThingHandler(thing);
         }
         return null;
     }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiWlanThingConfig.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/UniFiWlanThingConfig.java
new file mode 100644 (file)
index 0000000..8725067
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2010-2022 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.openhab.binding.unifi.internal.handler.UniFiWlanThingHandler;
+
+/**
+ * The {@link UniFiWlanThingConfig} encapsulates all the configuration options for an instance of the
+ * {@link UniFiWlanThingHandler}.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+@SuppressWarnings("unused")
+public class UniFiWlanThingConfig {
+
+    private String wid = "";
+
+    public String getWlanId() {
+        return wid;
+    }
+
+    private void setWlanId(final String wid) {
+        // method to avoid auto format mark the field as final
+        this.wid = wid;
+    }
+
+    public boolean isValid() {
+        return !wid.isBlank();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UniFiWlanThingConfig{wid: '%s'}", wid);
+    }
+}
index 992fbdf6c4acc97d518cf3f89087ab8cd6b21394..693590d6863882a05b03740ee791a3ac14a7a4ff 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiCommunicationException} signals there was a problem communicating with the controller.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiCommunicationException extends UniFiException {
 
     private static final long serialVersionUID = -7261308872245069364L;
 
-    public UniFiCommunicationException(Throwable cause) {
+    public UniFiCommunicationException(final Throwable cause) {
         super(cause);
     }
 }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiController.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiController.java
new file mode 100644 (file)
index 0000000..d0c6678
--- /dev/null
@@ -0,0 +1,290 @@
+/**
+ * Copyright (c) 2010-2022 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;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.http.HttpMethod;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UnfiPortOverride;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
+import org.openhab.binding.unifi.internal.api.dto.UniFiPortTable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.binding.unifi.internal.api.dto.UniFiUnknownClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+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.openhab.binding.unifi.internal.api.util.UniFiWlanInstanceCreator;
+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
+ * @author Jacob Laursen - Fix online/blocked channels (broken by UniFi Controller 5.12.35)
+ * @author Hilbrand Bouwkamp - Added POEPort support, moved generic cache related code to cache object
+ */
+@NonNullByDefault
+public class UniFiController {
+
+    private static final int INSIGHT_WITHIN_HOURS = 7 * 24; // scurb: Changed to 7 days.
+
+    private final Logger logger = LoggerFactory.getLogger(UniFiController.class);
+
+    private final HttpClient httpClient;
+    private final UniFiControllerCache cache = new UniFiControllerCache();
+
+    private final String host;
+    private final int port;
+    private final String username;
+    private final String password;
+    private final boolean unifios;
+    private final Gson gson;
+    private final Gson poeGson;
+
+    private String csrfToken;
+
+    public UniFiController(final HttpClient httpClient, final String host, final int port, final String username,
+            final String password, final boolean unifios) {
+        this.httpClient = httpClient;
+        this.host = host;
+        this.port = port;
+        this.username = username;
+        this.password = password;
+        this.unifios = unifios;
+        this.csrfToken = "";
+        final UniFiSiteInstanceCreator siteInstanceCreator = new UniFiSiteInstanceCreator(cache);
+        final UniFiWlanInstanceCreator wlanInstanceCreator = new UniFiWlanInstanceCreator(cache);
+        final UniFiDeviceInstanceCreator deviceInstanceCreator = new UniFiDeviceInstanceCreator(cache);
+        final UniFiClientInstanceCreator clientInstanceCreator = new UniFiClientInstanceCreator(cache);
+        this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+                .registerTypeAdapter(UniFiSite.class, siteInstanceCreator)
+                .registerTypeAdapter(UniFiWlan.class, wlanInstanceCreator)
+                .registerTypeAdapter(UniFiDevice.class, deviceInstanceCreator)
+                .registerTypeAdapter(UniFiClient.class, new UniFiClientDeserializer())
+                .registerTypeAdapter(UniFiUnknownClient.class, clientInstanceCreator)
+                .registerTypeAdapter(UniFiWiredClient.class, clientInstanceCreator)
+                .registerTypeAdapter(UniFiWirelessClient.class, clientInstanceCreator).create();
+        this.poeGson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+                .excludeFieldsWithoutExposeAnnotation().create();
+    }
+
+    // Public API
+
+    public void start() throws UniFiException {
+        if (unifios) {
+            obtainCsrfToken();
+        }
+
+        login();
+    }
+
+    public void stop() throws UniFiException {
+        logout();
+    }
+
+    public void obtainCsrfToken() throws UniFiException {
+        csrfToken = "";
+
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.GET, gson);
+        req.setPath("/");
+        executeRequest(req);
+    }
+
+    public void login() throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
+        req.setPath(unifios ? "/api/auth/login" : "/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, true);
+    }
+
+    public void logout() throws UniFiException {
+        csrfToken = "";
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.GET, gson);
+        req.setPath(unifios ? "/api/auth/logout" : "/logout");
+        executeRequest(req);
+    }
+
+    public void refresh() throws UniFiException {
+        synchronized (this) {
+            cache.clear();
+            final Collection<UniFiSite> sites = refreshSites();
+            refreshWlans(sites);
+            refreshDevices(sites);
+            refreshClients(sites);
+            refreshInsights(sites);
+        }
+    }
+
+    public UniFiControllerCache getCache() {
+        return cache;
+    }
+
+    public @Nullable Map<Integer, UniFiPortTable> getSwitchPorts(@Nullable final String deviceId) {
+        return cache.getSwitchPorts(deviceId);
+    }
+
+    public void block(final UniFiClient client, final boolean blocked) throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
+        req.setAPIPath(String.format("/api/s/%s/cmd/stamgr", client.getSite().getName()));
+        req.setBodyParameter("cmd", blocked ? "block-sta" : "unblock-sta");
+        req.setBodyParameter("mac", client.getMac());
+        executeRequest(req);
+        refresh();
+    }
+
+    public void reconnect(final UniFiClient client) throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
+        req.setAPIPath(String.format("/api/s/%s/cmd/stamgr", client.getSite().getName()));
+        req.setBodyParameter("cmd", "kick-sta");
+        req.setBodyParameter("mac", client.getMac());
+        executeRequest(req);
+        refresh();
+    }
+
+    public void poeMode(final UniFiDevice device, final Map<Integer, UnfiPortOverride> data) throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.PUT, poeGson);
+        req.setAPIPath(String.format("/api/s/%s/rest/device/%s", device.getSite().getName(), device.getId()));
+        req.setBodyParameter("port_overrides", data.values());
+        executeRequest(req);
+        refresh();
+    }
+
+    public void poePowerCycle(final UniFiDevice device, final Integer portIdx) throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);
+        req.setAPIPath(String.format("/api/s/%s/cmd/devmgr", device.getSite().getName()));
+        req.setBodyParameter("cmd", "power-cycle");
+        req.setBodyParameter("mac", device.getMac());
+        req.setBodyParameter("port_idx", portIdx);
+        executeRequest(req);
+        refresh();
+    }
+
+    public void enableWifi(final UniFiWlan wlan, final boolean enable) throws UniFiException {
+        final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.PUT, poeGson);
+        req.setAPIPath(String.format("/api/s/%s/rest/wlanconf/%s", wlan.getSite().getName(), wlan.getId()));
+        req.setBodyParameter("_id", wlan.getId());
+        req.setBodyParameter("enabled", enable ? "true" : "false");
+        executeRequest(req);
+        refresh();
+    }
+
+    // Internal API
+
+    private <T> UniFiControllerRequest<T> newRequest(final Class<T> responseType, final HttpMethod method,
+            final Gson gson) {
+        return new UniFiControllerRequest<>(responseType, gson, httpClient, method, host, port, csrfToken, unifios);
+    }
+
+    private <T> @Nullable T executeRequest(final UniFiControllerRequest<T> request) throws UniFiException {
+        return executeRequest(request, false);
+    }
+
+    private <T> @Nullable T executeRequest(final UniFiControllerRequest<T> request, final boolean fromLogin)
+            throws UniFiException {
+        T result;
+        try {
+            result = request.execute();
+            csrfToken = request.getCsrfToken();
+        } catch (final UniFiExpiredSessionException e) {
+            if (fromLogin) {
+                // if this exception is thrown from a login attempt something is wrong, because the login should init
+                // the session.
+                throw new UniFiCommunicationException(e);
+            } else {
+                login();
+                result = executeRequest(request);
+            }
+        } catch (final UniFiNotAuthorizedException e) {
+            logger.warn("Not Authorized! Please make sure your controller credentials have administrator rights");
+            result = null;
+        }
+        return result;
+    }
+
+    private List<UniFiSite> refreshSites() throws UniFiException {
+        final UniFiControllerRequest<UniFiSite[]> req = newRequest(UniFiSite[].class, HttpMethod.GET, gson);
+        req.setAPIPath("/api/self/sites");
+        return cache.setSites(executeRequest(req));
+    }
+
+    private void refreshWlans(final Collection<UniFiSite> sites) throws UniFiException {
+        for (final UniFiSite site : sites) {
+            cache.putWlans(getWlans(site));
+        }
+    }
+
+    private UniFiWlan @Nullable [] getWlans(final UniFiSite site) throws UniFiException {
+        final UniFiControllerRequest<UniFiWlan[]> req = newRequest(UniFiWlan[].class, HttpMethod.GET, gson);
+        req.setAPIPath(String.format("/api/s/%s/rest/wlanconf", site.getName()));
+        return executeRequest(req);
+    }
+
+    private void refreshDevices(final Collection<UniFiSite> sites) throws UniFiException {
+        for (final UniFiSite site : sites) {
+            cache.putDevices(getDevices(site));
+        }
+    }
+
+    private UniFiDevice @Nullable [] getDevices(final UniFiSite site) throws UniFiException {
+        final UniFiControllerRequest<UniFiDevice[]> req = newRequest(UniFiDevice[].class, HttpMethod.GET, gson);
+        req.setAPIPath(String.format("/api/s/%s/stat/device", site.getName()));
+        return executeRequest(req);
+    }
+
+    private void refreshClients(final Collection<UniFiSite> sites) throws UniFiException {
+        for (final UniFiSite site : sites) {
+            cache.putClients(getClients(site));
+        }
+    }
+
+    private UniFiClient @Nullable [] getClients(final UniFiSite site) throws UniFiException {
+        final UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class, HttpMethod.GET, gson);
+        req.setAPIPath(String.format("/api/s/%s/stat/sta", site.getName()));
+        return executeRequest(req);
+    }
+
+    private void refreshInsights(final Collection<UniFiSite> sites) throws UniFiException {
+        for (final UniFiSite site : sites) {
+            cache.putInsights(getInsights(site));
+        }
+    }
+
+    private UniFiClient @Nullable [] getInsights(final UniFiSite site) throws UniFiException {
+        final UniFiControllerRequest<UniFiClient[]> req = newRequest(UniFiClient[].class, HttpMethod.GET, gson);
+        req.setAPIPath(String.format("/api/s/%s/stat/alluser", site.getName()));
+        req.setQueryParameter("within", INSIGHT_WITHIN_HOURS);
+        return executeRequest(req);
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiControllerRequest.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/UniFiControllerRequest.java
new file mode 100644 (file)
index 0000000..4acf1e0
--- /dev/null
@@ -0,0 +1,255 @@
+/**
+ * Copyright (c) 2010-2022 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+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.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.client.util.InputStreamResponseListener;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+/**
+ * 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
+class UniFiControllerRequest<T> {
+
+    private static final String CONTENT_TYPE_APPLICATION_JSON_UTF_8 = MimeTypes.Type.APPLICATION_JSON_UTF_8.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 final boolean unifios;
+
+    private final HttpMethod method;
+
+    private String path = "/";
+
+    private String csrfToken;
+
+    private final Map<String, String> queryParameters = new HashMap<>();
+
+    private final Map<String, Object> bodyParameters = new HashMap<>();
+
+    private final Class<T> resultType;
+
+    // Public API
+
+    public UniFiControllerRequest(final Class<T> resultType, final Gson gson, final HttpClient httpClient,
+            final HttpMethod method, final String host, final int port, final String csrfToken, final boolean unifios) {
+        this.resultType = resultType;
+        this.gson = gson;
+        this.httpClient = httpClient;
+        this.method = method;
+        this.host = host;
+        this.port = port;
+        this.csrfToken = csrfToken;
+        this.unifios = unifios;
+    }
+
+    public void setAPIPath(final String relativePath) {
+        if (unifios) {
+            this.path = "/proxy/network" + relativePath;
+        } else {
+            this.path = relativePath;
+        }
+    }
+
+    public void setPath(final String path) {
+        this.path = path;
+    }
+
+    public void setBodyParameter(final String key, final Object value) {
+        this.bodyParameters.put(key, value);
+    }
+
+    public void setQueryParameter(final String key, final Object value) {
+        this.queryParameters.put(key, String.valueOf(value));
+    }
+
+    public @Nullable T execute() throws UniFiException {
+        T result = null;
+        final String json = getContent();
+        // mgb: only try and unmarshall non-void result types
+        if (!Void.class.equals(resultType)) {
+            final JsonObject jsonObject = JsonParser.parseString(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;
+        final InputStreamResponseListener listener = new InputStreamResponseListener();
+        final Response response = getContentResponse(listener);
+        final int status = response.getStatus();
+        switch (status) {
+            case HttpStatus.OK_200:
+                content = responseToString(listener);
+                if (logger.isTraceEnabled()) {
+                    logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
+                }
+
+                final String csrfToken = response.getHeaders().get("X-CSRF-Token");
+                if (csrfToken != null && !csrfToken.isEmpty()) {
+                    this.csrfToken = csrfToken;
+                }
+                break;
+            case HttpStatus.BAD_REQUEST_400:
+                logger.info("UniFi returned a status 400: {}", prettyPrintJson(responseToString(listener)));
+                throw new UniFiInvalidCredentialsException("Invalid Credentials");
+            case HttpStatus.UNAUTHORIZED_401:
+                throw new UniFiExpiredSessionException("Expired Credentials");
+            case HttpStatus.FORBIDDEN_403:
+                throw new UniFiNotAuthorizedException("Unauthorized Access");
+            default:
+                logger.info("UniFi returned a status code {}: {}", status, prettyPrintJson(responseToString(listener)));
+                throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
+        }
+        return content;
+    }
+
+    private Response getContentResponse(final InputStreamResponseListener listener) throws UniFiException {
+        final Request request = newRequest();
+        logger.trace(">> {} {}", request.getMethod(), request.getURI());
+        Response response;
+        try {
+            request.send(listener);
+            response = listener.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        } catch (TimeoutException | InterruptedException e) {
+            throw new UniFiCommunicationException(e);
+        } catch (final ExecutionException e) {
+            // mgb: unwrap the cause and try to cleanly handle it
+            final 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 ExecutionException to be thrown
+                // - we unwrap the response from the exception for proper handling of the 401 status code
+                response = ((HttpResponseException) cause).getResponse();
+            } else {
+                // catch all
+                throw new UniFiException(cause);
+            }
+        }
+        return response;
+    }
+
+    private static String responseToString(final InputStreamResponseListener listener) throws UniFiException {
+        final ByteArrayOutputStream responseContent = new ByteArrayOutputStream();
+        try (InputStream input = listener.getInputStream()) {
+            input.transferTo(responseContent);
+        } catch (final IOException e) {
+            throw new UniFiException(e);
+        }
+        return new String(responseContent.toByteArray(), StandardCharsets.UTF_8);
+    }
+
+    public String getCsrfToken() {
+        return csrfToken;
+    }
+
+    private Request newRequest() {
+        final HttpURI uri = new HttpURI(HttpScheme.HTTPS.asString(), host, port, path);
+        final Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+                .method(method);
+        for (final Entry<String, String> entry : queryParameters.entrySet()) {
+            request.param(entry.getKey(), entry.getValue());
+        }
+        if (!bodyParameters.isEmpty()) {
+            final String jsonBody = gson.toJson(bodyParameters);
+
+            request.content(
+                    new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON_UTF_8, jsonBody, StandardCharsets.UTF_8));
+        }
+
+        if (!csrfToken.isEmpty()) {
+            request.header("x-csrf-token", this.csrfToken);
+        }
+
+        return request;
+    }
+
+    private static String prettyPrintJson(final String content) {
+        try {
+            final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
+            final Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
+
+            return prettyGson.toJson(json);
+        } catch (final RuntimeException e) {
+            // If could not parse the string as json, just return the string
+            return content;
+        }
+    }
+}
index 93f58b9f1985c0245c03726204c7ad5d29f9860d..ce92b56e6bce65aec230548ac8accf72a52b2ae1 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * The {@link UniFiException} represents a binding specific {@link Exception}.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiException extends Exception {
 
     private static final long serialVersionUID = -7422254981644510570L;
 
-    public UniFiException(String message) {
+    public UniFiException(final String message) {
         super(message);
     }
 
-    public UniFiException(String message, Throwable cause) {
+    public UniFiException(final String message, final Throwable cause) {
         super(message, cause);
     }
 
-    public UniFiException(Throwable cause) {
+    public UniFiException(final @Nullable Throwable cause) {
         super(cause);
     }
 }
index c909525c4ded3a8de8d849ac02a501165e3f6949..cc801aa72271006d0b6ad3dd6d0899e061438d54 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiExpiredSessionException} signals the session with the controller has expired.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiExpiredSessionException extends UniFiException {
 
     private static final long serialVersionUID = -2002650048964514035L;
 
-    public UniFiExpiredSessionException(String message) {
+    public UniFiExpiredSessionException(final String message) {
         super(message);
     }
 }
index 8cf8a681a12fde3bb0d445d025108fd79049b043..ee5fde3c2b0851e0a86eb87f84ee32aa532603bd 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiInvalidCredentialsException} signals the credentials used to authenticate with the controller are
  * invalid.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiInvalidCredentialsException extends UniFiException {
 
     private static final long serialVersionUID = -7159360851783088458L;
 
-    public UniFiInvalidCredentialsException(String message) {
+    public UniFiInvalidCredentialsException(final String message) {
         super(message);
     }
 }
index ec9ea6a912d0b3a5c2d1937db9a22e692ddb666f..493897f421f2ecd124bb7db54d3e9654677d88f7 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiInvalidHostException} signals there was a problem with the hostname of the controller.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiInvalidHostException extends UniFiException {
 
     private static final long serialVersionUID = -7261308872245069364L;
 
-    public UniFiInvalidHostException(String message) {
+    public UniFiInvalidHostException(final String message) {
         super(message);
     }
 
-    public UniFiInvalidHostException(String message, Throwable cause) {
+    public UniFiInvalidHostException(final String message, final Throwable cause) {
         super(message, cause);
     }
 
-    public UniFiInvalidHostException(Throwable cause) {
+    public UniFiInvalidHostException(final Throwable cause) {
         super(cause);
     }
 }
index e89b42447b51f6871686fa969d070c0b115dcfa1..1bc40ef4496771e5f75579c3c3ff048e46d40b6b 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiNotAuthorizedException} signals the controller denied a request due to non-admin credentials.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiNotAuthorizedException extends UniFiException {
 
     private static final long serialVersionUID = 1379973398415636322L;
 
-    public UniFiNotAuthorizedException(String message) {
+    public UniFiNotAuthorizedException(final String message) {
         super(message);
     }
 
-    public UniFiNotAuthorizedException(String message, Throwable cause) {
+    public UniFiNotAuthorizedException(final String message, final Throwable cause) {
         super(message, cause);
     }
 
-    public UniFiNotAuthorizedException(Throwable cause) {
+    public UniFiNotAuthorizedException(final Throwable cause) {
         super(cause);
     }
 }
index 59397991cb100adac03cea228268545485b583c1..02043f0bd00908c1ff591acbff1b4b84a29fb4af 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * The {@link UniFiSSLException} signals a failure establishing an SSL connection with the controller.
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiSSLException extends UniFiException {
 
     private static final long serialVersionUID = 4688857482270932413L;
 
-    public UniFiSSLException(String message) {
+    public UniFiSSLException(final String message) {
         super(message);
     }
 
-    public UniFiSSLException(String message, Throwable cause) {
+    public UniFiSSLException(final String message, final Throwable cause) {
         super(message, cause);
     }
 
-    public UniFiSSLException(Throwable cause) {
+    public UniFiSSLException(final Throwable cause) {
         super(cause);
     }
 }
index de7cec15d350d0e65c20b5b2e481a5ce95dcc748..44107ff2c208b8eeba8bf99031faddc9c1ebec95 100644 (file)
@@ -14,9 +14,13 @@ package org.openhab.binding.unifi.internal.api.cache;
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.HasId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -29,41 +33,65 @@ import org.slf4j.LoggerFactory;
  * <code>prefix:suffix</code> are searched in the order of their priority.
  *
  * @author Matthew Bowman - Initial contribution
+ * @author Hilbrand Bouwkamp - Moved generic code into this class
  */
-public abstract class UniFiCache<T> {
+@NonNullByDefault
+abstract class UniFiCache<T extends @Nullable HasId> {
+
+    public enum Prefix {
+        ALIAS,
+        DESC,
+        HOSTNAME,
+        ID,
+        IP,
+        MAC,
+        NAME;
+    }
 
     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";
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+    // Map of cid keys to the id.
+    private final Map<String, String> mapToId = new HashMap<>();
+    // Map of id to data object
+    private final Map<String, T> map = new HashMap<>();
+    private final Prefix[] prefixes;
 
-    public static final String PREFIX_NAME = "name";
+    protected UniFiCache(final Prefix... prefixes) {
+        this.prefixes = prefixes;
+    }
 
-    private final Logger logger = LoggerFactory.getLogger(getClass());
+    public void clear() {
+        map.clear();
+    }
 
-    private Map<String, T> map = new HashMap<>();
+    public final @Nullable T get(final @Nullable String cid) {
+        final @Nullable T value;
 
-    private String[] prefixes;
+        if (cid != null && !cid.isBlank()) {
+            synchronized (this) {
+                final String id = getId(cid);
 
-    protected UniFiCache(String... prefixes) {
-        this.prefixes = prefixes;
+                if (id == null) {
+                    logger.debug("Could not find an entry in the cache for id: '{}'", cid);
+                    value = null;
+                } else {
+                    value = map.get(id);
+                }
+            }
+        } else {
+            value = null;
+        }
+        return value;
     }
 
-    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);
+    public @Nullable String getId(final String cid) {
+        String value = null;
+        for (final Prefix prefix : prefixes) {
+            final String key = key(prefix, cid);
+
+            if (mapToId.containsKey(key)) {
+                value = mapToId.get(key);
                 logger.trace("Cache HIT : '{}' -> {}", key, value);
                 break;
             } else {
@@ -73,23 +101,48 @@ public abstract class UniFiCache<T> {
         return value;
     }
 
-    public final void put(T value) {
-        for (String prefix : prefixes) {
-            String suffix = getSuffix(value, prefix);
+    public final void putAll(final T @Nullable [] values) {
+        if (values != null) {
+            logger.debug("Put #{} entries in {}: {}", values.length, getClass().getSimpleName(),
+                    lazyFormatAsList(values));
+            for (final T value : values) {
+                put(value.getId(), value);
+            }
+        }
+    }
+
+    public final void put(final String id, final T value) {
+        for (final Prefix prefix : prefixes) {
+            final String suffix = getSuffix(value, prefix);
+
             if (suffix != null && !suffix.isBlank()) {
-                String key = prefix + SEPARATOR + suffix;
-                map.put(key, value);
+                mapToId.put(key(prefix, suffix), id);
             }
         }
+        map.put(id, value);
     }
 
-    public final void putAll(UniFiCache<T> cache) {
-        map.putAll(cache.map);
+    private static String key(final Prefix prefix, final String suffix) {
+        return prefix.name() + SEPARATOR + suffix.replace(":", "").toLowerCase(Locale.ROOT);
     }
 
     public final Collection<T> values() {
         return map.values().stream().distinct().collect(Collectors.toList());
     }
 
-    protected abstract String getSuffix(T value, String prefix);
+    protected abstract @Nullable String getSuffix(T value, Prefix prefix);
+
+    private static Object lazyFormatAsList(final Object[] arr) {
+        return new Object() {
+
+            @Override
+            public String toString() {
+                String value = "";
+                for (final Object o : arr) {
+                    value += "\n - " + o.toString();
+                }
+                return value;
+            }
+        };
+    }
 }
index eaa07548fe0c69640c6ea4609d2cc9831300a10c..5819577393c722695290acc23dbda29988ae57dd 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api.cache;
 
-import org.openhab.binding.unifi.internal.api.model.UniFiClient;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.ALIAS;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.HOSTNAME;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.ID;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.IP;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.MAC;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
 
 /**
  * The {@link UniFiClientCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
@@ -23,26 +31,28 @@ import org.openhab.binding.unifi.internal.api.model.UniFiClient;
  *
  * @author Matthew Bowman - Initial contribution
  */
-public class UniFiClientCache extends UniFiCache<UniFiClient> {
+@NonNullByDefault
+class UniFiClientCache extends UniFiCache<UniFiClient> {
 
     public UniFiClientCache() {
-        super(PREFIX_ID, PREFIX_MAC, PREFIX_IP, PREFIX_HOSTNAME, PREFIX_ALIAS);
+        super(ID, MAC, IP, HOSTNAME, ALIAS);
     }
 
     @Override
-    protected String getSuffix(UniFiClient client, String prefix) {
+    protected @Nullable String getSuffix(final UniFiClient client, final Prefix prefix) {
         switch (prefix) {
-            case PREFIX_ID:
+            case ID:
                 return client.getId();
-            case PREFIX_MAC:
+            case MAC:
                 return client.getMac();
-            case PREFIX_IP:
+            case IP:
                 return client.getIp();
-            case PREFIX_HOSTNAME:
+            case HOSTNAME:
                 return client.getHostname();
-            case PREFIX_ALIAS:
+            case ALIAS:
                 return client.getAlias();
+            default:
+                return null;
         }
-        return null;
     }
 }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiControllerCache.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiControllerCache.java
new file mode 100644 (file)
index 0000000..174062c
--- /dev/null
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2010-2022 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.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
+import org.openhab.binding.unifi.internal.api.dto.UniFiPortTable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to manager cache for the controller keeping track of all specific cache objects.
+ *
+ * @author Matthew Bowman - Initial contribution
+ * @author Hilbrand Bouwkamp - Moved cache to this dedicated class.
+ */
+@NonNullByDefault
+public class UniFiControllerCache {
+
+    private final Logger logger = LoggerFactory.getLogger(UniFiControllerCache.class);
+
+    private final UniFiSiteCache sitesCache = new UniFiSiteCache();
+    private final UniFiWlanCache wlansCache = new UniFiWlanCache();
+    private final UniFiDeviceCache devicesCache = new UniFiDeviceCache();
+    private final UniFiClientCache clientsCache = new UniFiClientCache();
+    private final UniFiClientCache insightsCache = new UniFiClientCache();
+    private final Map<String, Map<Integer, UniFiPortTable>> devicesToPortTables = new ConcurrentHashMap<>();
+
+    public void clear() {
+        sitesCache.clear();
+        wlansCache.clear();
+        devicesCache.clear();
+        clientsCache.clear();
+        insightsCache.clear();
+    }
+
+    // Sites Cache
+
+    public List<UniFiSite> setSites(final UniFiSite @Nullable [] sites) {
+        sitesCache.putAll(sites);
+        return List.of(sites);
+    }
+
+    public @Nullable UniFiSite getSite(final @Nullable String id) {
+        return sitesCache.get(id);
+    }
+
+    public Collection<UniFiSite> getSites() {
+        return sitesCache.values();
+    }
+
+    // Wlans Cache
+
+    public void putWlans(final UniFiWlan @Nullable [] wlans) {
+        wlansCache.putAll(wlans);
+    }
+
+    public @Nullable UniFiWlan getWlan(@Nullable final String id) {
+        return wlansCache.get(id);
+    }
+
+    public Collection<UniFiWlan> getWlans() {
+        return wlansCache.values();
+    }
+
+    // Devices Cache
+
+    public void putDevices(final UniFiDevice @Nullable [] devices) {
+        devicesCache.putAll(devices);
+        if (devices != null) {
+            Stream.of(devices).filter(Objects::nonNull).forEach(d -> {
+                Stream.ofNullable(d.getPortTable()).filter(ptl -> ptl.length > 0 && ptl[0].isPortPoe()).forEach(pt -> {
+                    Stream.of(pt).forEach(p -> p.setDevice(d));
+                    devicesToPortTables.put(d.getMac(),
+                            Stream.of(pt).collect(Collectors.toMap(UniFiPortTable::getPortIdx, Function.identity())));
+                });
+            });
+        }
+    }
+
+    public @Nullable UniFiDevice getDevice(@Nullable final String id) {
+        return devicesCache.get(id);
+    }
+
+    public Map<Integer, UniFiPortTable> getSwitchPorts(@Nullable final String deviceId) {
+        return deviceId == null ? Map.of() : devicesToPortTables.getOrDefault(deviceId, Map.of());
+    }
+
+    public Collection<Map<Integer, UniFiPortTable>> getSwitchPorts() {
+        return devicesToPortTables.values();
+    }
+
+    // Clients Cache
+
+    public void putClients(final UniFiClient @Nullable [] clients) {
+        clientsCache.putAll(clients);
+    }
+
+    public Collection<UniFiClient> getClients() {
+        return clientsCache.values();
+    }
+
+    public long countClients(final UniFiSite site, final Function<UniFiClient, Boolean> filter) {
+        return getClients().stream().filter(c -> site.isSite(c.getSite())).filter(filter::apply).count();
+    }
+
+    public @Nullable UniFiClient getClient(@Nullable final String cid) {
+        UniFiClient client = null;
+        if (cid != null && !cid.isBlank()) {
+            synchronized (this) {
+                // mgb: first check active clients and fallback to insights if not found
+                client = clientsCache.get(cid);
+                if (client == null) {
+                    final String id = clientsCache.getId(cid);
+
+                    client = insightsCache.get(id == null ? cid : id);
+                }
+            }
+            if (client == null) {
+                logger.debug("Could not find a matching client for cid = {}", cid);
+            }
+        }
+        return client;
+    }
+
+    public synchronized Stream<UniFiClient> getClientStreamForSite(final UniFiSite site) {
+        return clientsCache.values().stream().filter(client -> client.getSite().equals(site));
+    }
+
+    // Insights Cache
+
+    public void putInsights(final UniFiClient @Nullable [] insights) {
+        insightsCache.putAll(insights);
+    }
+}
index 04382cbaa1a1035eeb73807d0fd6801daec787c8..8cfd91c977fd470dbb8d7e057a5bdc68ec2eb9cd 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api.cache;
 
-import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.MAC;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
 
 /**
  * The {@link UniFiDeviceCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
@@ -22,16 +26,17 @@ import org.openhab.binding.unifi.internal.api.model.UniFiDevice;
  *
  * @author Matthew Bowman - Initial contribution
  */
-public class UniFiDeviceCache extends UniFiCache<UniFiDevice> {
+@NonNullByDefault
+class UniFiDeviceCache extends UniFiCache<UniFiDevice> {
 
     public UniFiDeviceCache() {
-        super(PREFIX_MAC);
+        super(MAC);
     }
 
     @Override
-    protected String getSuffix(UniFiDevice device, String prefix) {
+    protected @Nullable String getSuffix(final UniFiDevice device, final Prefix prefix) {
         switch (prefix) {
-            case PREFIX_MAC:
+            case MAC:
                 return device.getMac();
         }
         return null;
index 9a9cbdb48282e961155f51dd66797984746631c3..77f7b658afe0efa48c3e1e7b2c706f2d14be3bfb 100644 (file)
  */
 package org.openhab.binding.unifi.internal.api.cache;
 
-import org.openhab.binding.unifi.internal.api.model.UniFiSite;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.DESC;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.ID;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.NAME;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
 
 /**
  * The {@link UniFiSiteCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
@@ -22,22 +28,24 @@ import org.openhab.binding.unifi.internal.api.model.UniFiSite;
  *
  * @author Matthew Bowman - Initial contribution
  */
-public class UniFiSiteCache extends UniFiCache<UniFiSite> {
+@NonNullByDefault
+class UniFiSiteCache extends UniFiCache<UniFiSite> {
 
     public UniFiSiteCache() {
-        super(PREFIX_ID, PREFIX_NAME, PREFIX_DESC);
+        super(ID, NAME, DESC);
     }
 
     @Override
-    protected String getSuffix(UniFiSite site, String prefix) {
+    protected @Nullable String getSuffix(final UniFiSite site, final Prefix prefix) {
         switch (prefix) {
-            case PREFIX_ID:
+            case ID:
                 return site.getId();
-            case PREFIX_NAME:
+            case NAME:
                 return site.getName();
-            case PREFIX_DESC:
+            case DESC:
                 return site.getDescription();
+            default:
+                return null;
         }
-        return null;
     }
 }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiWlanCache.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/cache/UniFiWlanCache.java
new file mode 100644 (file)
index 0000000..01dc034
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2022 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 static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.ID;
+import static org.openhab.binding.unifi.internal.api.cache.UniFiCache.Prefix.NAME;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+
+/**
+ * The {@link UniFiWlanCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
+ * {@link UniFiWlan} instances.
+ *
+ * The cache uses the following prefixes: <code>id</code>, <code>name</code>
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+class UniFiWlanCache extends UniFiCache<UniFiWlan> {
+
+    public UniFiWlanCache() {
+        super(ID, NAME);
+    }
+
+    @Override
+    protected @Nullable String getSuffix(final UniFiWlan wlan, final Prefix prefix) {
+        switch (prefix) {
+            case ID:
+                return wlan.getId();
+            case NAME:
+                return wlan.getName();
+            default:
+                return null;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/HasId.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/HasId.java
new file mode 100644 (file)
index 0000000..7f03bd1
--- /dev/null
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+/**
+ * Data classes that have an id as identifier.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public interface HasId {
+
+    String getId();
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UnfiPortOverride.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UnfiPortOverride.java
new file mode 100644 (file)
index 0000000..bd4a41f
--- /dev/null
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * The {@link UnfiPortOverride} represents the data model of UniFi port override.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class UnfiPortOverride {
+
+    @Expose
+    private int portIdx;
+
+    @Expose
+    private String portconfId;
+
+    @Expose
+    private String poeMode;
+
+    public UnfiPortOverride() {
+        // Constructor for GSON.
+    }
+
+    public UnfiPortOverride(final int portIdx, final String portconfId, final String poeMode) {
+        this.portIdx = portIdx;
+        this.portconfId = portconfId;
+        this.poeMode = poeMode;
+    }
+
+    public int getPortIdx() {
+        return portIdx;
+    }
+
+    public String getPortconfId() {
+        return portconfId;
+    }
+
+    public String getPoeMode() {
+        return poeMode;
+    }
+
+    public void setPortIdx(final int portIdx) {
+        this.portIdx = portIdx;
+    }
+
+    public void setPortconfId(final String portconfId) {
+        this.portconfId = portconfId;
+    }
+
+    public void setPoeMode(final String poeMode) {
+        this.poeMode = poeMode;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UnfiPortOverride{portIx: '%d', portconfId: '%s', poeMode: '%s'}", portIdx, portconfId,
+                poeMode);
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiClient.java
new file mode 100644 (file)
index 0000000..28def26
--- /dev/null
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import java.time.Instant;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+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 implements HasId {
+
+    private final transient UniFiControllerCache cache;
+
+    @SerializedName("_id")
+    private String id;
+
+    private String siteId;
+
+    @JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
+    private String mac;
+
+    private String ip;
+
+    @JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
+    private String hostname;
+
+    @SerializedName("name")
+    @JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
+    private String alias;
+
+    private Integer uptime;
+
+    @JsonAdapter(UniFiTimestampDeserializer.class)
+    private Instant lastSeen;
+
+    private boolean blocked;
+
+    @SerializedName("is_guest")
+    private boolean guest;
+
+    @SerializedName("fixed_ip")
+    private String fixedIp;
+
+    @SerializedName("satisfaction")
+    private Integer experience;
+
+    protected UniFiClient(final UniFiControllerCache cache) {
+        this.cache = cache;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    public String getMac() {
+        return mac;
+    }
+
+    public String getIp() {
+        return this.ip == null || this.ip.isBlank() ? this.fixedIp : this.ip;
+    }
+
+    public String getHostname() {
+        return hostname;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public Integer getUptime() {
+        return uptime;
+    }
+
+    public Instant getLastSeen() {
+        return lastSeen;
+    }
+
+    public boolean isBlocked() {
+        return blocked;
+    }
+
+    public abstract Boolean isWired();
+
+    public final Boolean isWireless() {
+        return isWired() == null ? null : Boolean.FALSE.equals(isWired());
+    }
+
+    protected abstract String getDeviceMac();
+
+    public UniFiSite getSite() {
+        return cache.getSite(siteId);
+    }
+
+    public UniFiDevice getDevice() {
+        return cache.getDevice(getDeviceMac());
+    }
+
+    public boolean isGuest() {
+        return guest;
+    }
+
+    public Integer getExperience() {
+        return experience;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "UniFiClient{id: '%s', mac: '%s', ip: '%s', hostname: '%s', alias: '%s', wired: %b, guest: %b, blocked: %b, experience: %d, device: %s}",
+                id, mac, getIp(), hostname, alias, isWired(), guest, blocked, experience, getDevice());
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiDevice.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiDevice.java
new file mode 100644 (file)
index 0000000..0cd1e1b
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+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
+ * @author Hilbrand Bouwkamp - Added PoEPort support
+ */
+public class UniFiDevice implements HasId {
+
+    protected final transient UniFiControllerCache cache;
+
+    @SerializedName("_id")
+    private String id;
+
+    @JsonAdapter(UniFiTidyLowerCaseStringDeserializer.class)
+    private String mac;
+
+    private String model;
+
+    private String name;
+
+    private String siteId;
+
+    private UniFiPortTable[] portTable;
+
+    public UniFiDevice(final UniFiControllerCache cache) {
+        this.cache = cache;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    public String getModel() {
+        return model;
+    }
+
+    public String getName() {
+        return name == null || name.isBlank() ? mac : name;
+    }
+
+    public String getMac() {
+        return mac;
+    }
+
+    public UniFiSite getSite() {
+        return cache.getSite(siteId);
+    }
+
+    public UniFiPortTable[] getPortTable() {
+        return portTable;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UniFiDevice{mac: '%s', name: '%s', model: '%s', site: %s}", mac, name, model, getSite());
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortOverrides.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortOverrides.java
new file mode 100644 (file)
index 0000000..f664578
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gson.annotations.Expose;
+
+/**
+ * The {@link UniFiPortOverrides} represents the data model of UniFi port overrides.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class UniFiPortOverrides {
+
+    @Expose
+    private final List<UnfiPortOverride> portOverrides = new ArrayList<>();
+
+    public void addPortOverride(final UnfiPortOverride unfiPortOverride) {
+        portOverrides.add(unfiPortOverride);
+    }
+
+    public void addPortOverride(final int portIdx, final String portconfId, final String poeMode) {
+        portOverrides.add(new UnfiPortOverride(portIdx, portconfId, poeMode));
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UniFiPortOverrides: {}", String.join(", ", portOverrides.toArray(new String[0])));
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortTable.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiPortTable.java
new file mode 100644 (file)
index 0000000..637ed2d
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+/**
+ * The {@link UniFiPortTable} represents the data model of UniFi port table, which is an extend of port override.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class UniFiPortTable extends UnfiPortOverride {
+
+    private transient UniFiDevice device;
+
+    private String name;
+
+    private boolean enable;
+
+    private boolean up;
+
+    /**
+     * If true supports PoE.
+     */
+    private boolean portPoe;
+
+    private boolean poeEnable;
+
+    private String poePower;
+
+    private String poeVoltage;
+
+    private String poeCurrent;
+
+    public UniFiDevice getDevice() {
+        return device;
+    }
+
+    public void setDevice(final UniFiDevice device) {
+        this.device = device;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isUp() {
+        return up;
+    }
+
+    public boolean isEnabled() {
+        return enable;
+    }
+
+    public boolean isPortPoe() {
+        return portPoe;
+    }
+
+    public boolean isPoeEnabled() {
+        return poeEnable;
+    }
+
+    public String getPoePower() {
+        return poePower;
+    }
+
+    public String getPoeVoltage() {
+        return poeVoltage;
+    }
+
+    public String getPoeCurrent() {
+        return poeCurrent;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "UniFiPortTable{name: '%s', enable: '%b', up: '%b', portPoe: '%b', poeEnable: '%b, poePower: '%s', poeVoltage: '%s', poeCurrent: '%s'}",
+                name, enable, up, portPoe, poeEnable, poePower, poeVoltage, poeCurrent);
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiSite.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiSite.java
new file mode 100644 (file)
index 0000000..f72ef5e
--- /dev/null
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+
+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 implements HasId {
+
+    private final transient UniFiControllerCache cache;
+
+    public UniFiSite(final UniFiControllerCache cache) {
+        this.cache = cache;
+    }
+
+    @SerializedName("_id")
+    private String id;
+
+    private String name;
+
+    private String desc;
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return desc;
+    }
+
+    public UniFiControllerCache getCache() {
+        return cache;
+    }
+
+    public boolean isSite(final UniFiSite site) {
+        return site != null && id.equals(site.getId());
+    }
+
+    public boolean matchesName(final String siteName) {
+        return siteName.equalsIgnoreCase(desc) || siteName.equalsIgnoreCase(name) || siteName.equalsIgnoreCase(id);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("UniFiSite{id: '%s', name: '%s', desc: '%s'}", id, name, desc);
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiUnknownClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiUnknownClient.java
new file mode 100644 (file)
index 0000000..ab0646e
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+
+/**
+ * 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(final UniFiControllerCache cache) {
+        super(cache);
+    }
+
+    @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
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWiredClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWiredClient.java
new file mode 100644 (file)
index 0000000..62a0d4d
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+
+/**
+ * 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(final UniFiControllerCache cache) {
+        super(cache);
+    }
+
+    @Override
+    public Boolean isWired() {
+        return true;
+    }
+
+    @Override
+    public String getDeviceMac() {
+        return swMac;
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWirelessClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWirelessClient.java
new file mode 100644 (file)
index 0000000..9cad57f
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+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(final UniFiControllerCache cache) {
+        super(cache);
+    }
+
+    @Override
+    public Boolean isWired() {
+        return false;
+    }
+
+    @Override
+    public String getDeviceMac() {
+        return apMac;
+    }
+
+    public String getEssid() {
+        return essid;
+    }
+
+    public Integer getRssi() {
+        return rssi;
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWlan.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/dto/UniFiWlan.java
new file mode 100644 (file)
index 0000000..1eaf52e
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2010-2022 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.dto;
+
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+public class UniFiWlan implements HasId {
+
+    protected final transient UniFiControllerCache cache;
+
+    @SerializedName("_id")
+    private String id;
+
+    private String name;
+
+    private boolean enabled;
+
+    private String security; // ": "wpapsk",
+    private String wlanBand; // ": "both",
+    private String wpaEnc; // ": "ccmp",
+    private String wpaMode;// ": "wpa2",
+    private String xPassphrase; // : "1234",
+    private Boolean hideSsid;
+    private String siteId;
+
+    public UniFiWlan(final UniFiControllerCache cache) {
+        this.cache = cache;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public UniFiSite getSite() {
+        return cache.getSite(siteId);
+    }
+
+    public String getSecurity() {
+        return security;
+    }
+
+    public String getWlanBand() {
+        return wlanBand;
+    }
+
+    public String getWpaEnc() {
+        return wpaEnc;
+    }
+
+    public String getWpaMode() {
+        return wpaMode;
+    }
+
+    public String getXPassphrase() {
+        return xPassphrase;
+    }
+
+    public boolean isHideSsid() {
+        return Boolean.TRUE.equals(hideSsid);
+    }
+
+    @Override
+    public String toString() {
+        final String xPassphraseString = xPassphrase == null ? ""
+                : (xPassphrase.substring(0, Math.min(5, xPassphrase.length())) + "*".repeat(10));
+
+        return String.format(
+                "UniFiWlan{id: '%s', name: '%s', enable: '%b', security: '%s', wlanBand: '%s', wpaEnc: '%s', wpaMode: '%s', xPassphrase: '%s', hideSsid: '%b', site: '%s'}",
+                id, name, enabled, security, wlanBand, wpaEnc, wpaMode, xPassphraseString, hideSsid, getSite());
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiClient.java
deleted file mode 100644 (file)
index 85a8194..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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.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 isWired() == null ? null : (isWired().booleanValue() ? Boolean.FALSE : Boolean.TRUE);
-    }
-
-    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{id: '%s', mac: '%s', ip: '%s', hostname: '%s', alias: '%s', wired: %b, blocked: %b, device: %s}",
-                id, mac, ip, hostname, alias, isWired(), blocked, getDevice());
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiController.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiController.java
deleted file mode 100644 (file)
index 0a1d114..0000000
+++ /dev/null
@@ -1,335 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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 java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-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
- * @author Jacob Laursen - Fix online/blocked channels (broken by UniFi Controller 5.12.35)
- */
-@NonNullByDefault
-public class UniFiController {
-
-    private final Logger logger = LoggerFactory.getLogger(UniFiController.class);
-
-    private Map<String, String> cidToIdCache = new ConcurrentHashMap<String, String>();
-
-    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 boolean unifios;
-
-    private String csrfToken;
-
-    private final Gson gson;
-
-    public UniFiController(HttpClient httpClient, String host, int port, String username, String password,
-            boolean unifios) {
-        this.httpClient = httpClient;
-        this.host = host;
-        this.port = port;
-        this.username = username;
-        this.password = password;
-        this.unifios = unifios;
-        this.csrfToken = "";
-        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 {
-        if (unifios) {
-            obtainCsrfToken();
-        }
-
-        login();
-    }
-
-    public void stop() throws UniFiException {
-        logout();
-    }
-
-    public void obtainCsrfToken() throws UniFiException {
-        csrfToken = "";
-
-        UniFiControllerRequest<Void> req = newRequest(Void.class);
-        req.setPath("/");
-        executeRequest(req);
-    }
-
-    public void login() throws UniFiException {
-        UniFiControllerRequest<Void> req = newRequest(Void.class);
-        req.setPath(unifios ? "/api/auth/login" : "/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 {
-        csrfToken = "";
-        UniFiControllerRequest<Void> req = newRequest(Void.class);
-        req.setPath(unifios ? "/api/auth/logout" : "/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 (id != null && !id.isBlank()) {
-            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 (id != null && !id.isBlank()) {
-            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 cid) {
-        UniFiClient client = null;
-        if (cid != null && !cid.isBlank()) {
-            // Prefer lookups through _id, until initialized use cid.
-            String id = cidToIdCache.get(cid);
-            synchronized (this) {
-                // mgb: first check active clients and fallback to insights if not found
-                client = clientsCache.get(id != null ? id : cid);
-                if (client == null) {
-                    client = insightsCache.get(id != null ? id : cid);
-                }
-            }
-            if (client == null) {
-                logger.debug("Could not find a matching client for cid = {}", cid);
-            } else {
-                cidToIdCache.put(cid, client.id);
-            }
-        }
-        return client;
-    }
-
-    protected void block(UniFiClient client, boolean blocked) throws UniFiException {
-        UniFiControllerRequest<Void> req = newRequest(Void.class);
-        req.setAPIPath("/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.setAPIPath("/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, csrfToken, unifios);
-    }
-
-    private <T> @Nullable T executeRequest(UniFiControllerRequest<T> request) throws UniFiException {
-        T result;
-        try {
-            result = request.execute();
-            csrfToken = request.getCsrfToken();
-        } 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.setAPIPath("/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.setAPIPath("/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.setAPIPath("/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.setAPIPath("/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;
-            }
-        };
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiControllerRequest.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiControllerRequest.java
deleted file mode 100644 (file)
index b5185fe..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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 final boolean unifios;
-
-    private String csrfToken;
-
-    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,
-            String csrfToken, boolean unifios) {
-        this.resultType = resultType;
-        this.gson = gson;
-        this.httpClient = httpClient;
-        this.host = host;
-        this.port = port;
-        this.csrfToken = csrfToken;
-        this.unifios = unifios;
-    }
-
-    public void setAPIPath(String relativePath) {
-        if (unifios) {
-            this.path = "/proxy/network" + relativePath;
-        } else {
-            this.path = relativePath;
-        }
-    }
-
-    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 = JsonParser.parseString(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));
-                }
-
-                String csrfToken = response.getHeaders().get("X-CSRF-Token");
-                if (csrfToken != null && !csrfToken.isEmpty()) {
-                    this.csrfToken = csrfToken;
-                }
-                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;
-    }
-
-    public String getCsrfToken() {
-        return csrfToken;
-    }
-
-    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);
-        }
-
-        if (!csrfToken.isEmpty()) {
-            request.header("x-csrf-token", this.csrfToken);
-        }
-
-        return request;
-    }
-
-    private String getRequestBodyAsJson() {
-        JsonObject jsonObject = new JsonObject();
-        JsonElement jsonElement = null;
-        for (Entry<String, String> entry : bodyParameters.entrySet()) {
-            try {
-                jsonElement = JsonParser.parseString(entry.getValue());
-            } catch (JsonSyntaxException e) {
-                jsonElement = new JsonPrimitive(entry.getValue());
-            }
-            jsonObject.add(entry.getKey(), jsonElement);
-        }
-        return jsonObject.toString();
-    }
-
-    private static String prettyPrintJson(String content) {
-        JsonObject json = JsonParser.parseString(content).getAsJsonObject();
-        Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
-        return prettyGson.toJson(json);
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiDevice.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiDevice.java
deleted file mode 100644 (file)
index a086b2d..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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;
-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 name == null || name.isBlank() ? mac : name;
-    }
-
-    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());
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiSite.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiSite.java
deleted file mode 100644 (file)
index 6a54b14..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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 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 siteName.equalsIgnoreCase(desc) || siteName.equalsIgnoreCase(name) || siteName.equalsIgnoreCase(id);
-    }
-
-    @Override
-    public String toString() {
-        return String.format("UniFiSite{name: '%s', desc: '%s'}", name, desc);
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiUnknownClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiUnknownClient.java
deleted file mode 100644 (file)
index 215d851..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWiredClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWiredClient.java
deleted file mode 100644 (file)
index f913090..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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;
-    }
-}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWirelessClient.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/model/UniFiWirelessClient.java
deleted file mode 100644 (file)
index ecba2e6..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Copyright (c) 2010-2022 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;
-    }
-}
index 874505a107ba8274aa3d1e757495c7933ac11c46..c6362beb530727f7114c76a714885da95500f12c 100644 (file)
@@ -14,10 +14,12 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiUnknownClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
 
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
@@ -33,21 +35,21 @@ import com.google.gson.JsonParseException;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 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);
+    public @Nullable UniFiClient deserialize(final JsonElement json, final Type type,
+            final JsonDeserializationContext context) throws JsonParseException {
+        final JsonObject jsonObject = json.getAsJsonObject();
+        final JsonElement wiredElement = jsonObject.get(PROPERTY_IS_WIRED);
         // mgb: if the "is_wired "property is missing, the client is unknown
-        if (isWiredElement == null) {
+        if (wiredElement == null) {
             return context.deserialize(json, UniFiUnknownClient.class);
         }
-        boolean isWired = isWiredElement.getAsBoolean();
-        if (isWired) {
+        if (wiredElement.getAsBoolean()) {
             return context.deserialize(json, UniFiWiredClient.class);
         }
         return context.deserialize(json, UniFiWirelessClient.class);
index 6e902be39bbd6d5fd4b0c71d3ac7b3cb2b4764d2..94aa924d12cc16033d9d84f88027d6c31ec1f995 100644 (file)
@@ -14,13 +14,16 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiUnknownClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
 
 import com.google.gson.InstanceCreator;
+import com.google.gson.JsonSyntaxException;
 
 /**
  * The {@link UniFiClientInstanceCreator} creates instances of {@link UniFiClient}s during the JSON unmarshalling of
@@ -28,25 +31,27 @@ import com.google.gson.InstanceCreator;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiClientInstanceCreator implements InstanceCreator<UniFiClient> {
 
-    private final UniFiController controller;
+    private final UniFiControllerCache cache;
 
-    public UniFiClientInstanceCreator(UniFiController controller) {
-        this.controller = controller;
+    public UniFiClientInstanceCreator(final UniFiControllerCache cache) {
+        this.cache = cache;
     }
 
     @Override
-    public UniFiClient createInstance(Type type) {
+    public UniFiClient createInstance(final @Nullable Type type) {
         if (UniFiUnknownClient.class.equals(type)) {
-            return new UniFiUnknownClient(controller);
+            return new UniFiUnknownClient(cache);
         }
         if (UniFiWirelessClient.class.equals(type)) {
-            return new UniFiWirelessClient(controller);
+            return new UniFiWirelessClient(cache);
         }
         if (UniFiWiredClient.class.equals(type)) {
-            return new UniFiWiredClient(controller);
+            return new UniFiWiredClient(cache);
+        } else {
+            throw new JsonSyntaxException("Expected a UniFi Client type, but got " + type);
         }
-        return null;
     }
 }
index 39ce9361ae187d66b3a00b897516e37b0b7c8543..0b74a07fa8c9cb8569d30be3d0982f5b11140501 100644 (file)
@@ -14,28 +14,30 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.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
  */
+@NonNullByDefault
 public class UniFiDeviceInstanceCreator implements InstanceCreator<UniFiDevice> {
 
-    private final UniFiController controller;
+    private final UniFiControllerCache cache;
 
-    public UniFiDeviceInstanceCreator(UniFiController controller) {
-        this.controller = controller;
+    public UniFiDeviceInstanceCreator(final UniFiControllerCache cache) {
+        this.cache = cache;
     }
 
     @Override
-    public UniFiDevice createInstance(Type type) {
-        return new UniFiDevice(controller);
+    public UniFiDevice createInstance(final @Nullable Type type) {
+        return new UniFiDevice(cache);
     }
 }
index f3cb07d7ea48c5005cf118ede387de96504dbca5..fc10ac1f31dc2ee10f4e49d84147a6355c32d27c 100644 (file)
@@ -14,10 +14,13 @@ 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 org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
 
 import com.google.gson.InstanceCreator;
+import com.google.gson.JsonSyntaxException;
 
 /**
  *
@@ -26,16 +29,21 @@ import com.google.gson.InstanceCreator;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiSiteInstanceCreator implements InstanceCreator<UniFiSite> {
 
-    private final UniFiController controller;
+    private final UniFiControllerCache cache;
 
-    public UniFiSiteInstanceCreator(UniFiController controller) {
-        this.controller = controller;
+    public UniFiSiteInstanceCreator(final UniFiControllerCache cache) {
+        this.cache = cache;
     }
 
     @Override
-    public UniFiSite createInstance(Type type) {
-        return new UniFiSite(controller);
+    public UniFiSite createInstance(final @Nullable Type type) {
+        if (UniFiSite.class.equals(type)) {
+            return new UniFiSite(cache);
+        } else {
+            throw new JsonSyntaxException("Expected a UniFiSite type, but got " + type);
+        }
     }
 }
index c628f2787c79556f8ecdc0accf248a792aeed8d2..77caffcd08cbbe0c60c71c40e520e4cf36e21256 100644 (file)
@@ -14,6 +14,9 @@ package org.openhab.binding.unifi.internal.api.util;
 
 import java.lang.reflect.Type;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
 import com.google.gson.JsonElement;
@@ -26,12 +29,13 @@ import com.google.gson.JsonParseException;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiTidyLowerCaseStringDeserializer implements JsonDeserializer<String> {
 
     @Override
-    public String deserialize(JsonElement json, Type type, JsonDeserializationContext context)
-            throws JsonParseException {
-        String s = json.getAsJsonPrimitive().getAsString();
+    public @Nullable String deserialize(final JsonElement json, final Type type,
+            final JsonDeserializationContext context) throws JsonParseException {
+        final String s = json.getAsJsonPrimitive().getAsString();
         return s.trim().toLowerCase();
     }
 }
index ae826d0d6d7c147f57a969a448f58d3229edbe89..4e7ce74b91d2daf300ec10ea7261b15360599a0f 100644 (file)
 package org.openhab.binding.unifi.internal.api.util;
 
 import java.lang.reflect.Type;
-import java.util.Calendar;
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 
 import com.google.gson.JsonDeserializationContext;
 import com.google.gson.JsonDeserializer;
@@ -25,14 +28,15 @@ import com.google.gson.JsonElement;
  *
  * @author Matthew Bowman - Initial contribution
  */
-public class UniFiTimestampDeserializer implements JsonDeserializer<Calendar> {
+@NonNullByDefault
+public class UniFiTimestampDeserializer implements JsonDeserializer<Instant> {
 
     @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;
+    public @Nullable Instant deserialize(final JsonElement json, final Type type,
+            final JsonDeserializationContext context) {
+        final String text = json.getAsJsonPrimitive().getAsString();
+        final long millis = Long.valueOf(text) * 1000;
+
+        return Instant.ofEpochMilli(millis);
     }
 }
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiWlanInstanceCreator.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/api/util/UniFiWlanInstanceCreator.java
new file mode 100644 (file)
index 0000000..6d14fa1
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2022 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.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+
+import com.google.gson.InstanceCreator;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link UniFiWlanInstanceCreator} creates instances of {@link UniFiWlan}s during the JSON unmarshalling of
+ * controller responses.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class UniFiWlanInstanceCreator implements InstanceCreator<UniFiWlan> {
+
+    private final UniFiControllerCache cache;
+
+    public UniFiWlanInstanceCreator(final UniFiControllerCache cache) {
+        this.cache = cache;
+    }
+
+    @Override
+    public UniFiWlan createInstance(final @Nullable Type type) {
+        if (UniFiWlan.class.equals(type)) {
+            return new UniFiWlan(cache);
+        } else {
+            throw new JsonSyntaxException("Expected a UniFiWlan type, but got " + type);
+        }
+    }
+}
index b9c1a5664419c518810945ffa73bc729a119f9c3..5b8e41bf28b3e78c50d3f9f465ca69733b2fe6e9 100644 (file)
  */
 package org.openhab.binding.unifi.internal.handler;
 
-import static org.openhab.core.thing.ThingStatus.*;
+import static org.openhab.core.thing.ThingStatus.OFFLINE;
+import static org.openhab.core.thing.ThingStatus.ONLINE;
 import static org.openhab.core.types.RefreshType.REFRESH;
 
 import java.lang.reflect.ParameterizedType;
+import java.util.Optional;
 
-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.UniFiController;
 import org.openhab.binding.unifi.internal.api.UniFiException;
-import org.openhab.binding.unifi.internal.api.model.UniFiController;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
 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.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -43,29 +46,32 @@ public abstract class UniFiBaseThingHandler<E, C> extends BaseThingHandler {
 
     private final Logger logger = LoggerFactory.getLogger(UniFiBaseThingHandler.class);
 
-    public UniFiBaseThingHandler(Thing thing) {
+    public UniFiBaseThingHandler(final Thing thing) {
         super(thing);
     }
 
     @Override
     @SuppressWarnings("unchecked")
     public final void initialize() {
-        Bridge bridge = getBridge();
+        final 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.");
+                    "@text/error.thing.offline.configuration_error");
             return;
         }
         // mgb: derive the config class from the generic type
-        Class<?> clazz = (Class<?>) (((ParameterizedType) getClass().getGenericSuperclass())
+        final Class<?> clazz = (Class<?>) (((ParameterizedType) getClass().getGenericSuperclass())
                 .getActualTypeArguments()[1]);
-        C config = (C) getConfigAs(clazz);
-        initialize(config);
+        final C config = (C) getConfigAs(clazz);
+        if (initialize(config)) {
+            if (bridge.getStatus() == OFFLINE) {
+                updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "@text/error.thing.offline.bridge_offline");
+                return;
+            } else {
+                updateStatus(ONLINE);
+            }
+        }
     }
 
     /**
@@ -75,7 +81,7 @@ public abstract class UniFiBaseThingHandler<E, C> extends BaseThingHandler {
      */
     @SuppressWarnings("null")
     private final @Nullable UniFiController getController() {
-        Bridge bridge = getBridge();
+        final Bridge bridge = getBridge();
         if (bridge != null && bridge.getHandler() != null
                 && (bridge.getHandler() instanceof UniFiControllerThingHandler)) {
             return ((UniFiControllerThingHandler) bridge.getHandler()).getController();
@@ -83,51 +89,109 @@ public abstract class UniFiBaseThingHandler<E, C> extends BaseThingHandler {
         return null;
     }
 
+    private @Nullable E getEntity() {
+        final UniFiController controller = getController();
+        return controller == null ? null : getEntity(controller.getCache());
+    }
+
     @Override
-    public final void handleCommand(ChannelUID channelUID, Command command) {
+    public final void handleCommand(final ChannelUID channelUID, final 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());
+            final E entity = getEntity();
+            final UniFiController controller = getController();
+
+            if (command == REFRESH) {
+                updateState(entity, channelUID);
+            } else {
+                if (entity != null && controller != null) {
+                    try {
+                        if (!handleCommand(controller, entity, channelUID, command)) {
+                            logger.info("Ignoring unsupported command = {} for channel = {}", command, channelUID);
                         }
+                    } catch (final UniFiException e) {
+                        logger.info("Unexpected error handling command = {} for channel = {} : {}", command, channelUID,
+                                e.getMessage());
                     }
+                } else {
+                    logger.info(
+                            "Could not handle command {} for channel = {} because no entity/controller data available.",
+                            command, channelUID);
                 }
             }
+        } else {
+            logger.info("Could not handle command {} for channel = {} because thing not online.", command, channelUID);
         }
     }
 
     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);
-                    }
-                }
-            }
+            final E entity = getEntity();
+
+            getThing().getChannels().forEach(channel -> updateState(entity, channel.getUID()));
+        }
+    }
+
+    private void updateState(final E entity, final ChannelUID channelUID) {
+        final String channelId = channelUID.getId();
+        final State state = Optional.ofNullable(entity).map(e -> getChannelState(e, channelId))
+                .orElseGet(() -> getDefaultState(channelId));
+
+        if (state != UnDefType.NULL) {
+            updateState(channelUID, state);
         }
     }
 
-    protected abstract void initialize(@NonNull C config);
+    /**
+     * Additional sub class specific initialization.
+     * If initialization is unsuccessful it should set the thing status and return false.
+     * if it was successful it should return true
+     *
+     * @param config thing configuration
+     * @return true if initialization was successful
+     */
+    protected abstract boolean initialize(C config);
+
+    /**
+     * Returns the default state if no data available. Default implementation return {@link UnDefType#UNDEF}.
+     *
+     * @param channelId channel to update
+     * @return default state
+     */
+    protected State getDefaultState(final String channelId) {
+        return UnDefType.UNDEF;
+    }
 
-    protected abstract @Nullable E getEntity(UniFiController controller);
+    /**
+     * Returns the cached UniFi entity object related to this thing.
+     *
+     * @param cache cache to get the cached entity from
+     * @return cached entry or null if not exists
+     */
+    protected abstract @Nullable E getEntity(UniFiControllerCache cache);
 
-    protected abstract void refreshChannel(E entity, ChannelUID channelUID);
+    /**
+     * Returns the state to set for the given channel. If {@link UnDefType#NULL} is returned it means the channel should
+     * not be updated.
+     *
+     * @param entity UniFi entity object to get the state information from
+     * @param channelId Channel to update
+     * @return state to set or {@link UnDefType#NULL} if channel state should not be updated.
+     */
+    protected abstract State getChannelState(E entity, String channelId);
 
-    protected abstract void handleCommand(E entity, ChannelUID channelUID, Command command) throws UniFiException;
+    /**
+     * Send the given command to the UniFi controller.
+     *
+     * @param controller controller object to use to send the command to the UniFi controller
+     * @param entity data object of the thing to send command to
+     * @param channelUID channel the command is from
+     * @param command command to send
+     * @return true if command was send
+     * @throws UniFiException
+     */
+    protected abstract boolean handleCommand(UniFiController controller, E entity, ChannelUID channelUID,
+            Command command) throws UniFiException;
 }
index d8fbe8bd20397338918c28ce58db354d6e328484..3d610285cf3db679f65ca9c978e97260df7d5abe 100644 (file)
  */
 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.binding.unifi.internal.UniFiBindingConstants.CHANNEL_AP;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_BLOCKED;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_CMD;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_CMD_RECONNECT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ESSID;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_EXPERIENCE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_GUEST;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_IP_ADDRESS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_LAST_SEEN;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_MAC_ADDRESS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ONLINE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RECONNECT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_RSSI;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_SITE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_UPTIME;
+import static org.openhab.core.thing.ThingStatus.OFFLINE;
 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
 
+import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
-import java.util.Calendar;
 
 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.UniFiController;
 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.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWiredClient;
+import org.openhab.binding.unifi.internal.api.dto.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.QuantityType;
 import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
 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;
@@ -54,35 +69,30 @@ import org.slf4j.LoggerFactory;
 @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) {
+    public UniFiClientThingHandler(final Thing thing) {
         super(thing);
     }
 
     @Override
-    protected synchronized void initialize(UniFiClientThingConfig config) {
+    protected boolean initialize(final UniFiClientThingConfig config) {
         // mgb: called when the config changes
         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;
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/error.thing.client.offline.configuration_error");
+            return false;
         }
         this.config = config;
-        updateStatus(ONLINE);
+        return true;
     }
 
-    private static boolean belongsToSite(UniFiClient client, String siteName) {
+    private static boolean belongsToSite(final UniFiClient client, final String siteName) {
         boolean result = true; // mgb: assume true = proof by contradiction
         if (!siteName.isEmpty()) {
-            UniFiSite site = client.getSite();
+            final 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
@@ -93,8 +103,8 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
     }
 
     @Override
-    protected synchronized @Nullable UniFiClient getEntity(UniFiController controller) {
-        UniFiClient client = controller.getClient(config.getClientID());
+    protected @Nullable UniFiClient getEntity(final UniFiControllerCache cache) {
+        final UniFiClient client = cache.getClient(config.getClientID());
         // mgb: short circuit
         if (client == null || !belongsToSite(client, config.getSite())) {
             return null;
@@ -102,10 +112,10 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
         return client;
     }
 
-    private State getDefaultState(String channelID, boolean clientHome) {
-        State state = UnDefType.NULL;
+    @Override
+    protected State getDefaultState(final String channelID) {
+        final State state;
         switch (channelID) {
-            case CHANNEL_ONLINE:
             case CHANNEL_SITE:
             case CHANNEL_AP:
             case CHANNEL_ESSID:
@@ -113,11 +123,15 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
             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
+                state = UnDefType.UNDEF;
                 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
+                state = DecimalType.ZERO;
+                break;
+            case CHANNEL_EXPERIENCE:
+                // mgb: uptime + experience should default to 0
+                state = new QuantityType<>(0, Units.PERCENT);
                 break;
             case CHANNEL_LAST_SEEN:
                 // mgb: lastSeen should keep the last state no matter what
@@ -126,34 +140,35 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
             case CHANNEL_RECONNECT:
                 state = OnOffType.OFF;
                 break;
+            default:
+                state = UnDefType.NULL;
+                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);
-            }
+    private synchronized boolean isClientHome(final UniFiClient client) {
+        final boolean online;
+
+        final Instant lastSeen = client.getLastSeen();
+        if (lastSeen == null) {
+            online = false;
+            logger.warn("Could not determine if client is online: cid = {}, lastSeen = null", config.getClientID());
+        } else {
+            final Instant considerHomeExpiry = lastSeen.plusSeconds(config.getConsiderHome());
+            online = Instant.now().isBefore(considerHomeExpiry);
         }
         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) {
+    protected State getChannelState(final UniFiClient client, final String channelId) {
+        final boolean clientHome = isClientHome(client);
+        final UniFiDevice device = client.getDevice();
+        final UniFiSite site = (device == null ? null : device.getSite());
+        State state = getDefaultState(channelId);
+
+        switch (channelId) {
             // mgb: common wired + wireless client channels
 
             // :online
@@ -163,28 +178,28 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
 
             // :site
             case CHANNEL_SITE:
-                if (clientHome && site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
+                if (site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
                     state = StringType.valueOf(site.getDescription());
                 }
                 break;
 
             // :macAddress
             case CHANNEL_MAC_ADDRESS:
-                if (clientHome && client.getMac() != null && !client.getMac().isBlank()) {
+                if (client.getMac() != null && !client.getMac().isBlank()) {
                     state = StringType.valueOf(client.getMac());
                 }
                 break;
 
             // :ipAddress
             case CHANNEL_IP_ADDRESS:
-                if (clientHome && client.getIp() != null && !client.getIp().isBlank()) {
+                if (client.getIp() != null && !client.getIp().isBlank()) {
                     state = StringType.valueOf(client.getIp());
                 }
                 break;
 
             // :uptime
             case CHANNEL_UPTIME:
-                if (clientHome && client.getUptime() != null) {
+                if (client.getUptime() != null) {
                     state = new DecimalType(client.getUptime());
                 }
                 break;
@@ -193,8 +208,7 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
             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()));
+                    state = new DateTimeType(ZonedDateTime.ofInstant(client.getLastSeen(), ZoneId.systemDefault()));
                 }
                 break;
 
@@ -203,97 +217,119 @@ public class UniFiClientThingHandler extends UniFiBaseThingHandler<UniFiClient,
                 state = OnOffType.from(client.isBlocked());
                 break;
 
+            // :guest
+            case CHANNEL_GUEST:
+                state = OnOffType.from(client.isGuest());
+                break;
+
+            // :experience
+            case CHANNEL_EXPERIENCE:
+                if (client.getExperience() != null) {
+                    state = new QuantityType<>(client.getExperience(), Units.PERCENT);
+                }
+                break;
+
             default:
                 // mgb: additional wired client channels
                 if (client.isWired() && (client instanceof UniFiWiredClient)) {
-                    state = getWiredChannelState((UniFiWiredClient) client, clientHome, channelID);
+                    state = getWiredChannelState((UniFiWiredClient) client, channelId, state);
                 }
 
                 // mgb: additional wireless client channels
                 else if (client.isWireless() && (client instanceof UniFiWirelessClient)) {
-                    state = getWirelessChannelState((UniFiWirelessClient) client, clientHome, channelID);
+                    state = getWirelessChannelState((UniFiWirelessClient) client, channelId, state);
                 }
                 break;
         }
-        // mgb: only non null states get updates
-        if (state != UnDefType.NULL) {
-            updateState(channelID, state);
-        }
+        return state;
     }
 
-    private State getWiredChannelState(UniFiWiredClient client, boolean clientHome, String channelID) {
-        State state = UnDefType.NULL;
-        return state;
+    private State getWiredChannelState(final UniFiWiredClient client, final String channelId,
+            final State defaultState) {
+        return defaultState;
     }
 
-    private State getWirelessChannelState(UniFiWirelessClient client, boolean clientHome, String channelID) {
-        State state = UnDefType.NULL;
-        switch (channelID) {
+    private State getWirelessChannelState(final UniFiWirelessClient client, final String channelId,
+            final State defaultState) {
+        State state = defaultState;
+        switch (channelId) {
             // :ap
             case CHANNEL_AP:
-                UniFiDevice device = client.getDevice();
-                if (clientHome && device != null && device.getName() != null && !device.getName().isBlank()) {
+                final UniFiDevice device = client.getDevice();
+                if (device != null && device.getName() != null && !device.getName().isBlank()) {
                     state = StringType.valueOf(device.getName());
                 }
                 break;
 
             // :essid
             case CHANNEL_ESSID:
-                if (clientHome && client.getEssid() != null && !client.getEssid().isBlank()) {
+                if (client.getEssid() != null && !client.getEssid().isBlank()) {
                     state = StringType.valueOf(client.getEssid());
                 }
                 break;
 
             // :rssi
             case CHANNEL_RSSI:
-                if (clientHome && client.getRssi() != null) {
+                if (client.getRssi() != null) {
                     state = new DecimalType(client.getRssi());
                 }
                 break;
 
             // :reconnect
             case CHANNEL_RECONNECT:
-                // nop - read-only channel
+                // nop - trigger channel so it's always OFF by default
+                state = OnOffType.OFF;
                 break;
         }
         return state;
     }
 
     @Override
-    protected void handleCommand(UniFiClient client, ChannelUID channelUID, Command command) throws UniFiException {
-        String channelID = channelUID.getIdWithoutGroup();
+    protected boolean handleCommand(final UniFiController controller, final UniFiClient client,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        final String channelID = channelUID.getIdWithoutGroup();
         switch (channelID) {
             case CHANNEL_BLOCKED:
-                handleBlockedCommand(client, channelUID, command);
-                break;
+                return handleBlockedCommand(controller, client, channelUID, command);
+            case CHANNEL_CMD:
+                return handleReconnectCommand(controller, client, channelUID, command);
             case CHANNEL_RECONNECT:
-                handleReconnectCommand(client, channelUID, command);
-                break;
+                return handleReconnectSwitch(controller, client, channelUID, command);
             default:
-                logger.warn("Ignoring unsupported command = {} for channel = {}", command, channelUID);
+                return false;
         }
     }
 
-    private void handleBlockedCommand(UniFiClient client, ChannelUID channelUID, Command command)
-            throws UniFiException {
+    private boolean handleBlockedCommand(final UniFiController controller, final UniFiClient client,
+            final ChannelUID channelUID, final 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);
+            controller.block(client, command == OnOffType.ON);
+            refresh();
+            return true;
         }
+        return false;
     }
 
-    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);
-            }
+    private boolean handleReconnectCommand(final UniFiController controller, final UniFiClient client,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        if (command instanceof StringType && CHANNEL_CMD_RECONNECT.equalsIgnoreCase(command.toFullString())) {
+            controller.reconnect(client);
+            return true;
         } else {
-            logger.warn("Ignoring unsupported command = {} for channel = {} - valid commands types are: OnOffType",
-                    command, channelUID);
+            logger.info("Unknown command '{}' given to wireless client thing '{}': client {}", command,
+                    getThing().getUID(), client);
+            return false;
+        }
+    }
+
+    private boolean handleReconnectSwitch(final UniFiController controller, final UniFiClient client,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        if (command instanceof OnOffType && command == OnOffType.ON) {
+            controller.reconnect(client);
+            updateState(channelUID, OnOffType.OFF);
+            refresh();
+            return true;
         }
+        return false;
     }
 }
index 562704a53be98f42ba7550c2eb79cb3c3358081b..6c799375f5cf84d96e4e0b370823e3e553c6e9ed 100644 (file)
@@ -14,29 +14,32 @@ 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 static org.openhab.core.thing.ThingStatus.UNKNOWN;
+import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
+import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
 
+import java.util.Collection;
+import java.util.List;
 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.UniFiController;
 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.ThingHandlerService;
 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
 import org.openhab.core.types.Command;
 import org.slf4j.Logger;
@@ -51,17 +54,10 @@ import org.slf4j.LoggerFactory;
 @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 static final String STATUS_DESCRIPTION_COMMUNICATION_ERROR = "@text/error.bridge.offline.communication_error";
+    private static final String STATUS_DESCRIPTION_SSL_ERROR = "@text/error.bridge.offline.ssl_error";
+    private static final String STATUS_DESCRIPTION_INVALID_CREDENTIALS = "@text/error.bridge.offline.invalid_credentials";
+    private static final String STATUS_DESCRIPTION_INVALID_HOSTNAME = "@text/error.bridge.offline.invalid_hostname";
 
     private final Logger logger = LoggerFactory.getLogger(UniFiControllerThingHandler.class);
 
@@ -73,48 +69,36 @@ public class UniFiControllerThingHandler extends BaseBridgeHandler {
 
     private final HttpClient httpClient;
 
-    public UniFiControllerThingHandler(Bridge bridge, HttpClient httpClient) {
+    public UniFiControllerThingHandler(final Bridge bridge, final HttpClient httpClient) {
         super(bridge);
         this.httpClient = httpClient;
     }
 
     // Public API
 
+    @Override
+    public Collection<Class<? extends ThingHandlerService>> getServices() {
+        return List.of(UniFiThingDiscoveryService.class);
+    }
+
     @Override
     public void initialize() {
-        // mgb: called when the config changes
-        cancelRefreshJob();
-        config = getConfig().as(UniFiControllerThingConfig.class);
+        config = getConfigAs(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(), config.isUniFiOS());
-            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());
-        }
+        final UniFiController uc = new UniFiController(httpClient, config.getHost(), config.getPort(),
+                config.getUsername(), config.getPassword(), config.isUniFiOS());
+
+        controller = uc;
+        updateStatus(UNKNOWN);
+        scheduler.schedule(() -> start(uc), 10, TimeUnit.MILLISECONDS);
     }
 
     @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();
-        }
+    protected void updateStatus(final ThingStatus status, final ThingStatusDetail statusDetail,
+            @Nullable final String description) {
         // mgb: update the status only if it's changed
-        ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
-                .build();
+        final ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail)
+                .withDescription(description).build();
         if (!statusInfo.equals(getThing().getStatusInfo())) {
             super.updateStatus(status, statusDetail, description);
         }
@@ -126,7 +110,7 @@ public class UniFiControllerThingHandler extends BaseBridgeHandler {
         if (controller != null) {
             try {
                 controller.stop();
-            } catch (UniFiException e) {
+            } catch (final UniFiException e) {
                 // mgb: nop as we're in dispose
             }
             controller = null;
@@ -134,7 +118,7 @@ public class UniFiControllerThingHandler extends BaseBridgeHandler {
     }
 
     @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
+    public void handleCommand(final ChannelUID channelUID, final Command command) {
         // nop - read-only binding
         logger.warn("Ignoring command = {} for channel = {} - the UniFi binding is read-only!", command, channelUID);
     }
@@ -143,26 +127,39 @@ public class UniFiControllerThingHandler extends BaseBridgeHandler {
         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 start(final UniFiController uc) {
+        boolean startRefresh = false;
+        try {
+            uc.start();
+            startRefresh = true;
+        } catch (final UniFiCommunicationException e) {
+            updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
+            startRefresh = true;
+        } catch (final UniFiInvalidHostException e) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_HOSTNAME);
+        } catch (final UniFiSSLException e) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_SSL_ERROR);
+        } catch (final UniFiInvalidCredentialsException e) {
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, STATUS_DESCRIPTION_INVALID_CREDENTIALS);
+        } catch (final UniFiException e) {
+            logger.debug("Unknown error while configuring the UniFi Controller", e);
+            updateStatus(OFFLINE, CONFIGURATION_ERROR, e.getMessage());
+        }
+        if (startRefresh) {
+            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) {
+            final ScheduledFuture<?> rj = refreshJob;
+
+            if (rj != null) {
                 logger.debug("Cancelling refresh job");
-                refreshJob.cancel(true);
+                rj.cancel(true);
                 refreshJob = null;
             }
         }
@@ -173,21 +170,22 @@ public class UniFiControllerThingHandler extends BaseBridgeHandler {
             logger.trace("Executing refresh job");
             refresh();
             updateStatus(ONLINE);
-        } catch (UniFiCommunicationException e) {
+        } catch (final UniFiCommunicationException e) {
             updateStatus(OFFLINE, COMMUNICATION_ERROR, STATUS_DESCRIPTION_COMMUNICATION_ERROR);
-        } catch (UniFiInvalidCredentialsException e) {
+        } catch (final 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());
+        } catch (final RuntimeException | UniFiException e) {
+            logger.debug("Unhandled exception while refreshing the UniFi Controller {}", getThing().getUID(), e);
             updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getMessage());
         }
     }
 
     private void refresh() throws UniFiException {
-        if (controller != null) {
+        final UniFiController uc = controller;
+
+        if (uc != null) {
             logger.debug("Refreshing the UniFi Controller {}", getThing().getUID());
-            controller.refresh();
+            uc.refresh();
             // mgb: then refresh all the client things
             getThing().getThings().forEach((thing) -> {
                 if (thing.getHandler() instanceof UniFiBaseThingHandler) {
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiPoePortThingHandler.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiPoePortThingHandler.java
new file mode 100644 (file)
index 0000000..236f1c0
--- /dev/null
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2010-2022 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.CHANNEL_ENABLE_PARAMETER_MODE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ENABLE_PARAMETER_MODE_AUTO;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ENABLE_PARAMETER_MODE_OFF;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ONLINE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_CMD;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_CMD_POWER_CYCLE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_CURRENT;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_ENABLE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_MODE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_POWER;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PORT_POE_VOLTAGE;
+import static org.openhab.core.library.unit.MetricPrefix.MILLI;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.measure.quantity.ElectricCurrent;
+import javax.measure.quantity.ElectricPotential;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.UniFiPoePortThingConfig;
+import org.openhab.binding.unifi.internal.api.UniFiController;
+import org.openhab.binding.unifi.internal.api.UniFiException;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UnfiPortOverride;
+import org.openhab.binding.unifi.internal.api.dto.UniFiDevice;
+import org.openhab.binding.unifi.internal.api.dto.UniFiPortTable;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Power Over Ethernet (PoE) port on a UniFi switch.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class UniFiPoePortThingHandler
+        extends UniFiBaseThingHandler<Map<Integer, UniFiPortTable>, UniFiPoePortThingConfig> {
+
+    private final Logger logger = LoggerFactory.getLogger(UniFiPoePortThingHandler.class);
+
+    private UniFiPoePortThingConfig config = new UniFiPoePortThingConfig();
+    private String poeEnableMode = "";
+
+    public UniFiPoePortThingHandler(final Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected boolean initialize(final UniFiPoePortThingConfig config) {
+        this.config = config;
+        if (!config.isValid()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/error.thing.poe.offline.configuration_error");
+            return false;
+        }
+        final String channelConfigPoeEnableMode = (String) getThing().getChannel(CHANNEL_PORT_POE_ENABLE)
+                .getConfiguration().get(CHANNEL_ENABLE_PARAMETER_MODE);
+        poeEnableMode = channelConfigPoeEnableMode.isBlank() ? CHANNEL_ENABLE_PARAMETER_MODE_AUTO
+                : channelConfigPoeEnableMode;
+        return true;
+    }
+
+    @Override
+    protected @Nullable Map<Integer, UniFiPortTable> getEntity(final UniFiControllerCache cache) {
+        return cache.getSwitchPorts(config.getMacAddress());
+    }
+
+    @Override
+    protected State getChannelState(final Map<Integer, UniFiPortTable> ports, final String channelId) {
+        final UniFiPortTable port = getPort(ports);
+
+        if (port == null) {
+            logger.debug("No PoE port for thing '{}' could be found in the data. Refresh ignored.",
+                    getThing().getUID());
+            return UnDefType.NULL;
+        }
+        final State state;
+
+        switch (channelId) {
+            case CHANNEL_ONLINE:
+                state = OnOffType.from(port.isUp());
+                break;
+            case CHANNEL_PORT_POE_ENABLE:
+                state = OnOffType.from(port.isPoeEnabled());
+                break;
+            case CHANNEL_PORT_POE_MODE:
+                state = StringType.valueOf(port.getPoeMode());
+                break;
+            case CHANNEL_PORT_POE_POWER:
+                state = new QuantityType<Power>(Double.valueOf(port.getPoePower()), Units.WATT);
+                break;
+            case CHANNEL_PORT_POE_VOLTAGE:
+                state = new QuantityType<ElectricPotential>(Double.valueOf(port.getPoeVoltage()), Units.VOLT);
+                break;
+            case CHANNEL_PORT_POE_CURRENT:
+                state = new QuantityType<ElectricCurrent>(Double.valueOf(port.getPoeCurrent()), MILLI(Units.AMPERE));
+                break;
+            default:
+                state = UnDefType.UNDEF;
+        }
+        return state;
+    }
+
+    private @Nullable UniFiPortTable getPort(final Map<Integer, UniFiPortTable> ports) {
+        return ports.get(config.getPortNumber());
+    }
+
+    @Override
+    protected boolean handleCommand(final UniFiController controller, final Map<Integer, UniFiPortTable> ports,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        final String channelID = channelUID.getIdWithoutGroup();
+
+        switch (channelID) {
+            case CHANNEL_PORT_POE_ENABLE:
+                if (command instanceof OnOffType) {
+                    return handleModeCommand(controller, ports, getPort(ports),
+                            OnOffType.ON == command ? poeEnableMode : CHANNEL_ENABLE_PARAMETER_MODE_OFF);
+                }
+                break;
+            case CHANNEL_PORT_POE_MODE:
+                if (command instanceof StringType) {
+                    return handleModeCommand(controller, ports, getPort(ports), command.toFullString());
+                }
+                break;
+            case CHANNEL_PORT_POE_CMD:
+                if (command instanceof StringType) {
+                    return handleCmd(controller, getPort(ports), command.toFullString());
+                }
+            default:
+                return false;
+        }
+        return false;
+    }
+
+    private boolean handleModeCommand(final UniFiController controller, final Map<Integer, UniFiPortTable> ports,
+            final @Nullable UniFiPortTable portToUpdate, final String poeMode) throws UniFiException {
+        final UniFiDevice device = controller.getCache().getDevice(config.getMacAddress());
+
+        if (device == null || portToUpdate == null) {
+            logger.info("Could not change the PoE port state for thing '{}': device {} or portToUpdate {} null",
+                    getThing().getUID(), device, portToUpdate);
+            return false;
+        } else {
+            final UnfiPortOverride override = new UnfiPortOverride();
+            override.setPortIdx(portToUpdate.getPortIdx());
+            override.setPortconfId(portToUpdate.getPortconfId());
+            override.setPoeMode(poeMode);
+            final Map<Integer, UnfiPortOverride> newMap = new HashMap<>(ports);
+
+            newMap.put(portToUpdate.getPortIdx(), override);
+            controller.poeMode(device, newMap);
+            refresh();
+            return true;
+        }
+    }
+
+    private boolean handleCmd(final UniFiController controller, @Nullable final UniFiPortTable portToUpdate,
+            final String command) throws UniFiException {
+        final UniFiDevice device = controller.getCache().getDevice(config.getMacAddress());
+        if (device == null || portToUpdate == null) {
+            logger.info("Could not change the PoE port state for thing '{}': device {} or portToUpdate {} null",
+                    getThing().getUID(), device, portToUpdate);
+            return false;
+        } else {
+            if (CHANNEL_PORT_POE_CMD_POWER_CYCLE.equalsIgnoreCase(command.replaceAll("[- ]", ""))) {
+                controller.poePowerCycle(device, portToUpdate.getPortIdx());
+                return true;
+            } else {
+                logger.info("Unknown command '{}' given to PoE port for thing '{}': device {} or portToUpdate {} null",
+                        command, getThing().getUID(), device, portToUpdate);
+                return false;
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiSiteThingHandler.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiSiteThingHandler.java
new file mode 100644 (file)
index 0000000..e996efa
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2022 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.CHANNEL_GUEST_CLIENTS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_TOTAL_CLIENTS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WIRED_CLIENTS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WIRELESS_CLIENTS;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.UniFiSiteThingConfig;
+import org.openhab.binding.unifi.internal.api.UniFiController;
+import org.openhab.binding.unifi.internal.api.UniFiException;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link UniFiSiteThingHandler} is responsible for handling commands and status
+ * updates for {@link UniFiSite} instances.
+ *
+ * @author Matthew Bowman - Initial contribution
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class UniFiSiteThingHandler extends UniFiBaseThingHandler<UniFiSite, UniFiSiteThingConfig> {
+
+    private UniFiSiteThingConfig config = new UniFiSiteThingConfig();
+
+    public UniFiSiteThingHandler(final Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected boolean initialize(final UniFiSiteThingConfig config) {
+        this.config = config;
+        if (!config.isValid()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/error.thing.site.offline.configuration_error");
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    protected @Nullable UniFiSite getEntity(final UniFiControllerCache cache) {
+        return cache.getSite(config.getSiteID());
+    }
+
+    @Override
+    protected State getChannelState(final UniFiSite site, final String channelId) {
+        final UniFiControllerCache cache = site.getCache();
+        final long count;
+
+        switch (channelId) {
+            case CHANNEL_TOTAL_CLIENTS:
+                count = cache.countClients(site, c -> true);
+                break;
+            case CHANNEL_WIRELESS_CLIENTS:
+                count = cache.countClients(site, c -> c.isWireless());
+                break;
+            case CHANNEL_WIRED_CLIENTS:
+                count = cache.countClients(site, c -> c.isWired());
+                break;
+            case CHANNEL_GUEST_CLIENTS:
+                count = cache.countClients(site, c -> c.isGuest());
+                break;
+            default:
+                // Unsupported channel; nothing to update
+                return UnDefType.NULL;
+        }
+        return new DecimalType(count);
+    }
+
+    @Override
+    protected boolean handleCommand(final UniFiController controller, final UniFiSite entity,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        return false;
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiThingDiscoveryService.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiThingDiscoveryService.java
new file mode 100644 (file)
index 0000000..f204154
--- /dev/null
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2010-2022 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.PARAMETER_CID;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_MAC_ADDRESS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_PORT_NUMBER;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_SID;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_SITE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_WID;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.PARAMETER_WIFI_NAME;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+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.api.UniFiController;
+import org.openhab.binding.unifi.internal.api.UniFiException;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiPortTable;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+import org.openhab.core.config.discovery.AbstractDiscoveryService;
+import org.openhab.core.config.discovery.DiscoveryResultBuilder;
+import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Discovery service for detecting things connected to a UniFi controller.
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class UniFiThingDiscoveryService extends AbstractDiscoveryService
+        implements ThingHandlerService, DiscoveryService {
+
+    /**
+     * Timeout for discovery time.
+     */
+    private static final int UNIFI_DISCOVERY_TIMEOUT_SECONDS = 30;
+    private static final long TTL_SECONDS = TimeUnit.MINUTES.toSeconds(5);
+    private static final int THING_ID_LENGTH = 8;
+    private static final String DEFAULT_PORTNAME = "Port";
+
+    private final Logger logger = LoggerFactory.getLogger(UniFiThingDiscoveryService.class);
+
+    private @Nullable UniFiControllerThingHandler bridgeHandler;
+
+    public UniFiThingDiscoveryService() {
+        super(UniFiBindingConstants.THING_TYPE_SUPPORTED, UNIFI_DISCOVERY_TIMEOUT_SECONDS, false);
+    }
+
+    @Override
+    public void deactivate() {
+        super.deactivate();
+    }
+
+    @Override
+    public void setThingHandler(final ThingHandler handler) {
+        if (handler instanceof UniFiControllerThingHandler) {
+            bridgeHandler = (UniFiControllerThingHandler) handler;
+        }
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return bridgeHandler;
+    }
+
+    @Override
+    protected void startScan() {
+        removeOlderResults(getTimestampOfLastScan());
+        final UniFiControllerThingHandler bh = bridgeHandler;
+        if (bh == null) {
+            return;
+        }
+        final UniFiController controller = bh.getController();
+        if (controller == null) {
+            return;
+        }
+        try {
+            controller.refresh();
+            final UniFiControllerCache cache = controller.getCache();
+            final ThingUID bridgeUID = bh.getThing().getUID();
+
+            discoverSites(cache, bridgeUID);
+            discoverWlans(cache, bridgeUID);
+            discoverClients(cache, bridgeUID);
+            discoverPoePorts(cache, bridgeUID);
+        } catch (final UniFiException e) {
+            logger.debug("Exception during discovery of UniFi Things", e);
+        }
+    }
+
+    private void discoverSites(final UniFiControllerCache cache, final ThingUID bridgeUID) {
+        for (final UniFiSite site : cache.getSites()) {
+            final ThingUID thingUID = new ThingUID(UniFiBindingConstants.THING_TYPE_SITE, bridgeUID,
+                    stripIdShort(site.getId()));
+            final Map<String, Object> properties = Map.of(PARAMETER_SID, site.getId());
+
+            thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(UniFiBindingConstants.THING_TYPE_SITE)
+                    .withBridge(bridgeUID).withRepresentationProperty(PARAMETER_SID).withTTL(TTL_SECONDS)
+                    .withProperties(properties).withLabel(site.getName()).build());
+        }
+    }
+
+    private void discoverWlans(final UniFiControllerCache cache, final ThingUID bridgeUID) {
+        for (final UniFiWlan wlan : cache.getWlans()) {
+            final ThingUID thingUID = new ThingUID(UniFiBindingConstants.THING_TYPE_WLAN, bridgeUID,
+                    stripIdShort(wlan.getId()));
+            final Map<String, Object> properties = Map.of(PARAMETER_WID, wlan.getId(), PARAMETER_SITE,
+                    wlan.getSite().getName(), PARAMETER_WIFI_NAME, wlan.getName());
+
+            thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(UniFiBindingConstants.THING_TYPE_WLAN)
+                    .withBridge(bridgeUID).withRepresentationProperty(PARAMETER_WID).withTTL(TTL_SECONDS)
+                    .withProperties(properties).withLabel(wlan.getName()).build());
+        }
+    }
+
+    private void discoverClients(final UniFiControllerCache cache, final ThingUID bridgeUID) {
+        for (final UniFiClient uc : cache.getClients()) {
+            final var thingTypeUID = uc.isWireless() ? UniFiBindingConstants.THING_TYPE_WIRELESS_CLIENT
+                    : UniFiBindingConstants.THING_TYPE_WIRED_CLIENT;
+            final ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, stripIdShort(uc.getId()));
+            final Map<String, Object> properties = Map.of(PARAMETER_CID, uc.getMac(), PARAMETER_SITE,
+                    uc.getSite().getName());
+
+            thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID).withBridge(bridgeUID)
+                    .withRepresentationProperty(PARAMETER_CID).withTTL(TTL_SECONDS).withProperties(properties)
+                    .withLabel(uc.getAlias()).build());
+        }
+    }
+
+    /**
+     * Shorten the id to make it a bit more comprehensible.
+     *
+     * @param id id to shorten.
+     * @return shortened id or if to short the original id
+     */
+    private static String stripIdShort(final String id) {
+        return id.length() > THING_ID_LENGTH ? id.substring(id.length() - THING_ID_LENGTH) : id;
+    }
+
+    private void discoverPoePorts(final UniFiControllerCache cache, final ThingUID bridgeUID) {
+        for (final Map<Integer, UniFiPortTable> uc : cache.getSwitchPorts()) {
+            for (final Entry<Integer, UniFiPortTable> sp : uc.entrySet()) {
+                final UniFiPortTable pt = sp.getValue();
+                final String deviceMac = pt.getDevice().getMac();
+                final String id = deviceMac.replace(":", "") + "_" + pt.getPortIdx();
+                final ThingUID thingUID = new ThingUID(UniFiBindingConstants.THING_TYPE_POE_PORT, bridgeUID, id);
+                final Map<String, Object> properties = Map.of(PARAMETER_PORT_NUMBER, pt.getPortIdx(),
+                        PARAMETER_MAC_ADDRESS, deviceMac);
+
+                thingDiscovered(DiscoveryResultBuilder.create(thingUID)
+                        .withThingType(UniFiBindingConstants.THING_TYPE_POE_PORT).withBridge(bridgeUID)
+                        .withTTL(TTL_SECONDS).withProperties(properties).withLabel(portName(pt)).build());
+            }
+        }
+    }
+
+    /**
+     * If the PoE port hasn't it's own name, but is named Port with a number the name is prefixed with the device name.
+     *
+     * @param pt port object
+     * @return label for the discovered PoE port
+     */
+    private static @Nullable String portName(final UniFiPortTable pt) {
+        final String portName = pt.getName();
+
+        return portName.startsWith(DEFAULT_PORTNAME) ? pt.getDevice().getName() + " " + portName : portName;
+    }
+}
diff --git a/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiWlanThingHandler.java b/bundles/org.openhab.binding.unifi/src/main/java/org/openhab/binding/unifi/internal/handler/UniFiWlanThingHandler.java
new file mode 100644 (file)
index 0000000..009387b
--- /dev/null
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2010-2022 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.CHANNEL_ENABLE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_ESSID;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_GUEST_CLIENTS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_PASSPHRASE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_QRCODE_ENCODING;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_SECURITY;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_SITE;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WIRELESS_CLIENTS;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WLANBAND;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WPAENC;
+import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_WPAMODE;
+
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.unifi.internal.UniFiWlanThingConfig;
+import org.openhab.binding.unifi.internal.api.UniFiController;
+import org.openhab.binding.unifi.internal.api.UniFiException;
+import org.openhab.binding.unifi.internal.api.cache.UniFiControllerCache;
+import org.openhab.binding.unifi.internal.api.dto.UniFiClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWirelessClient;
+import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
+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.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ *
+ * @author Hilbrand Bouwkamp - Initial contribution
+ */
+@NonNullByDefault
+public class UniFiWlanThingHandler extends UniFiBaseThingHandler<UniFiWlan, UniFiWlanThingConfig> {
+
+    private UniFiWlanThingConfig config = new UniFiWlanThingConfig();
+
+    public UniFiWlanThingHandler(final Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected boolean initialize(final UniFiWlanThingConfig config) {
+        this.config = config;
+
+        if (!config.isValid()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "@text/error.thing.wlan.offline.configuration_error");
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    protected @Nullable UniFiWlan getEntity(final UniFiControllerCache cache) {
+        return cache.getWlan(config.getWlanId());
+    }
+
+    @Override
+    protected State getChannelState(final UniFiWlan wlan, final String channelId) {
+        final State state;
+
+        switch (channelId) {
+            case CHANNEL_ENABLE:
+                state = OnOffType.from(wlan.isEnabled());
+                break;
+            case CHANNEL_ESSID:
+                state = StringType.valueOf(wlan.getName());
+                break;
+            case CHANNEL_SITE:
+                final UniFiSite site = wlan.getSite();
+                if (site != null && site.getDescription() != null && !site.getDescription().isBlank()) {
+                    state = StringType.valueOf(site.getDescription());
+                } else {
+                    state = UnDefType.UNDEF;
+                }
+                break;
+            case CHANNEL_WIRELESS_CLIENTS:
+                state = countClients(wlan, c -> true);
+                break;
+            case CHANNEL_GUEST_CLIENTS:
+                state = countClients(wlan, c -> c.isGuest());
+                break;
+            case CHANNEL_SECURITY:
+                state = StringType.valueOf(wlan.getSecurity());
+                break;
+            case CHANNEL_WLANBAND:
+                state = StringType.valueOf(wlan.getWlanBand());
+                break;
+            case CHANNEL_WPAENC:
+                state = StringType.valueOf(wlan.getWpaEnc());
+                break;
+            case CHANNEL_WPAMODE:
+                state = StringType.valueOf(wlan.getWpaMode());
+                break;
+            case CHANNEL_PASSPHRASE:
+                state = StringType.valueOf(wlan.getXPassphrase());
+                break;
+            case CHANNEL_QRCODE_ENCODING:
+                state = qrcodeEncoding(wlan);
+                break;
+            default:
+                // Unsupported channel; nothing to update
+                state = UnDefType.NULL;
+        }
+        return state;
+    }
+
+    private static State countClients(final UniFiWlan wlan, final Function<UniFiClient, Boolean> filter) {
+        final UniFiSite site = wlan.getSite();
+        return new DecimalType(site.getCache().countClients(site, c -> c instanceof UniFiWirelessClient
+                && wlan.getName().equals(((UniFiWirelessClient) c).getEssid()) && filter.apply(c)));
+    }
+
+    /**
+     * Returns a MERCARD like notation of the Wi-Fi access code. Format:
+     * <code>WIFI:S:&lt;SSID>;T:WPA|blank;P:&lt;password>;;</code>
+     *
+     * @param wlan wlan UniFi entity object containing the data
+     * @return MERCARD like Wi-Fi access format
+     * @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11
+     */
+    private static State qrcodeEncoding(final UniFiWlan wlan) {
+        final String name = encode(wlan.getName());
+        final String xPassphrase = wlan.getXPassphrase();
+        final boolean nopass = xPassphrase == null || xPassphrase.isBlank();
+        final String mode = nopass ? "nopass" : "WPA";
+        final String hidden = wlan.isHideSsid() ? "H:true" : "";
+        final String passcode = nopass ? "" : "P:" + encode(xPassphrase);
+
+        return StringType.valueOf(String.format("WIFI:S:%s;T:%s;%s;%s;", name, mode, passcode, hidden));
+    }
+
+    private static String encode(final @Nullable String value) {
+        return value == null ? "" : value.replaceAll("([\\;,\":])", "\\\\$1");
+    }
+
+    @Override
+    protected boolean handleCommand(final UniFiController controller, final UniFiWlan entity,
+            final ChannelUID channelUID, final Command command) throws UniFiException {
+        final String channelID = channelUID.getId();
+
+        if (CHANNEL_ENABLE.equals(channelID) && command instanceof OnOffType) {
+            controller.enableWifi(entity, OnOffType.ON == command);
+            return true;
+        }
+        return false;
+    }
+}
index 96148a6f74e6b6357ce9ce5b27b91317471e9fbc..b6b258431f12e4fa6e5f429059656a63746b66c8 100644 (file)
@@ -19,6 +19,9 @@ import java.security.cert.X509Certificate;
 import javax.net.ssl.SSLEngine;
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  *
  * The {@link UniFiTrustManager} is a "trust all" implementation of {@link X509ExtendedTrustManager}.
@@ -27,6 +30,7 @@ import javax.net.ssl.X509ExtendedTrustManager;
  *
  * @author Matthew Bowman - Initial contribution
  */
+@NonNullByDefault
 public class UniFiTrustManager extends X509ExtendedTrustManager {
 
     private static UniFiTrustManager instance = new UniFiTrustManager();
@@ -42,35 +46,37 @@ public class UniFiTrustManager extends X509ExtendedTrustManager {
     }
 
     @Override
-    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+    public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType)
+            throws CertificateException {
     }
 
     @Override
-    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+    public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType)
+            throws CertificateException {
     }
 
     @Override
-    public X509Certificate[] getAcceptedIssuers() {
+    public X509Certificate @Nullable [] getAcceptedIssuers() {
         return null;
     }
 
     @Override
-    public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
-            throws CertificateException {
+    public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType,
+            final @Nullable Socket socket) throws CertificateException {
     }
 
     @Override
-    public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
-            throws CertificateException {
+    public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType,
+            final @Nullable SSLEngine engine) throws CertificateException {
     }
 
     @Override
-    public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
-            throws CertificateException {
+    public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType,
+            final @Nullable Socket socket) throws CertificateException {
     }
 
     @Override
-    public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
-            throws CertificateException {
+    public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType,
+            final @Nullable SSLEngine engine) throws CertificateException {
     }
 }
index d090185b772de3f15216780276eb561437c770f4..75c89d9c67440a6411a0eb0db7cbbf0244e27908 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.unifi.internal.ssl;
 
 import javax.net.ssl.X509ExtendedTrustManager;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.core.io.net.http.TlsTrustManagerProvider;
 
 /**
@@ -25,6 +26,7 @@ import org.openhab.core.io.net.http.TlsTrustManagerProvider;
  * @author Matthew Bowman - Initial contribution
  */
 // @Component // [wip] mgb: disabled due to issues with service order loading
+@NonNullByDefault
 public class UniFiTrustManagerProvider implements TlsTrustManagerProvider {
 
     @Override
diff --git a/bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..c09f56e
--- /dev/null
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
+       <config-description uri="thing-type:unifi:controller">
+               <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="unifios" type="boolean" required="true">
+                       <label>UniFi OS</label>
+                       <description>If the UniFi Controller is running on UniFi OS.</description>
+                       <default>false</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" unit="s">
+                       <label>Refresh Interval</label>
+                       <description>The refresh interval in seconds to poll the UniFi controller</description>
+                       <default>10</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:unifi:site">
+               <parameter name="sid" type="text" required="true">
+                       <label>Site Id</label>
+                       <description>The id, name or description of the site</description>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:unifi:wlan">
+               <parameter name="wid" type="text" required="true">
+                       <label>WLAN Id</label>
+                       <description>The id or name of the wlan</description>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:unifi:client">
+               <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" unit="s">
+                       <label>Consider Home Interval</label>
+                       <description>The interval in seconds to consider the client as home</description>
+                       <default>180</default>
+               </parameter>
+       </config-description>
+
+       <config-description uri="thing-type:unifi:poePort">
+               <parameter name="portNumber" type="integer" required="true">
+                       <label>Port Number</label>
+                       <description>The number of the port as reported by the UniFi switch</description>
+               </parameter>
+               <parameter name="macAddress" type="text" required="true">
+                       <label>Switch MAC Address</label>
+                       <description>The MAC address of the switch this port is part of</description>
+               </parameter>
+       </config-description>
+
+       <config-description uri="channel-type:unifi:poeEnable">
+               <parameter name="mode" type="text">
+                       <label>On Mode</label>
+                       <description>The value to set when setting PoE on.</description>
+                       <options>
+                               <option value="auto">Auto</option>
+                               <option value="24v">24V</option>
+                               <option value="passthrough">Passthrough</option>
+                       </options>
+                       <default>auto</default>
+               </parameter>
+       </config-description>
+
+</config-description:config-descriptions>
index 8fe04b3f4a16de41e36101f77eb7c78ba066cb22..ccd435fd0e6b26a086a930f808393199a895539e 100644 (file)
@@ -6,12 +6,26 @@ binding.unifi.description = The UniFi binding integrates the UniFi controller fr
 # thing types
 
 thing-type.unifi.controller.label = UniFi Controller
-thing-type.unifi.controller.description = A UniFi controller.
+thing-type.unifi.controller.description = A UniFi controller
+thing-type.unifi.poePort.label = UniFi PoE Port
+thing-type.unifi.poePort.description = A Power Over Ethernet (PoE) port on a UniFi switch
+thing-type.unifi.site.label = UniFi Site
+thing-type.unifi.site.description = A site defined in a UniFi network
+thing-type.unifi.wiredClient.label = UniFi Wired Client
+thing-type.unifi.wiredClient.description = A wired client connected to a UniFi switch
 thing-type.unifi.wirelessClient.label = UniFi Wireless Client
 thing-type.unifi.wirelessClient.description = A wireless client connected to a UniFi wireless network
+thing-type.unifi.wlan.label = UniFi WLAN
+thing-type.unifi.wlan.description = A UniFi Wireless LAN
 
 # thing types config
 
+thing-type.config.unifi.client.cid.label = Client ID
+thing-type.config.unifi.client.cid.description = The MAC address, IP address, hostname or alias of the client
+thing-type.config.unifi.client.considerHome.label = Consider Home Interval
+thing-type.config.unifi.client.considerHome.description = The interval in seconds to consider the client as home
+thing-type.config.unifi.client.site.label = Site
+thing-type.config.unifi.client.site.description = The site where the client should be found (optional)
 thing-type.config.unifi.controller.host.label = Hostname
 thing-type.config.unifi.controller.host.description = Hostname of IP address of the UniFi Controller
 thing-type.config.unifi.controller.password.label = Password
@@ -24,12 +38,14 @@ thing-type.config.unifi.controller.unifios.label = UniFi OS
 thing-type.config.unifi.controller.unifios.description = If the UniFi Controller is running on UniFi OS.
 thing-type.config.unifi.controller.username.label = Username
 thing-type.config.unifi.controller.username.description = The username to access the UniFi Controller.
-thing-type.config.unifi.wirelessClient.cid.label = Client ID
-thing-type.config.unifi.wirelessClient.cid.description = The MAC address, IP address, hostname or alias of the client
-thing-type.config.unifi.wirelessClient.considerHome.label = Consider Home Interval
-thing-type.config.unifi.wirelessClient.considerHome.description = The interval in seconds to consider the client as home
-thing-type.config.unifi.wirelessClient.site.label = Site
-thing-type.config.unifi.wirelessClient.site.description = The site where the client should be found (optional)
+thing-type.config.unifi.poePort.macAddress.label = Switch MAC Address
+thing-type.config.unifi.poePort.macAddress.description = The MAC address of the switch this port is part of
+thing-type.config.unifi.poePort.portNumber.label = Port Number
+thing-type.config.unifi.poePort.portNumber.description = The number of the port as reported by the UniFi switch
+thing-type.config.unifi.site.sid.label = Site Id
+thing-type.config.unifi.site.sid.description = The id, name or description of the site
+thing-type.config.unifi.wlan.wid.label = WLAN Id
+thing-type.config.unifi.wlan.wid.description = The id or name of the wlan
 
 # channel types
 
@@ -39,6 +55,12 @@ channel-type.unifi.blocked.label = Blocked
 channel-type.unifi.blocked.description = Is device blocked
 channel-type.unifi.essid.label = Wireless Network
 channel-type.unifi.essid.description = Wireless Network (ESSID) the wireless client is connected to
+channel-type.unifi.experience.label = Experience
+channel-type.unifi.experience.description = The wired/wireless experience of the client
+channel-type.unifi.guest.label = Guest
+channel-type.unifi.guest.description = Is the client connected a guest
+channel-type.unifi.guestClients.label = Guest Clients
+channel-type.unifi.guestClients.description = Number of guest clients connected
 channel-type.unifi.ipAddress.label = IP Address
 channel-type.unifi.ipAddress.description = IP address of the client
 channel-type.unifi.lastSeen.label = Last Seen
@@ -46,12 +68,76 @@ channel-type.unifi.lastSeen.description = Timestamp of when the client was last
 channel-type.unifi.macAddress.label = MAC Address
 channel-type.unifi.macAddress.description = MAC address of the client
 channel-type.unifi.online.label = Online
-channel-type.unifi.online.description = Online status of the wireless client
+channel-type.unifi.online.description = Online status of the client
+channel-type.unifi.poeCmd.label = PoE Command
+channel-type.unifi.poeCmd.description = Command that can be given to the PoE port
+channel-type.unifi.poeCmd.command.option.power-cycle = Power Cycle
+channel-type.unifi.poeCurrent.label = Port PoE Current
+channel-type.unifi.poeCurrent.description = Current usage of the PoE port
+channel-type.unifi.poeEnable.label = Enabled
+channel-type.unifi.poeEnable.description = If PoE is enabled
+channel-type.unifi.poeMode.label = PoE Mode
+channel-type.unifi.poeMode.description = The PoE mode the port is in
+channel-type.unifi.poeMode.state.option.off = Off
+channel-type.unifi.poeMode.state.option.auto = Auto
+channel-type.unifi.poeMode.state.option.24v = 24V
+channel-type.unifi.poeMode.state.option.passthrough = Passthrough
+channel-type.unifi.poePower.label = Port PoE Power
+channel-type.unifi.poePower.description = Power usage of the PoE port
+channel-type.unifi.poeVoltage.label = Port PoE Voltage
+channel-type.unifi.poeVoltage.description = Voltage usage of the PoE port
+channel-type.unifi.portOnline.label = Port Active
+channel-type.unifi.portOnline.description = PoE port is active
+channel-type.unifi.qrcodeEncoding.label = QR Code Encoding
+channel-type.unifi.qrcodeEncoding.description = MECARD like encoding to generate a QRCode for easy access to the Wi-Fi network
 channel-type.unifi.reconnect.label = Reconnect
 channel-type.unifi.reconnect.description = Forces a client to reconnect
 channel-type.unifi.rssi.label = Received Signal Strength Indicator
 channel-type.unifi.rssi.description = Received Signal Strength Indicator (RSSI) of the wireless client
+channel-type.unifi.security.label = Security
+channel-type.unifi.security.description = Security protocol of the Wi-Fi network
 channel-type.unifi.site.label = Site Name
-channel-type.unifi.site.description = UniFi Site the client is associated with
+channel-type.unifi.site.description = UniFi Site the device is associated with
+channel-type.unifi.totalClients.label = Total Clients
+channel-type.unifi.totalClients.description = Total number of clients connected
 channel-type.unifi.uptime.label = Uptime
 channel-type.unifi.uptime.description = Uptime of the client (in seconds)
+channel-type.unifi.wiredClients.label = Wired Clients
+channel-type.unifi.wiredClients.description = Number of wired clients connected
+channel-type.unifi.wirelessClients.label = Wireless Clients
+channel-type.unifi.wirelessClients.description = Number of wireless clients connected
+channel-type.unifi.wirelessCmd.label = Wireless Command
+channel-type.unifi.wirelessCmd.description = Command that can be given to the wireless client
+channel-type.unifi.wirelessCmd.command.option.reconnect = Reconnect
+channel-type.unifi.wlanBand.label = WLAN Band
+channel-type.unifi.wlanBand.description = Wireless LAN band of the Wi-Fi network
+channel-type.unifi.wlanEnable.label = Enable
+channel-type.unifi.wlanEnable.description = Enable status of the wLAN
+channel-type.unifi.wlanEssid.label = Wireless Network
+channel-type.unifi.wlanEssid.description = Wireless Network (ESSID)
+channel-type.unifi.wpaEnc.label = WPA Encoding
+channel-type.unifi.wpaEnc.description = WPA Encoding of the Wi-Fi network
+channel-type.unifi.wpaMode.label = WPA Mode
+channel-type.unifi.wpaMode.description = WPA Mode of the Wi-Fi network
+channel-type.unifi.passphrase.label = Passphrase
+channel-type.unifi.passphrase.description = Passphrase of the Wi-Fi network
+
+# channel types config
+
+channel-type.config.unifi.poeEnable.mode.label = On Mode
+channel-type.config.unifi.poeEnable.mode.description = The value to set when setting PoE on.
+channel-type.config.unifi.poeEnable.mode.option.auto = Auto
+channel-type.config.unifi.poeEnable.mode.option.24v = 24V
+channel-type.config.unifi.poeEnable.mode.option.passthrough = Passthrough
+
+# status messages
+
+error.bridge.offline.communication_error = Error communicating with the UniFi controller.
+error.bridge.offline.invalid_credentials = Invalid username and/or password - please double-check your configuration.
+error.bridge.offline.invalid_hostname = Invalid hostname - please double-check your configuration.
+error.bridge.offline.ssl_error = Error establishing an SSL connection with the UniFi controller.
+error.thing.client.offline.configuration_error = You must define a MAC address, IP address, hostname or alias for this thing.
+error.thing.offline.bridge_offline = The UniFi Controller is currently offline.
+error.thing.offline.configuration_error = You must choose a UniFi Controller for this thing.
+error.thing.poe.offline.configuration_error = The configuration parameter macAddress must be set and not be empty.
+error.thing.site.offline.configuration_error = The configuration parameter sid must be set and not be empty.
index 4758944a5e9ee87149cef4470d69819dafef7f88..103fc4ef2c8c6a22ee8b7ab59db275ae706386fd 100644 (file)
@@ -12,6 +12,12 @@ thing-type.unifi.wirelessClient.description = Ein drahtloser Client der mit eine
 
 # thing types config
 
+thing-type.config.unifi.client.cid.label = Client-ID
+thing-type.config.unifi.client.cid.description = Die MAC-Adresse, IP-Adresse, Hostname oder Alias des Clients
+thing-type.config.unifi.client.considerHome.label = Anwesenheitsinterval
+thing-type.config.unifi.client.considerHome.description = Das Intervall in Sekunden, um den Client als zu Hause anwesend zu betrachten
+thing-type.config.unifi.client.site.label = Site
+thing-type.config.unifi.client.site.description = Die Site, auf der der Client gefunden werden soll (optional)
 thing-type.config.unifi.controller.host.label = Hostname
 thing-type.config.unifi.controller.host.description = Hostname oder IP-Adresse des UniFi-Controllers
 thing-type.config.unifi.controller.password.label = Passwort
@@ -24,12 +30,6 @@ thing-type.config.unifi.controller.unifios.label = UniFi OS
 thing-type.config.unifi.controller.unifios.description = Ob der UniFi Controller unter UniFi-OS lƤuft.
 thing-type.config.unifi.controller.username.label = Benutzername
 thing-type.config.unifi.controller.username.description = Der Benutzername für den Zugriff auf den UniFi-Controller.
-thing-type.config.unifi.wirelessClient.cid.label = Client-ID
-thing-type.config.unifi.wirelessClient.cid.description = Die MAC-Adresse, IP-Adresse, Hostname oder Alias des Clients
-thing-type.config.unifi.wirelessClient.considerHome.label = Anwesenheitsinterval
-thing-type.config.unifi.wirelessClient.considerHome.description = Das Intervall in Sekunden, um den Client als zu Hause anwesend zu betrachten
-thing-type.config.unifi.wirelessClient.site.label = Site
-thing-type.config.unifi.wirelessClient.site.description = Die Site, auf der der Client gefunden werden soll (optional)
 
 # channel types
 
index dd5fb0839e990025d5b611b6901f857e0e7218a3..0989838cf29e231b9144ca5d70611fca514275ae 100644 (file)
@@ -12,6 +12,12 @@ thing-type.unifi.wirelessClient.description = A UniFi hĆ”lózathoz kapcsolódó
 
 # thing types config
 
+thing-type.config.unifi.client.cid.label = Kliens azonosĆ­tó
+thing-type.config.unifi.client.cid.description = Az Ć¼gyfĆ©l MAC cĆ­me, IP cĆ­me, gĆ©pneve vagy Ć”lneve (alias)
+thing-type.config.unifi.client.considerHome.label = ItthonlĆ©t vizsgĆ”lati időkƶze
+thing-type.config.unifi.client.considerHome.description = Az ithonlĆ©t vizsgĆ”lati időkƶze mĆ”sodpercben
+thing-type.config.unifi.client.site.label = TelepĆ­tĆ©si hely
+thing-type.config.unifi.client.site.description = A telepĆ­tĆ©si hely, ahol az Ć¼gyfĆ©l tartózkodik (nem szüksĆ©ges)
 thing-type.config.unifi.controller.host.label = GĆ©pnĆ©v
 thing-type.config.unifi.controller.host.description = A UniFi vezĆ©rlő gĆ©pneve vagy IP cĆ­me
 thing-type.config.unifi.controller.password.label = Jelszó
@@ -24,12 +30,6 @@ thing-type.config.unifi.controller.unifios.label = UniFi OS
 thing-type.config.unifi.controller.unifios.description = A UniFi vezĆ©rlő UniFi OS-t futtat.
 thing-type.config.unifi.controller.username.label = FelhasznĆ”lónĆ©v
 thing-type.config.unifi.controller.username.description = A UniFi vezĆ©rlőhƶz szüksĆ©ges felhasznĆ”lói nĆ©v.
-thing-type.config.unifi.wirelessClient.cid.label = Kliens azonosĆ­tó
-thing-type.config.unifi.wirelessClient.cid.description = Az Ć¼gyfĆ©l MAC cĆ­me, IP cĆ­me, gĆ©pneve vagy Ć”lneve (alias)
-thing-type.config.unifi.wirelessClient.considerHome.label = ItthonlĆ©t vizsgĆ”lati időkƶze
-thing-type.config.unifi.wirelessClient.considerHome.description = Az ithonlĆ©t vizsgĆ”lati időkƶze mĆ”sodpercben
-thing-type.config.unifi.wirelessClient.site.label = TelepĆ­tĆ©si hely
-thing-type.config.unifi.wirelessClient.site.description = A telepĆ­tĆ©si hely, ahol az Ć¼gyfĆ©l tartózkodik (nem szüksĆ©ges)
 
 # channel types
 
diff --git a/bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi_nl.properties b/bundles/org.openhab.binding.unifi/src/main/resources/OH-INF/i18n/unifi_nl.properties
new file mode 100644 (file)
index 0000000..9d28d6d
--- /dev/null
@@ -0,0 +1,143 @@
+# binding
+
+binding.unifi.name = UniFi Binding
+binding.unifi.description = De UniFi-binding integreert de UniFi-controller van Ubiquiti Networks om het volgen van Wi-Fi-cliĆ«nten te vergemakkelijken.
+
+# thing types
+
+thing-type.unifi.controller.label = UniFi Controller
+thing-type.unifi.controller.description = Een UniFi-controller
+thing-type.unifi.poePort.label = UniFi PoE-poort
+thing-type.unifi.poePort.description = Een Power Over Ethernet (PoE)-poort op een UniFi-switch
+thing-type.unifi.site.label = UniFi Site
+thing-type.unifi.site.description = Een site gedefinieerd in een UniFi-netwerk
+thing-type.unifi.wiredClient.label = UniFi Bekabelde CliĆ«nt
+thing-type.unifi.wiredClient.description = Een bekabelde cliĆ«nt aangesloten op een UniFi-switch
+thing-type.unifi.wirelessClient.label = UniFi Draadloze CliĆ«nt
+thing-type.unifi.wirelessClient.description = Een draadloze cliĆ«nt die is verbonden met een draadloos UniFi-netwerk
+thing-type.unifi.wlan.label = UniFi WLAN
+thing-type.unifi.wlan.description = Een UniFi draadloos lokaal netwerk (wLAN)
+
+# thing types config
+
+thing-type.config.unifi.client.cid.label = CliĆ«nt-Id
+thing-type.config.unifi.client.cid.description = Het MAC-adres, IP-adres, hostnaam of alias van de cliĆ«nt
+thing-type.config.unifi.client.considerHome.label = Aanwezigheidsinterval
+thing-type.config.unifi.client.considerHome.description = Het interval in seconden om de cliĆ«nt als thuis te beschouwen
+thing-type.config.unifi.client.site.label = Site
+thing-type.config.unifi.client.site.description = De site waar de cliĆ«nt moet worden gevonden (optioneel)
+thing-type.config.unifi.controller.host.label = Hostnaam
+thing-type.config.unifi.controller.host.description = Hostnaam van het IP-adres van de UniFi Controller
+thing-type.config.unifi.controller.password.label = Wachtwoord
+thing-type.config.unifi.controller.password.description = Het wachtwoord voor toegang tot de UniFi Controller.
+thing-type.config.unifi.controller.port.label = Poort
+thing-type.config.unifi.controller.port.description = Poort van de UniFi Controller
+thing-type.config.unifi.controller.refresh.label = Vernieuwingsinterval
+thing-type.config.unifi.controller.refresh.description = Het interval in seconden om de UniFi-controller te pollen
+thing-type.config.unifi.controller.unifios.label = UniFi OS
+thing-type.config.unifi.controller.unifios.description = Of de UniFi Controller op UniFi OS draait.
+thing-type.config.unifi.controller.username.label = Gebruikersnaam
+thing-type.config.unifi.controller.username.description = De gebruikersnaam voor toegang tot de UniFi Controller.
+thing-type.config.unifi.poePort.macAddress.label = Wissel van MAC-adres
+thing-type.config.unifi.poePort.macAddress.description = Het MAC-adres van de switch waar deze poort deel van uitmaakt
+thing-type.config.unifi.poePort.portNumber.label = Poortnummer
+thing-type.config.unifi.poePort.portNumber.description = Het nummer van de poort zoals gerapporteerd door de UniFi-switch
+thing-type.config.unifi.site.sid.label = Site-Id
+thing-type.config.unifi.site.sid.description = Het id, de naam of beschrijving van de site
+thing-type.config.unifi.wlan.wid.label = WLAN-Id
+thing-type.config.unifi.wlan.wid.description = Het id of de naam van de wLAN
+
+# channel types
+
+channel-type.unifi.ap.label = Toegangspunt
+channel-type.unifi.ap.description = Toegangspunt waarmee de draadloze cliĆ«nt is verbonden
+channel-type.unifi.blocked.label = Geblokkeerd
+channel-type.unifi.blocked.description = Is apparaat geblokkeerd
+channel-type.unifi.essid.label = Draadloos Netwerk
+channel-type.unifi.essid.description = Draadloos netwerk (ESSID) waarmee de draadloze cliĆ«nt is verbonden
+channel-type.unifi.experience.label = Ervaring
+channel-type.unifi.experience.description = De ervaring van de bedraade/draadloze cliĆ«nt
+channel-type.unifi.guest.label = Gast
+channel-type.unifi.guest.description = Is de cliĆ«nt verbonden als gast?
+channel-type.unifi.guestClients.label = Gasten
+channel-type.unifi.guestClients.description = Aantal verbonden gasten
+channel-type.unifi.ipAddress.label = IP-adres
+channel-type.unifi.ipAddress.description = IP-adres van de cliĆ«nt
+channel-type.unifi.lastSeen.label = Laatst Gezien
+channel-type.unifi.lastSeen.description = Tijdstempel van wanneer de cliĆ«nt voor het laatst is gezien
+channel-type.unifi.macAddress.label = MAC-adres
+channel-type.unifi.macAddress.description = MAC-adres van de cliĆ«nt
+channel-type.unifi.online.label = Online
+channel-type.unifi.online.description = Online status van de cliĆ«nt
+channel-type.unifi.poeCmd.label = PoE Commando
+channel-type.unifi.poeCmd.description = Commando die kan worden gegeven aan de PoE poort
+channel-type.unifi.poeCmd.command.option.power\-cycle = Power Cycle
+channel-type.unifi.poeCurrent.label = Poort PoE Stroom
+channel-type.unifi.poeCurrent.description = Huidig stroom verbruik van de PoE-poort
+channel-type.unifi.poeEnable.label = Actief
+channel-type.unifi.poeEnable.description = Of PoE is ingeschakeld
+channel-type.unifi.poeMode.label = PoE-modus
+channel-type.unifi.poeMode.description = De PoE-modus waarin de poort zich bevindt.
+channel-type.unifi.poeMode.state.option.off = Uit
+channel-type.unifi.poeMode.state.option.auto = Auto
+channel-type.unifi.poeMode.state.option.24v = 24V
+channel-type.unifi.poeMode.state.option.passthrough = Doorlussen
+channel-type.unifi.poePower.label = Poort PoE Stroom
+channel-type.unifi.poePower.description = Stroomverbruik van de PoE-poort
+channel-type.unifi.poeVoltage.label = Poort PoE Voltage
+channel-type.unifi.poeVoltage.description = Voltage van de PoE-poort
+channel-type.unifi.portOnline.label = Poort Actief
+channel-type.unifi.portOnline.description = Poort is in gebruik
+channel-type.unifi.qrcodeEncoding.label = QR Code Codering
+channel-type.unifi.qrcodeEncoding.description = MECARD-achtige codering om een QR Code te genereren voor snelle toegang tot het Wi-Fi-netwerk
+channel-type.unifi.reconnect.label = Opnieuw Verbinden
+channel-type.unifi.reconnect.description = Dwingt een cliĆ«nt om opnieuw verbinding te maken
+channel-type.unifi.rssi.label = Signaalsterkte
+channel-type.unifi.rssi.description = Ontvanger signaal sterkte indicator (RSSI) van de draadloze cliĆ«nt
+channel-type.unifi.security.label = Beveiliging
+channel-type.unifi.security.description = Beveiligingsprotocol van het Wi-Fi-netwerk
+channel-type.unifi.site.label = Sitenaam
+channel-type.unifi.site.description = UniFi site waaraan het apparaat is gekoppeld
+channel-type.unifi.totalClients.label = Totaal Aantal CliĆ«nten
+channel-type.unifi.totalClients.description = Totaal aantal aangesloten cliĆ«nten
+channel-type.unifi.uptime.label = Uptime
+channel-type.unifi.uptime.description = Uptime van de cliĆ«nt (in seconden)
+channel-type.unifi.wiredClients.label = Bedrade CliĆ«nten
+channel-type.unifi.wiredClients.description = Aantal aangesloten bedrade cliĆ«nten
+channel-type.unifi.wirelessClients.label = Draadloze CliĆ«nten
+channel-type.unifi.wirelessClients.description = Aantal aangesloten draadloze cliĆ«nten
+channel-type.unifi.wirelessCmd.label = Wireless Commando
+channel-type.unifi.wirelessCmd.description = Commando die aan de  draadloze cliĆ«nt gegeven kan worden
+channel-type.unifi.wirelessCmd.command.option.reconnect = Opnieuw Verbinden
+channel-type.unifi.wlanBand.label = WLAN Band
+channel-type.unifi.wlanBand.description = Draadloze LAN-band van het Wi-Fi-netwerk
+channel-type.unifi.wlanEnable.label = Actief
+channel-type.unifi.wlanEnable.description = Of the wLAN actief is
+channel-type.unifi.wlanEssid.label = Draadloos netwerk
+channel-type.unifi.wlanEssid.description = Draadloos netwerk (ESSID)
+channel-type.unifi.wpaEnc.label = WPA-codering
+channel-type.unifi.wpaEnc.description = WPA-codering van het Wi-Fi-netwerk
+channel-type.unifi.wpaMode.label = WPA-modus
+channel-type.unifi.wpaMode.description = WPA-modus van het Wi-Fi-netwerk
+channel-type.unifi.passphrase.label = Wachtwoord
+channel-type.unifi.passphrase.description = Wachtwoord van het Wi-Fi-netwerk
+
+# channel types config
+
+channel-type.config.unifi.poeEnable.mode.label = Aan-modus
+channel-type.config.unifi.poeEnable.mode.description = De waarde die moet worden ingesteld wanneer PoE wordt ingeschakeld.
+channel-type.config.unifi.poeEnable.mode.option.auto = Auto
+channel-type.config.unifi.poeEnable.mode.option.24v = 24V
+channel-type.config.unifi.poeEnable.mode.option.passthrough = Doorlussen
+
+# status messages
+
+error.bridge.offline.communication_error = Fout bij communicatie met de UniFi-controller.
+error.bridge.offline.invalid_credentials = Ongeldige gebruikersnaam en/of wachtwoord - controleer uw configuratie.
+error.bridge.offline.invalid_hostname = Ongeldige hostnaam - controleer uw configuratie nogmaals.
+error.bridge.offline.ssl_error = Fout bij het tot stand brengen van een SSL-verbinding met de UniFi-controller.
+error.thing.client.offline.configuration_error = Je moet een MAC-adres, IP-adres, hostnaam of alias voor dit ding definiĆ«ren.
+error.thing.offline.bridge_offline = De UniFi-controller is momenteel offline.
+error.thing.offline.configuration_error = Je moet hiervoor een UniFi-controller kiezen.
+error.thing.poe.offline.configuration_error = De configuratieparameter macAddress moet zijn ingesteld en mag niet leeg zijn.
+error.thing.site.offline.configuration_error = De configuratieparameter sid moet ingesteld zijn en mag niet leeg zijn.
index 87069a33ca094968b173f5159544596a069d8112..40f70b3182813194bbaf4b6f62ecfbc76ac47deb 100644 (file)
@@ -7,47 +7,83 @@
        <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="unifios" type="boolean" required="true">
-                               <label>UniFi OS</label>
-                               <description>If the UniFi Controller is running on UniFi OS.</description>
-                               <default>false</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>
+               <description>A UniFi controller</description>
 
+               <config-description-ref uri="thing-type:unifi:controller"/>
        </bridge-type>
 
-       <!-- <thing-type id="wiredClient"> .. coming soon .. </thing-type> -->
+       <thing-type id="site">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
 
-       <thing-type id="wirelessClient">
+               <label>UniFi Site</label>
+               <description>A site defined in a UniFi network</description>
+
+               <channels>
+                       <channel id="totalClients" typeId="totalClients"/>
+                       <channel id="wirelessClients" typeId="wirelessClients"/>
+                       <channel id="wiredClients" typeId="wiredClients"/>
+                       <channel id="guestClients" typeId="guestClients"/>
+               </channels>
+
+               <representation-property>sid</representation-property>
+
+               <config-description-ref uri="thing-type:unifi:site"/>
+       </thing-type>
+
+       <thing-type id="wlan">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+
+               <label>UniFi WLAN</label>
+               <description>A UniFi Wireless LAN</description>
+
+               <channels>
+                       <channel id="enable" typeId="wlanEnable"/>
+                       <channel id="wirelessClients" typeId="wirelessClients"/>
+                       <channel id="guestClients" typeId="guestClients"/>
+                       <channel id="essid" typeId="wlanEssid"/>
+                       <channel id="site" typeId="site"/>
+                       <channel id="security" typeId="security"/>
+                       <channel id="wlanBand" typeId="wlanBand"/>
+                       <channel id="wpaEnc" typeId="wpaEnc"/>
+                       <channel id="wpaMode" typeId="wpaMode"/>
+                       <channel id="passphrase" typeId="passphrase"/>
+                       <channel id="qrcodeEncoding" typeId="qrcodeEncoding"/>
+               </channels>
+
+               <representation-property>wid</representation-property>
+
+               <config-description-ref uri="thing-type:unifi:wlan"/>
+       </thing-type>
+
+       <thing-type id="wiredClient">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+               <label>UniFi Wired Client</label>
+               <description>A wired client connected to a UniFi switch</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"/>
+                       <channel id="experience" typeId="experience"/>
+               </channels>
+
+               <representation-property>cid</representation-property>
+
+               <config-description-ref uri="thing-type:unifi:client"/>
+       </thing-type>
 
+       <thing-type id="wirelessClient">
                <supported-bridge-type-refs>
                        <bridge-type-ref id="controller"/>
                </supported-bridge-type-refs>
                        <channel id="uptime" typeId="uptime"/>
                        <channel id="lastSeen" typeId="lastSeen"/>
                        <channel id="blocked" typeId="blocked"/>
+                       <channel id="experience" typeId="experience"/>
                        <!-- additional wireless client channels -->
+                       <channel id="guest" typeId="guest"/>
                        <channel id="ap" typeId="ap"/>
                        <channel id="essid" typeId="essid"/>
                        <channel id="rssi" typeId="rssi"/>
+                       <channel id="cmd" typeId="wirelessCmd"/>
                        <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>
+               <config-description-ref uri="thing-type:unifi:client"/>
+       </thing-type>
+
+       <thing-type id="poePort">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="controller"/>
+               </supported-bridge-type-refs>
+
+               <label>UniFi PoE Port</label>
+               <description>A Power Over Ethernet (PoE) port on a UniFi switch</description>
 
+               <channels>
+                       <channel id="online" typeId="portOnline"/>
+                       <channel id="mode" typeId="poeMode"/>
+                       <channel id="enable" typeId="poeEnable"/>
+                       <channel id="cmd" typeId="poeCmd"/>
+                       <channel id="power" typeId="poePower"/>
+                       <channel id="voltage" typeId="poeVoltage"/>
+                       <channel id="current" typeId="poeCurrent"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:unifi:poePort"/>
        </thing-type>
 
+       <!-- Channels -->
+
+       <channel-type id="totalClients">
+               <item-type>Number</item-type>
+               <label>Total Clients</label>
+               <description>Total number of clients connected</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wirelessClients">
+               <item-type>Number</item-type>
+               <label>Wireless Clients</label>
+               <description>Number of wireless clients connected</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wiredClients">
+               <item-type>Number</item-type>
+               <label>Wired Clients</label>
+               <description>Number of wired clients connected</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="guestClients">
+               <item-type>Number</item-type>
+               <label>Guest Clients</label>
+               <description>Number of guest clients connected</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wlanEnable">
+               <item-type>Switch</item-type>
+               <label>Enable</label>
+               <description>Enable status of the wLAN</description>
+       </channel-type>
+
+       <channel-type id="wlanEssid">
+               <item-type>String</item-type>
+               <label>Wireless Network</label>
+               <description>Wireless Network (ESSID)</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="security" advanced="true">
+               <item-type>String</item-type>
+               <label>Security</label>
+               <description>Security protocol of the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wlanBand" advanced="true">
+               <item-type>String</item-type>
+               <label>WLAN Band</label>
+               <description>Wireless LAN band of the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wpaEnc" advanced="true">
+               <item-type>String</item-type>
+               <label>WPA Encoding</label>
+               <description>WPA Encoding of the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="wpaMode" advanced="true">
+               <item-type>String</item-type>
+               <label>WPA Mode</label>
+               <description>WPA Mode of the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="passphrase" advanced="true">
+               <item-type>String</item-type>
+               <label>Passphrase</label>
+               <description>Passphrase of the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="qrcodeEncoding">
+               <item-type>String</item-type>
+               <label>QR Code Encoding</label>
+               <description>MECARD like encoding to generate a QRCode for easy access to the Wi-Fi network</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
        <channel-type id="online">
                <item-type>Switch</item-type>
                <label>Online</label>
-               <description>Online status of the wireless client</description>
+               <description>Online status of the 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>
+               <description>UniFi Site the device is associated with</description>
                <state readOnly="true"></state>
        </channel-type>
 
                <description>Is device blocked</description>
        </channel-type>
 
-       <channel-type id="reconnect">
+       <channel-type id="guest">
+               <item-type>Switch</item-type>
+               <label>Guest</label>
+               <description>Is the client connected a guest</description>
+       </channel-type>
+
+       <channel-type id="experience">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Experience</label>
+               <description>The wired/wireless experience of the client</description>
+               <state pattern="%d %unit%" readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="reconnect" advanced="true">
                <item-type>Switch</item-type>
                <label>Reconnect</label>
                <description>Forces a client to reconnect</description>
        </channel-type>
 
+       <channel-type id="wirelessCmd">
+               <item-type>String</item-type>
+               <label>Wireless Command</label>
+               <description>Command that can be given to the wireless client</description>
+               <command>
+                       <options>
+                               <option value="reconnect">Reconnect</option>
+                       </options>
+               </command>
+       </channel-type>
+
+       <channel-type id="portOnline">
+               <item-type>Switch</item-type>
+               <label>Port Active</label>
+               <description>PoE port is active</description>
+               <state readOnly="true"></state>
+       </channel-type>
+
+       <channel-type id="poeEnable">
+               <item-type>Switch</item-type>
+               <label>Enabled</label>
+               <description>If PoE is enabled</description>
+               <config-description-ref uri="channel-type:unifi:poeEnable"/>
+       </channel-type>
+
+       <channel-type id="poeMode">
+               <item-type>String</item-type>
+               <label>PoE Mode</label>
+               <description>The PoE mode the port is in</description>
+               <state>
+                       <options>
+                               <option value="off">Off</option>
+                               <option value="auto">Auto</option>
+                               <option value="24v">24V</option>
+                               <option value="passthrough">Passthrough</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="poeCmd">
+               <item-type>String</item-type>
+               <label>PoE Command</label>
+               <description>Command that can be given to the PoE port</description>
+               <command>
+                       <options>
+                               <option value="power-cycle">Power Cycle</option>
+                       </options>
+               </command>
+       </channel-type>
+
+       <channel-type id="poePower" advanced="true">
+               <item-type>Number:Power</item-type>
+               <label>Port PoE Power</label>
+               <description>Power usage of the PoE port</description>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="poeVoltage" advanced="true">
+               <item-type>Number:ElectricPotential</item-type>
+               <label>Port PoE Voltage</label>
+               <description>Voltage usage of the PoE port</description>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="poeCurrent" advanced="true">
+               <item-type>Number:ElectricCurrent</item-type>
+               <label>Port PoE Current</label>
+               <description>Current usage of the PoE port</description>
+               <state pattern="%.2f %unit%" readOnly="true"/>
+       </channel-type>
+
 </thing:thing-descriptions>