[unifi] Add support for new thing type access point (#17499)

Signed-off-by: Thomas Lauterbach <thomas_lauterbach@arcor.de>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Thomas Lauterbach 2024-10-04 17:47:28 +02:00 committed by Ciprian Pascu
parent 21bd126386
commit 260f3e6023
12 changed files with 280 additions and 16 deletions

View File

@ -10,6 +10,7 @@ This binding integrates with [Ubiquiti UniFi Networks](https://www.ubnt.com/prod
- `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
- `accessPoint` - An access point managed by the UniFi controller software
## Discovery
@ -103,6 +104,15 @@ The following table describes the `poePort` configuration parameters:
| 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 |
### `accessPoint`
The following table describes the `accessPoint` configuration parameters:
| Parameter | Description | Config | Default |
| ------------ | ------------------------------------------------|--------- | ------- |
| mac | The MAC address of the access point | Required | - |
| site | The site where the access point should be found | Optional | - |
## Channels
### `site`
@ -225,6 +235,14 @@ The `poePort` information that is retrieved is available as these channels:
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`.
### `accessPoint`
The `accessPoint` information that is retrieved is available as these channels:
| Channel ID | Item Type | Description | Permissions |
|------------|-----------|------------------------------------|-------------|
| enable | Switch | Enable or disable the access point | Read, Write |
## Rule Actions
As an alternative to using the `guestVoucher` and `guestVouchersGenerate` channels on the `site` thing, it is possible to use rule actions on the thing to generate, revoke and list guest vouchers.

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.unifi.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link UniFiAccessPointThingConfig} encapsulates all the configuration options for an instance of the
* {@link org.openhab.binding.unifi.internal.handler.UniFiAccessPointThingHandler}.
*
* @author Thomas Lauterbach - Initial contribution
*/
@NonNullByDefault
@SuppressWarnings("unused")
public class UniFiAccessPointThingConfig {
private String macAddress = "";
private String site = "";
public String getSite() {
return site;
}
private void setSite(final String site) {
// method to avoid ide auto format mark the field as final
this.site = site;
}
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();
}
}

View File

@ -38,10 +38,12 @@ public final class UniFiBindingConstants {
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 ThingTypeUID THING_TYPE_ACCESS_POINT = new ThingTypeUID(BINDING_ID, "accessPoint");
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);
THING_TYPE_WLAN, THING_TYPE_WIRED_CLIENT, THING_TYPE_WIRELESS_CLIENT, THING_TYPE_POE_PORT,
THING_TYPE_ACCESS_POINT);
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);
THING_TYPE_WIRED_CLIENT, THING_TYPE_WIRELESS_CLIENT, THING_TYPE_POE_PORT, THING_TYPE_ACCESS_POINT);
// List of site channels
public static final String CHANNEL_TOTAL_CLIENTS = "totalClients";
@ -93,6 +95,9 @@ public final class UniFiBindingConstants {
public static final String CHANNEL_PORT_POE_VOLTAGE = "voltage";
public static final String CHANNEL_PORT_POE_CURRENT = "current";
// List of access point channels
public static final String CHANNEL_AP_ENABLE = "enable";
// List of all Parameters
public static final String PARAMETER_HOST = "host";
public static final String PARAMETER_PORT = "port";
@ -113,6 +118,9 @@ public final class UniFiBindingConstants {
public static final String PARAMETER_MAC_ADDRESS = "macAddress";
public static final String PARAMETER_WIFI_NAME = "wifi";
// UniFi device types
public static final String DEVICE_TYPE_UAP = "uap";
private UniFiBindingConstants() {
// Constants class
}

View File

@ -18,6 +18,7 @@ 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.UniFiAccessPointThingHandler;
import org.openhab.binding.unifi.internal.handler.UniFiClientThingHandler;
import org.openhab.binding.unifi.internal.handler.UniFiControllerThingHandler;
import org.openhab.binding.unifi.internal.handler.UniFiPoePortThingHandler;
@ -87,6 +88,8 @@ public class UniFiThingHandlerFactory extends BaseThingHandlerFactory {
return new UniFiClientThingHandler(thing);
} else if (THING_TYPE_POE_PORT.equals(thingTypeUID)) {
return new UniFiPoePortThingHandler(thing);
} else if (THING_TYPE_ACCESS_POINT.equals(thingTypeUID)) {
return new UniFiAccessPointThingHandler(thing);
}
return null;
}

View File

@ -215,6 +215,15 @@ public class UniFiController {
refresh();
}
public void disableAccessPoint(final UniFiDevice device, final boolean disable) throws UniFiException {
final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.PUT, gson);
req.setAPIPath(String.format("/api/s/%s/rest/device/%s", device.getSite().getName(), device.getId()));
req.setBodyParameter("_id", device.getId());
req.setBodyParameter("disabled", disable ? "true" : "false");
executeRequest(req);
refresh();
}
public void generateVouchers(final UniFiSite site, final int count, final int expiration, final int users,
@Nullable Integer upLimit, @Nullable Integer downLimit, @Nullable Integer dataQuota) throws UniFiException {
final UniFiControllerRequest<Void> req = newRequest(Void.class, HttpMethod.POST, gson);

View File

@ -124,6 +124,10 @@ public class UniFiControllerCache {
return devicesCache.get(id);
}
public Collection<UniFiDevice> getDevices() {
return devicesCache.values();
}
public UniFiSwitchPorts getSwitchPorts(@Nullable final String deviceId) {
return deviceId == null ? new UniFiSwitchPorts()
: devicesToPortTables.getOrDefault(deviceId, new UniFiSwitchPorts());

View File

@ -38,6 +38,8 @@ public class UniFiDevice implements HasId {
private String model;
private String type;
private String name;
private String siteId;
@ -46,6 +48,8 @@ public class UniFiDevice implements HasId {
private JsonObject[] portOverrides;
private boolean disabled;
public UniFiDevice(final UniFiControllerCache cache) {
this.cache = cache;
}
@ -55,6 +59,10 @@ public class UniFiDevice implements HasId {
return id;
}
public String getType() {
return type;
}
public String getModel() {
return model;
}
@ -79,8 +87,13 @@ public class UniFiDevice implements HasId {
return portOverrides;
}
public boolean isDisabled() {
return disabled;
}
@Override
public String toString() {
return String.format("UniFiDevice{mac: '%s', name: '%s', model: '%s', site: %s}", mac, name, model, getSite());
return String.format("UniFiDevice{mac: '%s', name: '%s', type: %s, model: '%s', disabled: %b, site: %s}", mac,
name, type, model, disabled, getSite());
}
}

View File

@ -0,0 +1,112 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.unifi.internal.handler;
import static org.openhab.binding.unifi.internal.UniFiBindingConstants.CHANNEL_AP_ENABLE;
import static org.openhab.binding.unifi.internal.UniFiBindingConstants.DEVICE_TYPE_UAP;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.unifi.internal.UniFiAccessPointThingConfig;
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.UniFiDevice;
import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
import org.openhab.core.library.types.OnOffType;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An access point managed by the UniFi controller software.
*
* @author Thomas Lauterbach - Initial contribution
*/
@NonNullByDefault
public class UniFiAccessPointThingHandler extends UniFiBaseThingHandler<UniFiDevice, UniFiAccessPointThingConfig> {
private final Logger logger = LoggerFactory.getLogger(UniFiAccessPointThingHandler.class);
private UniFiAccessPointThingConfig config = new UniFiAccessPointThingConfig();
public UniFiAccessPointThingHandler(final Thing thing) {
super(thing);
}
private static boolean belongsToSite(final UniFiDevice client, final String siteName) {
boolean result = true;
if (!siteName.isEmpty()) {
final UniFiSite site = client.getSite();
if (site == null || !site.matchesName(siteName)) {
result = false;
}
}
return result;
}
@Override
protected boolean initialize(final UniFiAccessPointThingConfig config) {
this.config = config;
if (!config.isValid()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.thing.ap.offline.configuration_error");
return false;
}
return true;
}
@Override
protected @Nullable UniFiDevice getEntity(final UniFiControllerCache cache) {
final UniFiDevice device = cache.getDevice(config.getMacAddress());
if (device == null || !belongsToSite(device, config.getSite()) || !DEVICE_TYPE_UAP.equals(device.getType())) {
return null;
}
return device;
}
@Override
protected State getChannelState(final UniFiDevice device, final String channelId) {
State state = getDefaultState(channelId);
switch (channelId) {
case CHANNEL_AP_ENABLE:
state = OnOffType.from(!device.isDisabled());
break;
}
return state;
}
@Override
protected boolean handleCommand(final UniFiController controller, final UniFiDevice device,
final ChannelUID channelUID, final Command command) throws UniFiException {
final String channelID = channelUID.getIdWithoutGroup();
if (CHANNEL_AP_ENABLE.equals(channelID) && command instanceof OnOffType onOffCommand) {
return handleEnableCommand(controller, device, channelUID, onOffCommand);
}
return false;
}
private boolean handleEnableCommand(final UniFiController controller, final UniFiDevice device,
final ChannelUID channelUID, final OnOffType command) throws UniFiException {
controller.disableAccessPoint(device, command == OnOffType.OFF);
refresh();
return true;
}
}

View File

@ -12,6 +12,7 @@
*/
package org.openhab.binding.unifi.internal.handler;
import static org.openhab.binding.unifi.internal.UniFiBindingConstants.DEVICE_TYPE_UAP;
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;
@ -31,6 +32,7 @@ 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.UniFiDevice;
import org.openhab.binding.unifi.internal.api.dto.UniFiPortTuple;
import org.openhab.binding.unifi.internal.api.dto.UniFiSite;
import org.openhab.binding.unifi.internal.api.dto.UniFiSwitchPorts;
@ -83,6 +85,7 @@ public class UniFiThingDiscoveryService extends AbstractThingHandlerDiscoverySer
discoverWlans(cache, bridgeUID);
discoverClients(cache, bridgeUID);
discoverPoePorts(cache, bridgeUID);
discoverAccessPoints(cache, bridgeUID);
} catch (final UniFiException e) {
logger.debug("Exception during discovery of UniFi Things", e);
}
@ -128,6 +131,20 @@ public class UniFiThingDiscoveryService extends AbstractThingHandlerDiscoverySer
}
}
private void discoverAccessPoints(final UniFiControllerCache cache, final ThingUID bridgeUID) {
for (final UniFiDevice ud : cache.getDevices()) {
if (DEVICE_TYPE_UAP.equals(ud.getType())) {
final var thingTypeUID = UniFiBindingConstants.THING_TYPE_ACCESS_POINT;
final ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, stripIdShort(ud.getId()));
final Map<String, Object> properties = Map.of(PARAMETER_SITE, ud.getSite().getName(),
PARAMETER_MAC_ADDRESS, ud.getMac());
thingDiscovered(DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withBridge(bridgeUID).withRepresentationProperty(PARAMETER_MAC_ADDRESS).withTTL(TTL_SECONDS)
.withProperties(properties).withLabel(ud.getName()).build());
}
}
}
/**
* Shorten the id to make it a bit more comprehensible.
*

View File

@ -108,6 +108,17 @@
</parameter>
</config-description>
<config-description uri="thing-type:unifi:accessPoint">
<parameter name="macAddress" type="text" required="true">
<label>Access Point MAC Address</label>
<description>The MAC address of the access point</description>
</parameter>
<parameter name="site" type="text" required="false">
<label>Site</label>
<description>The site where the access point should be found (optional)</description>
</parameter>
</config-description>
<config-description uri="channel-type:unifi:poeEnable">
<parameter name="mode" type="text">
<label>On Mode</label>

View File

@ -5,6 +5,8 @@ addon.unifi.description = The UniFi binding integrates the UniFi controller from
# thing types
thing-type.unifi.accessPoint.label = UniFi Access Point
thing-type.unifi.accessPoint.description = An access point managed by a UniFi controller
thing-type.unifi.controller.label = UniFi Controller
thing-type.unifi.controller.description = A UniFi controller
thing-type.unifi.poePort.label = UniFi PoE Port
@ -20,6 +22,10 @@ thing-type.unifi.wlan.description = A UniFi Wireless LAN
# thing types config
thing-type.config.unifi.accessPoint.macAddress.label = Access Point MAC Address
thing-type.config.unifi.accessPoint.macAddress.description = The MAC address of the access point
thing-type.config.unifi.accessPoint.site.label = Site
thing-type.config.unifi.accessPoint.site.description = The site where the access point should be found (optional)
thing-type.config.unifi.client.cid.label = Client ID
thing-type.config.unifi.client.cid.description = The MAC address, IP address, hostname or name of the client
thing-type.config.unifi.client.considerHome.label = Consider Home Interval
@ -51,6 +57,8 @@ thing-type.config.unifi.wlan.wid.description = The id or name of the wlan
channel-type.unifi.ap.label = Access Point
channel-type.unifi.ap.description = Access Point the wireless client is connected to
channel-type.unifi.apEnable.label = Enabled
channel-type.unifi.apEnable.description = If the access point is enabled
channel-type.unifi.blocked.label = Blocked
channel-type.unifi.blocked.description = Is device blocked
channel-type.unifi.essid.label = Wireless Network
@ -150,19 +158,6 @@ channel-type.config.unifi.poeEnable.mode.description = The value to set when set
channel-type.config.unifi.poeEnable.mode.option.auto = Auto
channel-type.config.unifi.poeEnable.mode.option.pasv24 = 24V
channel-type.config.unifi.poeEnable.mode.option.passthrough = Passthrough
channel-type.config.unifi.guestVouchersGenerate.voucherCount.label = Number
channel-type.config.unifi.guestVouchersGenerate.voucherCount.description = Number of vouchers to create
channel-type.config.unifi.guestVouchersGenerate.voucherExpiration.label = Expiration Time
channel-type.config.unifi.guestVouchersGenerate.voucherExpiration.description = Minutes a voucher is valid after activation
channel-type.config.unifi.guestVouchersGenerate.voucherUsers.label = Users
channel-type.config.unifi.guestVouchersGenerate.voucherUsers.description = Number of users for voucher, 0 if no limit
channel-type.config.unifi.guestVouchersGenerate.voucherUpLimit.label = Upload Speed Limit
channel-type.config.unifi.guestVouchersGenerate.voucherUpLimit.description = Upload speed limit in kbps, no limit if not set
channel-type.config.unifi.guestVouchersGenerate.voucherDownLimit.label = Download Speed Limit
channel-type.config.unifi.guestVouchersGenerate.voucherDownLimit.description = Download speed limit in kbps, no limit if not set
channel-type.config.unifi.guestVouchersGenerate.voucherDataQuota.label = Data Transfer Quota
channel-type.config.unifi.guestVouchersGenerate.voucherDataQuota.description = Data transfer quota in MB per user, no limit if not set
channel-type.config.unifi.guestVouchersGenerate.option.GENERATE = Generate
# status messages
@ -177,6 +172,7 @@ error.thing.offline.configuration_error = You must choose a UniFi Controller for
error.thing.poe.offline.configuration_error = The configuration parameter macAddress must be set and not be empty.
error.thing.poe.offline.nodata_error = No data for the PoE port could be found in the UniFi API data. See TRACE log for actual API data.
error.thing.site.offline.configuration_error = The configuration parameter sid must be set and not be empty.
error.thing.ap.offline.configuration_error = The configuration parameter mac must be set and not be empty
# actions

View File

@ -141,6 +141,21 @@
<config-description-ref uri="thing-type:unifi:poePort"/>
</thing-type>
<thing-type id="accessPoint">
<supported-bridge-type-refs>
<bridge-type-ref id="controller"/>
</supported-bridge-type-refs>
<label>UniFi Access Point</label>
<description>An access point managed by a UniFi controller</description>
<channels>
<channel id="enable" typeId="apEnable"/>
</channels>
<config-description-ref uri="thing-type:unifi:accessPoint"/>
</thing-type>
<!-- Channels -->
<channel-type id="totalClients">
@ -421,4 +436,10 @@
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="apEnable" advanced="false">
<item-type>Switch</item-type>
<label>Enabled</label>
<description>If the access point is enabled</description>
</channel-type>
</thing:thing-descriptions>