[Mikrotik] Add support for RouterOS 7 devices and support internal radios with capsman disabled (#17547)

* Add support for API version 7 and AX wifi6 range of devices.

Signed-off-by: Matthew Skinner <matt@pcmus.com>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Matthew Skinner 2024-10-19 00:02:50 +11:00 committed by Ciprian Pascu
parent 756ad0232a
commit 6f283a47d4
8 changed files with 164 additions and 16 deletions

View File

@ -12,8 +12,7 @@
*/ */
package org.openhab.binding.mikrotik.internal.handler; package org.openhab.binding.mikrotik.internal.handler;
import static org.openhab.core.thing.ThingStatus.OFFLINE; import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR; import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
import static org.openhab.core.types.RefreshType.REFRESH; import static org.openhab.core.types.RefreshType.REFRESH;
@ -63,8 +62,6 @@ public abstract class MikrotikBaseThingHandler<C extends ConfigValidation> exten
protected ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false); protected ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
protected Map<String, State> currentState = new HashMap<>(); protected Map<String, State> currentState = new HashMap<>();
// public static boolean supportsThingType(ThingTypeUID thingTypeUID) <- in subclasses
public MikrotikBaseThingHandler(Thing thing) { public MikrotikBaseThingHandler(Thing thing) {
super(thing); super(thing);
} }

View File

@ -12,8 +12,7 @@
*/ */
package org.openhab.binding.mikrotik.internal.handler; package org.openhab.binding.mikrotik.internal.handler;
import static org.openhab.core.thing.ThingStatus.OFFLINE; import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.thing.ThingStatusDetail.GONE; import static org.openhab.core.thing.ThingStatusDetail.GONE;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -31,6 +30,7 @@ import org.openhab.binding.mikrotik.internal.model.RouterosL2TPSrvInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosLTEInterface; import org.openhab.binding.mikrotik.internal.model.RouterosLTEInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosPPPCliInterface; import org.openhab.binding.mikrotik.internal.model.RouterosPPPCliInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosPPPoECliInterface; import org.openhab.binding.mikrotik.internal.model.RouterosPPPoECliInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosWifiInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosWlanInterface; import org.openhab.binding.mikrotik.internal.model.RouterosWlanInterface;
import org.openhab.binding.mikrotik.internal.util.RateCalculator; import org.openhab.binding.mikrotik.internal.util.RateCalculator;
import org.openhab.binding.mikrotik.internal.util.StateUtil; import org.openhab.binding.mikrotik.internal.util.StateUtil;
@ -184,6 +184,8 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
newState = getEtherIterfaceChannelState(channelID); newState = getEtherIterfaceChannelState(channelID);
} else if (iface instanceof RouterosCapInterface) { } else if (iface instanceof RouterosCapInterface) {
newState = getCapIterfaceChannelState(channelID); newState = getCapIterfaceChannelState(channelID);
} else if (iface instanceof RouterosWifiInterface) {
newState = getWifiIterfaceChannelState(channelID);
} else if (iface instanceof RouterosWlanInterface) { } else if (iface instanceof RouterosWlanInterface) {
newState = getWlanIterfaceChannelState(channelID); newState = getWlanIterfaceChannelState(channelID);
} else if (iface instanceof RouterosPPPCliInterface) { } else if (iface instanceof RouterosPPPCliInterface) {
@ -264,6 +266,26 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
} }
} }
protected State getWifiIterfaceChannelState(String channelID) {
RouterosWifiInterface wlIface = (RouterosWifiInterface) this.iface;
if (wlIface == null) {
return UnDefType.UNDEF;
}
switch (channelID) {
case MikrotikBindingConstants.CHANNEL_STATE:
return StateUtil.stringOrNull(wlIface.getCurrentState());
case MikrotikBindingConstants.CHANNEL_RATE:
return StateUtil.stringOrNull(wlIface.getRate());
case MikrotikBindingConstants.CHANNEL_REGISTERED_CLIENTS:
return StateUtil.intOrNull(wlIface.getRegisteredClients());
case MikrotikBindingConstants.CHANNEL_AUTHORIZED_CLIENTS:
return StateUtil.intOrNull(wlIface.getAuthorizedClients());
default:
return UnDefType.UNDEF;
}
}
protected State getPPPoECliChannelState(String channelID) { protected State getPPPoECliChannelState(String channelID) {
RouterosPPPoECliInterface pppCli = (RouterosPPPoECliInterface) this.iface; RouterosPPPoECliInterface pppCli = (RouterosPPPoECliInterface) this.iface;
if (pppCli == null) { if (pppCli == null) {

View File

@ -281,6 +281,8 @@ public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
case MikrotikBindingConstants.CHANNEL_CPU_LOAD: case MikrotikBindingConstants.CHANNEL_CPU_LOAD:
newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad()); newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad());
break; break;
default:
logger.warn("Unimplemented channel:{}", channelID);
} }
} }

View File

@ -27,6 +27,7 @@ import org.openhab.binding.mikrotik.internal.model.RouterosRegistrationBase;
import org.openhab.binding.mikrotik.internal.model.RouterosWirelessRegistration; import org.openhab.binding.mikrotik.internal.model.RouterosWirelessRegistration;
import org.openhab.binding.mikrotik.internal.util.RateCalculator; import org.openhab.binding.mikrotik.internal.util.RateCalculator;
import org.openhab.binding.mikrotik.internal.util.StateUtil; import org.openhab.binding.mikrotik.internal.util.StateUtil;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -214,11 +215,10 @@ public class MikrotikWirelessClientThingHandler extends MikrotikBaseThingHandler
if (this.wifiReg == null) { if (this.wifiReg == null) {
return UnDefType.UNDEF; return UnDefType.UNDEF;
} }
RouterosWirelessRegistration wirelessReg = (RouterosWirelessRegistration) this.wifiReg; RouterosWirelessRegistration wirelessReg = (RouterosWirelessRegistration) this.wifiReg;
switch (channelID) { switch (channelID) {
case CHANNEL_SIGNAL: case CHANNEL_SIGNAL:
return StateUtil.intOrNull(wirelessReg.getRxSignal()); return new DecimalType(wirelessReg.getRxSignal());
default: default:
return UnDefType.UNDEF; return UnDefType.UNDEF;
} }

View File

@ -52,12 +52,13 @@ public class RouterosDevice {
public static final String PROP_TYPE_KEY = "type"; public static final String PROP_TYPE_KEY = "type";
public static final String PROP_NAME_KEY = "name"; public static final String PROP_NAME_KEY = "name";
public static final String PROP_SSID_KEY = "ssid"; public static final String PROP_SSID_KEY = "ssid";
public static final String PROP_CONFIG_SSID_KEY = "configuration.ssid";
private static final String CMD_PRINT_IFACES = "/interface/print"; private static final String CMD_PRINT_IFACES = "/interface/print";
private static final String CMD_PRINT_IFACE_TYPE_TPL = "/interface/%s/print"; private static final String CMD_PRINT_IFACE_TYPE_TPL = "/interface/%s/print";
private static final String CMD_MONTOR_IFACE_MONITOR_TPL = "/interface/%s/monitor numbers=%s once"; private static final String CMD_MONTOR_IFACE_MONITOR_TPL = "/interface/%s/monitor numbers=%s once";
private static final String CMD_PRINT_CAPS_IFACES = "/caps-man/interface/print"; private static final String CMD_PRINT_CAPS_IFACES = "/caps-man/interface/print";
private static final String CMD_PRINT_CAPSMAN_REGS = "/caps-man/registration-table/print"; private static final String CMD_PRINT_CAPSMAN_REGS = "/caps-man/registration-table/print";
private static final String CMD_PRINT_WIFI_REGS = "/interface/wifi/registration-table/print";
private static final String CMD_PRINT_WIRELESS_REGS = "/interface/wireless/registration-table/print"; private static final String CMD_PRINT_WIRELESS_REGS = "/interface/wireless/registration-table/print";
private static final String CMD_PRINT_RESOURCE = "/system/resource/print"; private static final String CMD_PRINT_RESOURCE = "/system/resource/print";
private static final String CMD_PRINT_RB_INFO = "/system/routerboard/print"; private static final String CMD_PRINT_RB_INFO = "/system/routerboard/print";
@ -85,6 +86,8 @@ public class RouterosDevice {
return Optional.of(new RouterosCapInterface(interfaceProps)); return Optional.of(new RouterosCapInterface(interfaceProps));
case WLAN: case WLAN:
return Optional.of(new RouterosWlanInterface(interfaceProps)); return Optional.of(new RouterosWlanInterface(interfaceProps));
case WIFI:
return Optional.of(new RouterosWifiInterface(interfaceProps));
case PPPOE_CLIENT: case PPPOE_CLIENT:
return Optional.of(new RouterosPPPoECliInterface(interfaceProps)); return Optional.of(new RouterosPPPoECliInterface(interfaceProps));
case PPP_CLIENT: case PPP_CLIENT:
@ -116,6 +119,9 @@ public class RouterosDevice {
public void start() throws MikrotikApiException { public void start() throws MikrotikApiException {
login(); login();
updateRouterboardInfo(); updateRouterboardInfo();
if (rbInfo != null) {
logger.debug("RouterOS Version = {}", rbInfo.getFirmwareVersion());
}
} }
public void stop() { public void stop() {
@ -160,8 +166,25 @@ public class RouterosDevice {
synchronized (this) { synchronized (this) {
updateResources(); updateResources();
updateInterfaceData(); updateInterfaceData();
updateCapsmanRegistrations(); try {
updateWirelessRegistrations(); updateCapsmanRegistrations();
} catch (MikrotikApiException e) {
logger.debug(
"MikrotikApiException: Device may have the CAPsMAN feature for wireless management disabled.");
}
wirelessRegistrationCache.clear();
try {
updateWirelessRegistrations();
} catch (MikrotikApiException e) {
logger.debug(
"MikrotikApiException: Device does not appear to have any built in CAPsMAN wireless devices.");
}
try {
updateWifiRegistrations();
} catch (MikrotikApiException e) {
logger.debug("MikrotikApiException: Device does not appear to have any built in wifi.");
}
logger.trace("There are {} wirelessRegistration's registered in cache.", wirelessRegistrationCache.size());
} }
} }
@ -205,6 +228,7 @@ public class RouterosDevice {
this.wlanSsid.clear(); this.wlanSsid.clear();
this.interfaceCache.clear(); this.interfaceCache.clear();
ifaceResponse.forEach(props -> { ifaceResponse.forEach(props -> {
logger.trace("Interface Details:{}", props.toString());
Optional<RouterosInterfaceBase> ifaceOpt = createTypedInterface(props); Optional<RouterosInterfaceBase> ifaceOpt = createTypedInterface(props);
if (ifaceOpt.isPresent()) { if (ifaceOpt.isPresent()) {
RouterosInterfaceBase iface = ifaceOpt.get(); RouterosInterfaceBase iface = ifaceOpt.get();
@ -215,12 +239,14 @@ public class RouterosDevice {
} }
}); });
// Checks if any new interfaces have been setup since last check
Map<String, Map<String, String>> typedIfaceResponse = new HashMap<>(); Map<String, Map<String, String>> typedIfaceResponse = new HashMap<>();
for (String ifaceApiType : interfaceTypesToPoll) { for (String ifaceApiType : interfaceTypesToPoll) {
String cmd = String.format(CMD_PRINT_IFACE_TYPE_TPL, ifaceApiType); String cmd = String.format(CMD_PRINT_IFACE_TYPE_TPL, ifaceApiType);
if (ifaceApiType.compareTo("cap") == 0) { if (ifaceApiType.compareTo("cap") == 0) {
cmd = CMD_PRINT_CAPS_IFACES; cmd = CMD_PRINT_CAPS_IFACES;
} }
logger.debug("Command used for updating the interfaces is:{}", cmd);
connection.execute(cmd).forEach(propMap -> { connection.execute(cmd).forEach(propMap -> {
String ifaceName = propMap.get(PROP_NAME_KEY); String ifaceName = propMap.get(PROP_NAME_KEY);
if (ifaceName != null) { if (ifaceName != null) {
@ -235,24 +261,36 @@ public class RouterosDevice {
for (RouterosInterfaceBase ifaceModel : interfaceCache) { for (RouterosInterfaceBase ifaceModel : interfaceCache) {
// Enrich with detailed data // Enrich with detailed data
Map<String, String> additionalIfaceProps = typedIfaceResponse.get(ifaceModel.getName()); Map<String, String> additionalIfaceProps = typedIfaceResponse.get(ifaceModel.getName());
if (additionalIfaceProps != null) { if (additionalIfaceProps != null) {
ifaceModel.mergeProps(additionalIfaceProps); ifaceModel.mergeProps(additionalIfaceProps);
} }
// Get monitor data // Get monitor data, runs if you have added an interface thing.
if (ifaceModel.hasMonitor() && monitoredInterfaces.contains(ifaceModel.getName())) { if (ifaceModel.hasMonitor() && monitoredInterfaces.contains(ifaceModel.getName())) {
String cmd = String.format(CMD_MONTOR_IFACE_MONITOR_TPL, ifaceModel.getApiType(), ifaceModel.getName()); String cmd = String.format(CMD_MONTOR_IFACE_MONITOR_TPL, ifaceModel.getApiType(), ifaceModel.getName());
if (logger.isDebugEnabled()) {
logger.debug("Getting detailed data for Interface:{}, with command:{}", ifaceModel.getName(), cmd);
}
List<Map<String, String>> monitorProps = connection.execute(cmd); List<Map<String, String>> monitorProps = connection.execute(cmd);
ifaceModel.mergeProps(monitorProps.get(0)); ifaceModel.mergeProps(monitorProps.get(0));
} }
// Note SSIDs for non-CAPsMAN wireless clients // Note SSIDs for non-CAPsMAN wireless clients
String ifaceName = ifaceModel.getName(); String ifaceName = ifaceModel.getName();
String ifaceSsid = ifaceModel.getProperty(PROP_SSID_KEY); String ifaceSsid;
switch (ifaceModel.getApiType()) {
case "wifi":
ifaceSsid = ifaceModel.getProperty(PROP_CONFIG_SSID_KEY);
break;
case "caps":
case "wireless":
default:
ifaceSsid = ifaceModel.getProperty(PROP_SSID_KEY);
}
if (ifaceName != null && ifaceSsid != null && !ifaceName.isBlank() && !ifaceSsid.isBlank()) { if (ifaceName != null && ifaceSsid != null && !ifaceName.isBlank() && !ifaceSsid.isBlank()) {
this.wlanSsid.put(ifaceName, ifaceSsid); this.wlanSsid.put(ifaceName, ifaceSsid);
} }
} }
logger.debug("Found the following SSID's:{}", wlanSsid.toString());
} }
private void updateCapsmanRegistrations() throws MikrotikApiException { private void updateCapsmanRegistrations() throws MikrotikApiException {
@ -273,7 +311,6 @@ public class RouterosDevice {
return; return;
} }
List<Map<String, String>> response = conn.execute(CMD_PRINT_WIRELESS_REGS); List<Map<String, String>> response = conn.execute(CMD_PRINT_WIRELESS_REGS);
wirelessRegistrationCache.clear();
response.forEach(props -> { response.forEach(props -> {
String wlanIfaceName = props.get("interface"); String wlanIfaceName = props.get("interface");
String wlanSsidName = wlanSsid.get(wlanIfaceName); String wlanSsidName = wlanSsid.get(wlanIfaceName);
@ -285,6 +322,23 @@ public class RouterosDevice {
}); });
} }
private void updateWifiRegistrations() throws MikrotikApiException {
ApiConnection conn = this.connection;
if (conn == null) {
return;
}
List<Map<String, String>> response = conn.execute(CMD_PRINT_WIFI_REGS);
response.forEach(props -> {
String wlanIfaceName = props.get("interface");
String wlanSsidName = wlanSsid.get(wlanIfaceName);
if (wlanSsidName != null && wlanIfaceName != null && !wlanIfaceName.isBlank() && !wlanSsidName.isBlank()) {
props.put("configuration.ssid", wlanSsidName);
}
wirelessRegistrationCache.add(new RouterosWirelessRegistration(props));
});
}
private void updateResources() throws MikrotikApiException { private void updateResources() throws MikrotikApiException {
ApiConnection conn = this.connection; ApiConnection conn = this.connection;
if (conn == null) { if (conn == null) {

View File

@ -26,9 +26,12 @@ public enum RouterosInterfaceType {
ETHERNET("ether"), ETHERNET("ether"),
BRIDGE("bridge"), BRIDGE("bridge"),
WLAN("wlan"), WLAN("wlan"),
WIFI("wifi"),
CAP("cap"), CAP("cap"),
PPP_CLIENT("ppp-out"), PPP_CLIENT("ppp-out"),
PPPOE_CLIENT("pppoe-out"), PPPOE_CLIENT("pppoe-out"),
VLAN("vlan"),
VETH("veth"), // docker containers create virtual ether ports
L2TP_SERVER("l2tp-in"), L2TP_SERVER("l2tp-in"),
L2TP_CLIENT("l2tp-out"), L2TP_CLIENT("l2tp-out"),
LTE("lte"); LTE("lte");

View File

@ -0,0 +1,53 @@
/**
* 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.mikrotik.internal.model;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RouterosWifiInterface} is a model class for `wifi` interface models having casting accessors for
* data that is specific to this network interface kind. Is a subclass of {@link RouterosInterfaceBase}.
*
* @author Matthew Skinner - Initial contribution
*/
@NonNullByDefault
public class RouterosWifiInterface extends RouterosWlanInterface {
public RouterosWifiInterface(Map<String, String> props) {
super(props);
}
@Override
public RouterosInterfaceType getDesignedType() {
return RouterosInterfaceType.WIFI;
}
@Override
public String getApiType() {
return "wifi";
}
@Override
public int getRegisteredClients() {
Integer registeredClients = getIntProp("registered-peers");
return registeredClients == null ? 0 : registeredClients;
}
@Override
public int getAuthorizedClients() {
Integer authedClients = getIntProp("authorized-peers");
return authedClients == null ? 0 : authedClients;
}
}

View File

@ -27,12 +27,29 @@ import org.openhab.binding.mikrotik.internal.util.Converter;
*/ */
@NonNullByDefault @NonNullByDefault
public class RouterosWirelessRegistration extends RouterosRegistrationBase { public class RouterosWirelessRegistration extends RouterosRegistrationBase {
public RouterosWirelessRegistration(Map<String, String> props) { public RouterosWirelessRegistration(Map<String, String> props) {
super(props); super(props);
} }
public int getRxSignal() { public int getRxSignal() {
String signalValue = getProp("signal-strength", "0@hz").split("@")[0]; String signalValue = getProp("signal", "0");
if (signalValue.isEmpty()) {
signalValue = getProp("signal-strength", "0@hz").split("@")[0];
} else {
int value = Integer.parseInt(signalValue);
if (value < -80) {
return 0;
} else if (value < -75) {
return 1;
} else if (value < -68) {
return 2;
} else if (value < -55) {
return 3;
} else {
return 4;
}
}
return Integer.parseInt(signalValue); return Integer.parseInt(signalValue);
} }