2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mikrotik.internal.handler;
15 import static org.openhab.core.thing.ThingStatus.ONLINE;
16 import static org.openhab.core.types.RefreshType.REFRESH;
18 import java.util.HashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mikrotik.internal.MikrotikBindingConstants;
26 import org.openhab.binding.mikrotik.internal.config.RouterosThingConfig;
27 import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
28 import org.openhab.binding.mikrotik.internal.model.RouterosRouterboardInfo;
29 import org.openhab.binding.mikrotik.internal.model.RouterosSystemResources;
30 import org.openhab.binding.mikrotik.internal.util.StateUtil;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.ThingStatusInfo;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.binding.BaseBridgeHandler;
39 import org.openhab.core.thing.binding.ThingHandler;
40 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.State;
43 import org.openhab.core.types.UnDefType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 import me.legrange.mikrotik.MikrotikApiException;
50 * The {@link MikrotikRouterosBridgeHandler} is a main binding class that wraps a {@link RouterosDevice} and
51 * manages fetching data from RouterOS. It is also responsible for updating brindge thing properties and
52 * handling commands, which are sent to one of the channels and emit channel updates whenever required.
54 * @author Oleg Vivtash - Initial contribution
57 public class MikrotikRouterosBridgeHandler extends BaseBridgeHandler {
58 private final Logger logger = LoggerFactory.getLogger(MikrotikRouterosBridgeHandler.class);
59 private @Nullable RouterosThingConfig config;
60 private @Nullable volatile RouterosDevice routeros;
61 private @Nullable ScheduledFuture<?> refreshJob;
62 private Map<String, State> currentState = new HashMap<>();
64 public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
65 return MikrotikBindingConstants.THING_TYPE_ROUTEROS.equals(thingTypeUID);
68 public MikrotikRouterosBridgeHandler(Bridge bridge) {
73 public void initialize() {
75 var cfg = getConfigAs(RouterosThingConfig.class);
77 logger.debug("Initializing MikrotikRouterosBridgeHandler with config = {}", cfg);
79 this.routeros = new RouterosDevice(cfg.host, cfg.port, cfg.login, cfg.password);
80 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, String.format("Connecting to %s", cfg.host));
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration is not valid");
87 public @Nullable RouterosDevice getRouteros() {
91 public @Nullable RouterosThingConfig getBridgeConfig() {
96 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
97 if (status == ThingStatus.ONLINE
98 || (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
100 } else if (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.CONFIGURATION_ERROR) {
103 // update the status only if it's changed
104 ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
106 if (!statusInfo.equals(getThing().getStatusInfo())) {
107 super.updateStatus(status, statusDetail, description);
112 public void dispose() {
114 var routeros = this.routeros;
115 if (routeros != null) {
117 this.routeros = null;
121 private void scheduleRefreshJob() {
122 synchronized (this) {
123 var cfg = this.config;
124 if (refreshJob == null) {
125 int refreshPeriod = 10;
127 refreshPeriod = cfg.refresh;
129 logger.warn("null config spotted in scheduleRefreshJob");
131 logger.debug("Scheduling refresh job every {}s", refreshPeriod);
132 refreshJob = scheduler.scheduleWithFixedDelay(this::scheduledRun, 0, refreshPeriod, TimeUnit.SECONDS);
137 private void cancelRefreshJob() {
138 synchronized (this) {
139 var job = this.refreshJob;
141 logger.debug("Cancelling refresh job");
143 this.refreshJob = null;
148 private void scheduledRun() {
149 var routeros = this.routeros;
150 if (routeros == null) {
151 logger.error("RouterOS device is null in scheduledRun");
154 if (!routeros.isConnected()) {
155 // Perform connection
157 logger.debug("Starting routeros model");
160 RouterosRouterboardInfo rbInfo = routeros.getRouterboardInfo();
161 if (rbInfo != null) {
162 Map<String, String> bridgeProps = editProperties();
163 bridgeProps.put(MikrotikBindingConstants.PROPERTY_MODEL, rbInfo.getModel());
164 bridgeProps.put(MikrotikBindingConstants.PROPERTY_FIRMWARE, rbInfo.getFirmware());
165 bridgeProps.put(MikrotikBindingConstants.PROPERTY_SERIAL_NUMBER, rbInfo.getSerialNumber());
166 updateProperties(bridgeProps);
168 logger.warn("Failed to set RouterBOARD properties for bridge {}", getThing().getUID());
170 updateStatus(ThingStatus.ONLINE);
171 } catch (MikrotikApiException e) {
172 logger.warn("Error while logging in to RouterOS {} | Cause: {}", getThing().getUID(), e, e.getCause());
174 String errorMessage = e.getMessage();
175 if (errorMessage == null) {
176 errorMessage = "Error connecting (UNKNOWN ERROR)";
178 if (errorMessage.contains("Command timed out") || errorMessage.contains("Error connecting")) {
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
181 } else if (errorMessage.contains("Connection refused")) {
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
183 "Remote host refused to connect, make sure port is correct");
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
189 // We're connected - do a usual polling cycle
194 private void performRefresh() {
195 var routeros = this.routeros;
196 if (routeros == null) {
197 logger.error("RouterOS device is null in performRefresh");
201 logger.debug("Refreshing RouterOS caches for {}", getThing().getUID());
203 // refresh own channels
204 for (Channel channel : getThing().getChannels()) {
206 refreshChannel(channel.getUID());
207 } catch (RuntimeException e) {
208 throw new ChannelUpdateException(getThing().getUID(), channel.getUID(), e);
211 // refresh all the client things below
212 getThing().getThings().forEach(thing -> {
213 ThingHandler handler = thing.getHandler();
214 if (handler instanceof MikrotikBaseThingHandler<?> thingHandler) {
215 thingHandler.refresh();
218 } catch (ChannelUpdateException e) {
219 logger.debug("Error updating channel! {}", e.getMessage(), e.getCause());
220 } catch (MikrotikApiException e) {
221 logger.error("RouterOS cache refresh failed in {} due to Mikrotik API error", getThing().getUID(), e);
223 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
224 } catch (Exception e) {
225 logger.error("Unhandled exception while refreshing the {} RouterOS model", getThing().getUID(), e);
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
231 public void handleCommand(ChannelUID channelUID, Command command) {
232 logger.debug("Handling command = {} for channel = {}", command, channelUID);
233 if (getThing().getStatus() == ONLINE) {
234 RouterosDevice routeros = getRouteros();
235 if (routeros != null) {
236 if (command == REFRESH) {
237 refreshChannel(channelUID);
239 logger.warn("Ignoring command = {} for channel = {} as it is not yet supported", command,
246 protected void refreshChannel(ChannelUID channelUID) {
247 RouterosDevice routerOs = getRouteros();
248 String channelID = channelUID.getIdWithoutGroup();
249 RouterosSystemResources rbRes = null;
250 if (routerOs != null) {
251 rbRes = routerOs.getSysResources();
253 State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
254 State newState = oldState;
257 newState = UnDefType.NULL;
260 case MikrotikBindingConstants.CHANNEL_UP_SINCE:
261 newState = StateUtil.timeOrNull(rbRes.getUptimeStart());
263 case MikrotikBindingConstants.CHANNEL_FREE_SPACE:
264 newState = StateUtil.qtyBytesOrNull(rbRes.getFreeSpace());
266 case MikrotikBindingConstants.CHANNEL_TOTAL_SPACE:
267 newState = StateUtil.qtyBytesOrNull(rbRes.getTotalSpace());
269 case MikrotikBindingConstants.CHANNEL_USED_SPACE:
270 newState = StateUtil.qtyPercentOrNull(rbRes.getSpaceUse());
272 case MikrotikBindingConstants.CHANNEL_FREE_MEM:
273 newState = StateUtil.qtyBytesOrNull(rbRes.getFreeMem());
275 case MikrotikBindingConstants.CHANNEL_TOTAL_MEM:
276 newState = StateUtil.qtyBytesOrNull(rbRes.getTotalMem());
278 case MikrotikBindingConstants.CHANNEL_USED_MEM:
279 newState = StateUtil.qtyPercentOrNull(rbRes.getMemUse());
281 case MikrotikBindingConstants.CHANNEL_CPU_LOAD:
282 newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad());
287 if (!newState.equals(oldState)) {
288 updateState(channelID, newState);
289 currentState.put(channelID, newState);