[unifi] Guest wifi vouchers (#14284)

* guest voucher support

Signed-off-by: Mark Herwege <mark.herwege@telenet.be>
This commit is contained in:
Mark Herwege 2023-01-28 21:30:12 +01:00 committed by GitHub
parent aa7580965e
commit 16f3a3e854
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 440 additions and 18 deletions

View File

@ -114,11 +114,25 @@ The following table describes the `poePort` configuration parameters:
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 |
| guestVoucher | String | Guest voucher for access through the guest portal | Read |
| guestVouchersGenerate | String | Generate additional guest vouchers for access through the guest portal | Write |
The `guestVouchersGenerate` string channel is a command only channel that will trigger voucher creation.
It has configuration parameters to tailor the vouchers created:
| Parameter | Description | Config | Default |
| ------------------------ | --------------------------------------------------------------------------- |--------- | ------- |
| voucherCount | Number of vouchers to create | Optional | 1 |
| voucherExpiration | Minutes a voucher is valid after activation (default is 1 day) | Optional | 1440 |
| voucherUsers | Number of users for voucher, 0 for no limit | Optional | 1 |
| voucherUpLimit | Upload speed limit in kbps, no limit if not set | Optional | |
| voucherDownLimit | Download speed limit in kbps, no limit if not set | Optional | |
| voucherDataQuota | Data transfer quota in MB per user, no limit if not set | Optional | |
### `wlan`

View File

@ -24,6 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
* @author Matthew Bowman - Initial contribution
* @author Patrik Wimnell - Blocking / Unblocking client support
* @author Hilbrand Bouwkamp - Added poePort
* @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
public final class UniFiBindingConstants {
@ -47,6 +48,8 @@ public final class UniFiBindingConstants {
public static final String CHANNEL_WIRELESS_CLIENTS = "wirelessClients";
public static final String CHANNEL_WIRED_CLIENTS = "wiredClients";
public static final String CHANNEL_GUEST_CLIENTS = "guestClients";
public static final String CHANNEL_GUEST_VOUCHER = "guestVoucher";
public static final String CHANNEL_GUEST_VOUCHERS_GENERATE = "guestVouchersGenerate";
// List of wlan channels
public static final String CHANNEL_SECURITY = "security";
@ -98,6 +101,12 @@ public final class UniFiBindingConstants {
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_VOUCHER_COUNT = "voucherCount";
public static final String PARAMETER_VOUCHER_EXPIRATION = "voucherExpiration";
public static final String PARAMETER_VOUCHER_USERS = "voucherUsers";
public static final String PARAMETER_VOUCHER_UP_LIMIT = "voucherUpLimit";
public static final String PARAMETER_VOUCHER_DOWN_LIMIT = "voucherDownLimit";
public static final String PARAMETER_VOUCHER_DATA_QUOTA = "voucherDataQuota";
public static final String PARAMETER_PORT_NUMBER = "portNumber";
public static final String PARAMETER_MAC_ADDRESS = "macAddress";
public static final String PARAMETER_WIFI_NAME = "wifi";

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.unifi.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link UniFiVoucherChannelConfig} encapsulates all the configuration options for the guestVouchersGenerate
* channel on the UniFi Site thing.
*
* @author Mark Herwege - Initial contribution
*/
@NonNullByDefault
public class UniFiVoucherChannelConfig {
private int voucherCount;
private int voucherExpiration;
private int voucherUsers;
private @Nullable Integer voucherUpLimit;
private @Nullable Integer voucherDownLimit;
private @Nullable Integer voucherDataQuota;
public int getCount() {
return voucherCount;
}
public int getExpiration() {
return voucherExpiration;
}
public int getVoucherUsers() {
return voucherUsers;
}
public @Nullable Integer getUpLimit() {
return voucherUpLimit;
}
public @Nullable Integer getDownLimit() {
return voucherDownLimit;
}
public @Nullable Integer getDataQuota() {
return voucherDataQuota;
}
}

View File

@ -26,6 +26,7 @@ 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.UniFiSwitchPorts;
import org.openhab.binding.unifi.internal.api.dto.UniFiUnknownClient;
import org.openhab.binding.unifi.internal.api.dto.UniFiVoucher;
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;
@ -34,6 +35,7 @@ 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.UniFiVoucherInstanceCreator;
import org.openhab.binding.unifi.internal.api.util.UniFiWlanInstanceCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,6 +53,7 @@ import com.google.gson.JsonObject;
* @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
* @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
public class UniFiController {
@ -85,6 +88,7 @@ public class UniFiController {
final UniFiWlanInstanceCreator wlanInstanceCreator = new UniFiWlanInstanceCreator(cache);
final UniFiDeviceInstanceCreator deviceInstanceCreator = new UniFiDeviceInstanceCreator(cache);
final UniFiClientInstanceCreator clientInstanceCreator = new UniFiClientInstanceCreator(cache);
final UniFiVoucherInstanceCreator voucherInstanceCreator = new UniFiVoucherInstanceCreator(cache);
this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(UniFiSite.class, siteInstanceCreator)
.registerTypeAdapter(UniFiWlan.class, wlanInstanceCreator)
@ -92,7 +96,8 @@ public class UniFiController {
.registerTypeAdapter(UniFiClient.class, new UniFiClientDeserializer())
.registerTypeAdapter(UniFiUnknownClient.class, clientInstanceCreator)
.registerTypeAdapter(UniFiWiredClient.class, clientInstanceCreator)
.registerTypeAdapter(UniFiWirelessClient.class, clientInstanceCreator).create();
.registerTypeAdapter(UniFiWirelessClient.class, clientInstanceCreator)
.registerTypeAdapter(UniFiVoucher.class, voucherInstanceCreator).create();
this.poeGson = new GsonBuilder()
.registerTypeAdapter(UnfiPortOverrideJsonObject.class, new UnfiPortOverrideJsonElementDeserializer())
.create();
@ -146,6 +151,7 @@ public class UniFiController {
refreshDevices(sites);
refreshClients(sites);
refreshInsights(sites);
refreshVouchers(sites);
}
}
@ -209,6 +215,27 @@ public class UniFiController {
refresh();
}
public void generateGuestVouchers(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);
req.setAPIPath(String.format("/api/s/%s/cmd/hotspot", site.getName()));
req.setBodyParameter("cmd", "create-voucher");
req.setBodyParameter("expire", expiration);
req.setBodyParameter("n", count);
req.setBodyParameter("quota", users);
if (upLimit != null) {
req.setBodyParameter("up", upLimit);
}
if (downLimit != null) {
req.setBodyParameter("down", downLimit);
}
if (dataQuota != null) {
req.setBodyParameter("bytes", dataQuota);
}
executeRequest(req);
refresh();
}
// Internal API
private <T> UniFiControllerRequest<T> newRequest(final Class<T> responseType, final HttpMethod method,
@ -284,6 +311,18 @@ public class UniFiController {
return executeRequest(req);
}
private void refreshVouchers(final Collection<UniFiSite> sites) throws UniFiException {
for (final UniFiSite site : sites) {
cache.putVouchers(getVouchers(site));
}
}
private UniFiVoucher @Nullable [] getVouchers(final UniFiSite site) throws UniFiException {
final UniFiControllerRequest<UniFiVoucher[]> req = newRequest(UniFiVoucher[].class, HttpMethod.GET, gson);
req.setAPIPath(String.format("/api/s/%s/stat/voucher", site.getName()));
return executeRequest(req);
}
private void refreshInsights(final Collection<UniFiSite> sites) throws UniFiException {
for (final UniFiSite site : sites) {
cache.putInsights(getInsights(site));

View File

@ -13,6 +13,7 @@
package org.openhab.binding.unifi.internal.api.cache;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -27,6 +28,7 @@ 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;
import org.openhab.binding.unifi.internal.api.dto.UniFiVoucher;
import org.openhab.binding.unifi.internal.api.dto.UniFiWlan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -36,6 +38,7 @@ import org.slf4j.LoggerFactory;
*
* @author Matthew Bowman - Initial contribution
* @author Hilbrand Bouwkamp - Moved cache to this dedicated class.
* @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
public class UniFiControllerCache {
@ -47,6 +50,7 @@ public class UniFiControllerCache {
private final UniFiDeviceCache devicesCache = new UniFiDeviceCache();
private final UniFiClientCache clientsCache = new UniFiClientCache();
private final UniFiClientCache insightsCache = new UniFiClientCache();
private final UniFiVoucherCache vouchersCache = new UniFiVoucherCache();
private final Map<String, UniFiSwitchPorts> devicesToPortTables = new ConcurrentHashMap<>();
public void clear() {
@ -55,6 +59,7 @@ public class UniFiControllerCache {
devicesCache.clear();
clientsCache.clear();
insightsCache.clear();
vouchersCache.clear();
}
// Sites Cache
@ -170,4 +175,19 @@ public class UniFiControllerCache {
public void putInsights(final UniFiClient @Nullable [] insights) {
insightsCache.putAll(insights);
}
// Vouchers Cache
public void putVouchers(final UniFiVoucher @Nullable [] vouchers) {
vouchersCache.putAll(vouchers);
}
public synchronized Stream<UniFiVoucher> getVoucherStreamForSite(final UniFiSite site) {
return vouchersCache.values().stream().filter(voucher -> voucher.getSite().equals(site));
}
public @Nullable UniFiVoucher getVoucher(final UniFiSite site) {
// Use one of the oldest vouchers first
return getVoucherStreamForSite(site).min(Comparator.comparing(UniFiVoucher::getCreateTime)).orElse(null);
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2023 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.unifi.internal.api.dto.UniFiVoucher;
/**
* The {@link UniFiVoucherCache} is a specific implementation of {@link UniFiCache} for the purpose of caching
* {@link UniFiVoucher} instances.
*
* The cache uses the following prefixes: <code>id</code>
*
* @author Mark Herwege - Initial contribution
*/
@NonNullByDefault
class UniFiVoucherCache extends UniFiCache<UniFiVoucher> {
public UniFiVoucherCache() {
super(ID);
}
@Override
protected @Nullable String getSuffix(final UniFiVoucher voucher, final Prefix prefix) {
switch (prefix) {
case ID:
return voucher.getId();
default:
return null;
}
}
}

View File

@ -20,6 +20,7 @@ import com.google.gson.annotations.SerializedName;
* The {@link UniFiSite} represents the data model of a UniFi site.
*
* @author Matthew Bowman - Initial contribution
* @author Mark Herwege - Added guest vouchers
*/
public class UniFiSite implements HasId {
@ -53,6 +54,14 @@ public class UniFiSite implements HasId {
return cache;
}
public String getVoucher() {
UniFiVoucher voucher = cache.getVoucher(this);
if (voucher == null) {
return null;
}
return voucher.getCode();
}
public boolean isSite(final UniFiSite site) {
return site != null && id.equals(site.getId());
}

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2023 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.UniFiTimestampDeserializer;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
/**
* The {@link UniFiVoucher} is the base data model for a guest network voucher
*
* @author Mark Herwege - Initial contribution
*/
public class UniFiVoucher implements HasId {
private final transient UniFiControllerCache cache;
@SerializedName("_id")
private String id;
private String siteId;
private String code;
@JsonAdapter(UniFiTimestampDeserializer.class)
private Instant createTime;
private Integer duration;
private Integer quota;
private Integer used;
private boolean qosOverwrite;
private Integer qosUsageQuota;
private String status;
public UniFiVoucher(final UniFiControllerCache cache) {
this.cache = cache;
}
@Override
public String getId() {
return id;
}
public String getCode() {
return code;
}
public Instant getCreateTime() {
return createTime;
}
public Integer getDuration() {
return duration;
}
public Integer getQuota() {
return quota;
}
public Integer getUsed() {
return used;
}
public boolean isQosOverwrite() {
return qosOverwrite;
}
public Integer getQosUsageQuota() {
return qosUsageQuota;
}
public String getStatus() {
return status;
}
public UniFiSite getSite() {
return cache.getSite(siteId);
}
@Override
public String toString() {
return String.format(
"UniFiVoucher{id: '%s', code: '%s', created: '%s', duration: '%s', quota: '%s', used: '%s', qosUsageQuota: '%s', status: '%s', site: %s}",
id, code, createTime, duration, quota, used, qosUsageQuota, status, getSite());
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2023 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.UniFiVoucher;
import com.google.gson.InstanceCreator;
import com.google.gson.JsonSyntaxException;
/**
* The {@link UniFiVoucherInstanceCreator} creates instances of {@link UniFiVoucher}s during the JSON unmarshalling of
* controller responses.
*
* @author Mark Herwege - Initial contribution
*/
@NonNullByDefault
public class UniFiVoucherInstanceCreator implements InstanceCreator<UniFiVoucher> {
private final UniFiControllerCache cache;
public UniFiVoucherInstanceCreator(final UniFiControllerCache cache) {
this.cache = cache;
}
@Override
public UniFiVoucher createInstance(final @Nullable Type type) {
if (UniFiVoucher.class.equals(type)) {
return new UniFiVoucher(cache);
} else {
throw new JsonSyntaxException("Expected a UniFi Voucher type, but got " + type);
}
}
}

View File

@ -12,19 +12,23 @@
*/
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 static org.openhab.binding.unifi.internal.UniFiBindingConstants.*;
import java.util.function.Predicate;
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.UniFiVoucherChannelConfig;
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.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -39,6 +43,7 @@ import org.openhab.core.types.UnDefType;
*
* @author Matthew Bowman - Initial contribution
* @author Hilbrand Bouwkamp - Initial contribution
* @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
public class UniFiSiteThingHandler extends UniFiBaseThingHandler<UniFiSite, UniFiSiteThingConfig> {
@ -67,32 +72,59 @@ public class UniFiSiteThingHandler extends UniFiBaseThingHandler<UniFiSite, UniF
@Override
protected State getChannelState(final UniFiSite site, final String channelId) {
final UniFiControllerCache cache = site.getCache();
final long count;
final State state;
switch (channelId) {
case CHANNEL_TOTAL_CLIENTS:
count = cache.countClients(site, c -> true);
state = countClients(site, c -> true);
break;
case CHANNEL_WIRELESS_CLIENTS:
count = cache.countClients(site, c -> c.isWireless());
state = countClients(site, c -> c.isWireless());
break;
case CHANNEL_WIRED_CLIENTS:
count = cache.countClients(site, c -> c.isWired());
state = countClients(site, c -> c.isWired());
break;
case CHANNEL_GUEST_CLIENTS:
count = cache.countClients(site, c -> c.isGuest());
state = countClients(site, c -> c.isGuest());
break;
case CHANNEL_GUEST_VOUCHER:
String voucher = site.getVoucher();
state = (voucher != null) ? StringType.valueOf(voucher) : UnDefType.UNDEF;
break;
case CHANNEL_GUEST_VOUCHERS_GENERATE:
state = OnOffType.OFF;
break;
default:
// Unsupported channel; nothing to update
return UnDefType.NULL;
}
return new DecimalType(count);
return state;
}
private static State countClients(final UniFiSite site, final Predicate<UniFiClient> filter) {
return new DecimalType(site.getCache().countClients(site, filter));
}
@Override
protected boolean handleCommand(final UniFiController controller, final UniFiSite entity,
final ChannelUID channelUID, final Command command) throws UniFiException {
final String channelID = channelUID.getId();
if (CHANNEL_GUEST_VOUCHERS_GENERATE.equals(channelID)) {
Channel channel = getThing().getChannel(CHANNEL_GUEST_VOUCHERS_GENERATE);
if (channel == null) {
return false;
}
UniFiVoucherChannelConfig config = channel.getConfiguration().as(UniFiVoucherChannelConfig.class);
final int count = config.getCount();
final int expire = config.getExpiration();
final int users = config.getVoucherUsers();
final Integer upLimit = config.getUpLimit();
final Integer downLimit = config.getDownLimit();
final Integer dataQuota = config.getDataQuota();
controller.generateGuestVouchers(entity, count, expire, users, upLimit, downLimit, dataQuota);
return true;
}
return false;
}
}

View File

@ -67,6 +67,36 @@
</parameter>
</config-description>
<config-description uri="channel-type:unifi:guestVouchersGenerate">
<parameter name="voucherCount" type="integer">
<label>Number</label>
<description>Number of vouchers to create</description>
<default>1</default>
</parameter>
<parameter name="voucherExpiration" type="integer" unit="min">
<label>Expiration Time</label>
<description>Minutes a voucher is valid after activation</description>
<default>1440</default>
</parameter>
<parameter name="voucherUsers" type="integer">
<label>Users</label>
<description>Number of users for voucher, 0 if no limit</description>
<default>1</default>
</parameter>
<parameter name="voucherUpLimit" type="integer">
<label>Upload Speed Limit</label>
<description>Upload speed limit in kbps, no limit if not set</description>
</parameter>
<parameter name="voucherDownLimit" type="integer">
<label>Download Speed Limit</label>
<description>Download speed limit in kbps, no limit if not set</description>
</parameter>
<parameter name="voucherDataQuota" type="integer">
<label>Data Transfer Quota</label>
<description>Data transfer quota in MB per user, no limit if not set</description>
</parameter>
</config-description>
<config-description uri="thing-type:unifi:poePort">
<parameter name="portNumber" type="integer" required="true">
<label>Port Number</label>

View File

@ -25,6 +25,8 @@
<channel id="wirelessClients" typeId="wirelessClients"/>
<channel id="wiredClients" typeId="wiredClients"/>
<channel id="guestClients" typeId="guestClients"/>
<channel id="guestVoucher" typeId="guestVoucher"/>
<channel id="guestVouchersGenerate" typeId="guestVouchersGenerate"/>
</channels>
<representation-property>sid</representation-property>
@ -165,6 +167,25 @@
<state readOnly="true"></state>
</channel-type>
<channel-type id="guestVoucher">
<item-type>String</item-type>
<label>Guest Voucher</label>
<description>Guest voucher for access through the guest portal</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="guestVouchersGenerate">
<item-type>String</item-type>
<label>Generate Guest Vouchers</label>
<description>Generate additional guest vouchers for access through the guest portal</description>
<command>
<options>
<option value="GENERATE">Generate</option>
</options>
</command>
<config-description-ref uri="channel-type:unifi:guestVouchersGenerate"/>
</channel-type>
<channel-type id="wlanEnable">
<item-type>Switch</item-type>
<label>Enable</label>