[mikrotik] More interface support and ability to disable interfaces. (#20661)

* New features

Signed-off-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Matthew Skinner
2026-05-11 22:39:37 +02:00
committed by GitHub
parent 156dbd929d
commit 792343ff15
12 changed files with 321 additions and 32 deletions
+14 -10
View File
@@ -43,16 +43,17 @@ The RouterOS Bridge configuration parameters are:
### Bridge Channels
| Channel | Type | Description | Comment |
|-------------|----------------------|------------------------------------------------------|---------|
| freeSpace | Number:DataAmount | Amount of free storage left on device in bytes | |
| totalSpace | Number:DataAmount | Amount of total storage available on device in bytes | |
| usedSpace | Number:Dimensionless | Percentage of used device storage space | |
| freeMemory | Number:DataAmount | Amount of free memory left on device in bytes | |
| totalMemory | Number:DataAmount | Amount of total memory available on device in bytes | |
| usedMemory | Number:Dimensionless | Percentage of used device memory | |
| cpuLoad | Number:Dimensionless | CPU load percentage | |
| upSince | DateTime | Time when Thing got up | |
| Channel | Type | Description | Comment |
|-----------------|----------------------|--------------------------------------------------------------------------|---------|
| freeSpace | Number:DataAmount | Amount of free storage left on device in bytes | |
| totalSpace | Number:DataAmount | Amount of total storage available on device in bytes | |
| usedSpace | Number:Dimensionless | Percentage of used device storage space | |
| freeMemory | Number:DataAmount | Amount of free memory left on device in bytes | |
| totalMemory | Number:DataAmount | Amount of total memory available on device in bytes | |
| usedMemory | Number:Dimensionless | Percentage of used device memory | |
| cpuLoad | Number:Dimensionless | CPU load percentage | |
| upSince | DateTime | Time when Thing got up | |
| updateAvailable | String | Changes from "System is already up to date" to "New version is available"| |
## WiFi Client Thing Configuration
@@ -100,6 +101,9 @@ At the moment the binding supports the following RouterOS interface types:
- `ether`
- `bridge`
- `wlan`
- `wifi`
- `vlan`
- `veth`
- `cap`
- `pppoe-out`
- `ppp-out`
@@ -43,6 +43,7 @@ public class MikrotikBindingConstants {
public static final String CHANNEL_TOTAL_MEM = "totalMemory";
public static final String CHANNEL_USED_MEM = "usedMemory";
public static final String CHANNEL_CPU_LOAD = "cpuLoad";
public static final String CHANNEL_UPDATE_AVAILABLE = "updateAvailable";
public static final String CHANNEL_COMMENT = "comment";
@@ -205,7 +205,8 @@ public abstract class MikrotikBaseThingHandler<C extends ConfigValidation> exten
try {
refreshChannel(channel.getUID());
} catch (RuntimeException e) {
logger.warn("Unhandled exception while refreshing the {} Mikrotik thing", getThing().getUID(), e);
logger.warn("Unhandled exception while refreshing the channel {} of {} Mikrotik thing:{}",
channel.getUID(), getThing().getUID(), e.getMessage());
updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.mikrotik.internal.handler;
import static org.openhab.binding.mikrotik.internal.MikrotikBindingConstants.CHANNEL_ENABLED;
import static org.openhab.core.thing.ThingStatus.*;
import static org.openhab.core.thing.ThingStatusDetail.GONE;
@@ -30,10 +31,13 @@ import org.openhab.binding.mikrotik.internal.model.RouterosL2TPSrvInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosLTEInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosPPPCliInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosPPPoECliInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosVethInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosVlanInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosWifiInterface;
import org.openhab.binding.mikrotik.internal.model.RouterosWlanInterface;
import org.openhab.binding.mikrotik.internal.util.RateCalculator;
import org.openhab.binding.mikrotik.internal.util.StateUtil;
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;
@@ -202,6 +206,10 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
newState = getL2TPCliChannelState(channelID);
} else if (iface instanceof RouterosLTEInterface) {
newState = getLTEChannelState(channelID);
} else if (iface instanceof RouterosVlanInterface) {
newState = getEtherIterfaceChannelState(channelID);
} else if (iface instanceof RouterosVethInterface) {
newState = getVethIterfaceChannelState(channelID);
}
}
}
@@ -212,6 +220,24 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
}
}
protected State getVethIterfaceChannelState(String channelID) {
RouterosEthernetInterface etherIface = (RouterosEthernetInterface) this.iface;
if (etherIface == null) {
return UnDefType.UNDEF;
}
switch (channelID) {
case MikrotikBindingConstants.CHANNEL_DEFAULT_NAME:
return StateUtil.stringOrNull(etherIface.getDefaultName());
case MikrotikBindingConstants.CHANNEL_STATE:
return StateUtil.stringOrNull(etherIface.getState());
case MikrotikBindingConstants.CHANNEL_RATE:
return StateUtil.stringOrNull(etherIface.getRate());
default:
return UnDefType.UNDEF;
}
}
protected State getEtherIterfaceChannelState(String channelID) {
RouterosEthernetInterface etherIface = (RouterosEthernetInterface) this.iface;
if (etherIface == null) {
@@ -378,19 +404,19 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
@Override
protected void executeCommand(ChannelUID channelUID, Command command) {
RouterosDevice routeros = getRouterOs();
RouterosInterfaceBase iface = this.iface;
InterfaceThingConfig cfg = this.config;
if (routeros == null || iface == null || cfg == null) {
return;
}
switch (channelUID.getId()) {
case MikrotikBindingConstants.CHANNEL_POE_OUT_STATE:
RouterosInterfaceBase iface = this.iface;
if (!(iface instanceof RouterosEthernetInterface routerOsIface)) {
logger.warn("Cannot set POE Out State: interface is null or not an Ethernet interface");
return;
}
RouterosDevice routeros = getRouterOs();
if (routeros == null || !routeros.isConnected()) {
return;
}
try {
routeros.setPOEOutState(routerOsIface, command.toString());
} catch (MikrotikApiException e) {
@@ -398,6 +424,13 @@ public class MikrotikInterfaceThingHandler extends MikrotikBaseThingHandler<Inte
getThing().getUID(), e.getMessage());
}
break;
case CHANNEL_ENABLED:
if (command == OnOffType.ON) {
routeros.setEnabledState(iface, "no");
} else if (command == OnOffType.OFF) {
routeros.setEnabledState(iface, "yes");
}
break;
default:
logger.debug("Ignoring unsupported command = {} for channel = {}", command, channelUID);
}
@@ -12,6 +12,7 @@
*/
package org.openhab.binding.mikrotik.internal.handler;
import static org.openhab.binding.mikrotik.internal.MikrotikBindingConstants.*;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.types.RefreshType.REFRESH;
@@ -59,6 +60,7 @@ public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
private @Nullable RouterosThingConfig config;
private @Nullable volatile RouterosDevice routeros;
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable ScheduledFuture<?> dailyJob;
private Map<String, State> currentState = new HashMap<>();
public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -120,6 +122,9 @@ public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
private void scheduleRefreshJob() {
synchronized (this) {
if (dailyJob == null) {
dailyJob = scheduler.scheduleWithFixedDelay(this::dailyRun, 1, 1440, TimeUnit.MINUTES);
}
var cfg = this.config;
if (refreshJob == null) {
int refreshPeriod = 10;
@@ -142,9 +147,22 @@ public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
job.cancel(true);
this.refreshJob = null;
}
job = this.dailyJob;
if (job != null) {
job.cancel(true);
this.refreshJob = null;
}
}
}
private void dailyRun() {
RouterosDevice routerOs = getRouteros();
if (routerOs == null) {
return;
}
updateState(CHANNEL_UPDATE_AVAILABLE, StateUtil.stringOrNull(routerOs.firmwareCheck()));
}
private void scheduledRun() {
var routeros = this.routeros;
if (routeros == null) {
@@ -247,40 +265,43 @@ public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
RouterosDevice routerOs = getRouteros();
String channelID = channelUID.getIdWithoutGroup();
RouterosSystemResources rbRes = null;
if (routerOs != null) {
rbRes = routerOs.getSysResources();
if (routerOs == null) {
return;
}
rbRes = routerOs.getSysResources();
State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
State newState = oldState;
if (rbRes == null) {
newState = UnDefType.NULL;
} else {
switch (channelID) {
case MikrotikBindingConstants.CHANNEL_UP_SINCE:
switch (channelUID.getId()) {
case CHANNEL_UP_SINCE:
newState = StateUtil.timeOrNull(rbRes.getUptimeStart());
break;
case MikrotikBindingConstants.CHANNEL_FREE_SPACE:
case CHANNEL_FREE_SPACE:
newState = StateUtil.qtyBytesOrNull(rbRes.getFreeSpace());
break;
case MikrotikBindingConstants.CHANNEL_TOTAL_SPACE:
case CHANNEL_TOTAL_SPACE:
newState = StateUtil.qtyBytesOrNull(rbRes.getTotalSpace());
break;
case MikrotikBindingConstants.CHANNEL_USED_SPACE:
case CHANNEL_USED_SPACE:
newState = StateUtil.qtyPercentOrNull(rbRes.getSpaceUse());
break;
case MikrotikBindingConstants.CHANNEL_FREE_MEM:
case CHANNEL_FREE_MEM:
newState = StateUtil.qtyBytesOrNull(rbRes.getFreeMem());
break;
case MikrotikBindingConstants.CHANNEL_TOTAL_MEM:
case CHANNEL_TOTAL_MEM:
newState = StateUtil.qtyBytesOrNull(rbRes.getTotalMem());
break;
case MikrotikBindingConstants.CHANNEL_USED_MEM:
case CHANNEL_USED_MEM:
newState = StateUtil.qtyPercentOrNull(rbRes.getMemUse());
break;
case MikrotikBindingConstants.CHANNEL_CPU_LOAD:
case CHANNEL_CPU_LOAD:
newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad());
break;
case CHANNEL_UPDATE_AVAILABLE:
break;
default:
logger.warn("Unimplemented channel:{}", channelID);
}
@@ -56,6 +56,7 @@ public class RouterosDevice {
public static final String PROP_CONFIG_SSID_KEY = "configuration.ssid";
private static final String CMD_PRINT_IFACES = "/interface/print";
private static final String CMD_UPDATE_CHECK = "/system/package/update/check-for-updates";
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_MONITOR_POE_TPL = "/interface/%s/poe/monitor numbers=%s once";
@@ -90,6 +91,10 @@ public class RouterosDevice {
return Optional.of(new RouterosBridgeInterface(interfaceProps));
case CAP:
return Optional.of(new RouterosCapInterface(interfaceProps));
case VLAN:
return Optional.of(new RouterosVlanInterface(interfaceProps));
case VETH:
return Optional.of(new RouterosVethInterface(interfaceProps));
case WLAN:
return Optional.of(new RouterosWlanInterface(interfaceProps));
case WIFI:
@@ -225,6 +230,18 @@ public class RouterosDevice {
return rbWirelessType;
}
public String getWifiPackageName() {
switch (getWirelessType()) {
case WIRELESS:
return "wireless";
case WIFIWAVE2:
return "wifiwave2";
case WIFI:
default:
return "wifi";
}
}
public @Nullable RouterosCapsmanRegistration findCapsmanRegistration(String macAddress) {
Optional<RouterosCapsmanRegistration> searchResult = capsmanRegistrationCache.stream()
.filter(registration -> macAddress.equalsIgnoreCase(registration.getMacAddress())).findFirst();
@@ -404,6 +421,40 @@ public class RouterosDevice {
this.resourcesCache = new RouterosSystemResources(response.get(0));
}
public @Nullable String firmwareCheck() {
ApiConnection conn = this.connection;
if (conn == null) {
return "Firmware check failed";
}
List<Map<String, String>> response = null;
try {
response = conn.execute(CMD_UPDATE_CHECK);
} catch (MikrotikApiException e) {
logger.debug("Failed to determine if the firmware is the latest version");
}
if (response == null) {
return "Firmware check failed";
}
String finalStatus = "";
String newVersion = "Unknown";
for (Map<String, String> map : response) {
// Keep the very last status
if (map.containsKey("status")) {
finalStatus = map.get("status");
}
if (map.containsKey("latest-version")) {
newVersion = map.get("latest-version");
}
}
if (finalStatus == null) {
return "Firmware check failed";
}
if ("New version is available".equalsIgnoreCase(finalStatus)) {
return "New version " + newVersion + " is available";
}
return finalStatus.isEmpty() ? "Firmware check failed" : finalStatus;
}
private void updateRouterboardInfo() throws MikrotikApiException {
ApiConnection conn = this.connection;
if (conn == null) {
@@ -429,4 +480,18 @@ public class RouterosDevice {
String cmd = String.format(CMD_SET_POE_OUT_TPL, ifaceModel.getApiType(), ifaceModel.getName(), state);
conn.execute(cmd);
}
public void setEnabledState(RouterosInterfaceBase ifaceModel, String disabledState) {
ApiConnection conn = this.connection;
if (conn == null) {
return;
}
String cmd = String.format("/interface/%s/set disabled=%s .id=%s", ifaceModel.getApiType(), disabledState,
ifaceModel.getName());
try {
conn.execute(cmd);
} catch (MikrotikApiException e) {
logger.warn("Exception {} occured when disabling device {}", e.getMessage(), ifaceModel.getName());
}
}
}
@@ -46,7 +46,7 @@ public class RouterosEthernetInterface extends RouterosInterfaceBase {
@Override
public boolean hasMonitor() {
// PowerLine interfaces are of ehter type too
// PowerLine interfaces are of ether type too
String name = getDefaultName();
return name != null && !name.startsWith("pwr");
}
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2010-2026 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;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link RouterosVethInterface} is a model class for `veth` 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 RouterosVethInterface extends RouterosInterfaceBase {
public RouterosVethInterface(Map<String, String> props) {
super(props);
}
@Override
public RouterosInterfaceType getDesignedType() {
return RouterosInterfaceType.VETH;
}
@Override
public String getApiType() {
return "veth";
}
@Override
public boolean hasDetailedReport() {
return true;
}
@Override
public @Nullable String getMacAddress() {
return null;
}
@Override
public boolean hasMonitor() {
String name = getDefaultName();
return name != null;
}
public @Nullable String getDefaultName() {
return getProp("default-name");
}
public @Nullable String getRate() {
return getProp("rate");
}
public @Nullable String getState() {
return getProp("status");
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (c) 2010-2026 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;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link RouterosVlanInterface} is a model class for `vlan` 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 RouterosVlanInterface extends RouterosInterfaceBase {
public RouterosVlanInterface(Map<String, String> props) {
super(props);
}
@Override
public RouterosInterfaceType getDesignedType() {
return RouterosInterfaceType.VLAN;
}
@Override
public String getApiType() {
return "vlan";
}
@Override
public boolean hasDetailedReport() {
return true;
}
@Override
public boolean hasMonitor() {
String name = getDefaultName();
return name != null;
}
@Override
public @Nullable String getMacAddress() {
return null;
}
public @Nullable String getDefaultName() {
return getProp("default-name");
}
public @Nullable String getRate() {
return getProp("rate");
}
public @Nullable String getState() {
return getProp("status");
}
public @Nullable String getPOEOutState() {
return getProp("poe-out");
}
public @Nullable String getPOEOutStatus() {
return getProp("poe-out-status");
}
public @Nullable Float getPOEOutPower() {
return getFloatProp("poe-out-power");
}
}
@@ -121,6 +121,8 @@ channel-type.mikrotik.txRate.label = Transmission Rate
channel-type.mikrotik.txRate.description = Rate of data transmission in megabits per second
channel-type.mikrotik.upSince.label = Up since
channel-type.mikrotik.upSince.description = Time when thing got up
channel-type.mikrotik.updateAvailable.label = Update Available
channel-type.mikrotik.updateAvailable.description = Is a newer firmware available for this RouterOS device
channel-type.mikrotik.usedMemory.label = Used RAM %
channel-type.mikrotik.usedMemory.description = Percentage of used device memory
channel-type.mikrotik.usedSpace.label = Used Space %
@@ -16,6 +16,7 @@
<channel id="usedMemory" typeId="usedMemory"/>
<channel id="cpuLoad" typeId="cpuLoad"/>
<channel id="upSince" typeId="upSince"/>
<channel id="updateAvailable" typeId="updateAvailable"/>
</channels>
<properties>
<property name="vendor">Mikrotik</property>
@@ -209,6 +210,12 @@
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="updateAvailable">
<item-type>String</item-type>
<label>Update Available</label>
<description>Is a newer firmware available for this RouterOS device</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="interfaceType" advanced="true">
@@ -243,7 +250,6 @@
<item-type>Switch</item-type>
<label>Enabled</label>
<description>Reflects enabled or disabled state</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="connected">
@@ -2,7 +2,13 @@
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="mikrotik:routeros">
<instruction-set targetVersion="1">
<add-channel id="updateAvailable">
<type>mikrotik:updateAvailable</type>
</add-channel>
</instruction-set>
</thing-type>
<thing-type uid="mikrotik:interface">
<instruction-set targetVersion="1">
<add-channel id="poeOutState">