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 |
+| 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`
* @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 {
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";
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";
--- /dev/null
+/**
+ * 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;
+ }
+}
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;
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;
* @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 {
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)
.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();
refreshDevices(sites);
refreshClients(sites);
refreshInsights(sites);
+ refreshVouchers(sites);
}
}
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,
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));
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;
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;
*
* @author Matthew Bowman - Initial contribution
* @author Hilbrand Bouwkamp - Moved cache to this dedicated class.
+ * @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
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() {
devicesCache.clear();
clientsCache.clear();
insightsCache.clear();
+ vouchersCache.clear();
}
// Sites Cache
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);
+ }
}
--- /dev/null
+/**
+ * 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;
+ }
+ }
+}
* 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 {
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());
}
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
*/
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;
*
* @author Matthew Bowman - Initial contribution
* @author Hilbrand Bouwkamp - Initial contribution
+ * @author Mark Herwege - Added guest vouchers
*/
@NonNullByDefault
public class UniFiSiteThingHandler extends UniFiBaseThingHandler<UniFiSite, UniFiSiteThingConfig> {
@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;
}
}
</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>
<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>
<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>