]> git.basschouten.com Git - openhab-addons.git/blob
83985d0f5ea71bec86a6215f0aadfbac2b7eac8b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mikrotik.internal.handler;
14
15 import static org.openhab.core.thing.ThingStatus.ONLINE;
16 import static org.openhab.core.types.RefreshType.REFRESH;
17
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
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;
46
47 import me.legrange.mikrotik.MikrotikApiException;
48
49 /**
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.
53  *
54  * @author Oleg Vivtash - Initial contribution
55  */
56 @NonNullByDefault
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<>();
63
64     public static boolean supportsThingType(ThingTypeUID thingTypeUID) {
65         return MikrotikBindingConstants.THING_TYPE_ROUTEROS.equals(thingTypeUID);
66     }
67
68     public MikrotikRouterosBridgeHandler(Bridge bridge) {
69         super(bridge);
70     }
71
72     @Override
73     public void initialize() {
74         cancelRefreshJob();
75         var cfg = getConfigAs(RouterosThingConfig.class);
76         this.config = cfg;
77         logger.debug("Initializing MikrotikRouterosBridgeHandler with config = {}", cfg);
78         if (cfg.isValid()) {
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));
81             scheduleRefreshJob();
82         } else {
83             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Configuration is not valid");
84         }
85     }
86
87     public @Nullable RouterosDevice getRouteros() {
88         return routeros;
89     }
90
91     public @Nullable RouterosThingConfig getBridgeConfig() {
92         return config;
93     }
94
95     @Override
96     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
97         if (status == ThingStatus.ONLINE
98                 || (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
99             scheduleRefreshJob();
100         } else if (status == ThingStatus.OFFLINE && statusDetail == ThingStatusDetail.CONFIGURATION_ERROR) {
101             cancelRefreshJob();
102         }
103         // update the status only if it's changed
104         ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
105                 .build();
106         if (!statusInfo.equals(getThing().getStatusInfo())) {
107             super.updateStatus(status, statusDetail, description);
108         }
109     }
110
111     @Override
112     public void dispose() {
113         cancelRefreshJob();
114         var routeros = this.routeros;
115         if (routeros != null) {
116             routeros.stop();
117             this.routeros = null;
118         }
119     }
120
121     private void scheduleRefreshJob() {
122         synchronized (this) {
123             var cfg = this.config;
124             if (refreshJob == null) {
125                 int refreshPeriod = 10;
126                 if (cfg != null) {
127                     refreshPeriod = cfg.refresh;
128                 } else {
129                     logger.warn("null config spotted in scheduleRefreshJob");
130                 }
131                 logger.debug("Scheduling refresh job every {}s", refreshPeriod);
132                 refreshJob = scheduler.scheduleWithFixedDelay(this::scheduledRun, 0, refreshPeriod, TimeUnit.SECONDS);
133             }
134         }
135     }
136
137     private void cancelRefreshJob() {
138         synchronized (this) {
139             var job = this.refreshJob;
140             if (job != null) {
141                 logger.debug("Cancelling refresh job");
142                 job.cancel(true);
143                 this.refreshJob = null;
144             }
145         }
146     }
147
148     private void scheduledRun() {
149         var routeros = this.routeros;
150         if (routeros == null) {
151             logger.error("RouterOS device is null in scheduledRun");
152             return;
153         }
154         if (!routeros.isConnected()) {
155             // Perform connection
156             try {
157                 logger.debug("Starting routeros model");
158                 routeros.start();
159
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);
167                 } else {
168                     logger.warn("Failed to set RouterBOARD properties for bridge {}", getThing().getUID());
169                 }
170                 updateStatus(ThingStatus.ONLINE);
171             } catch (MikrotikApiException e) {
172                 logger.warn("Error while logging in to RouterOS {} | Cause: {}", getThing().getUID(), e, e.getCause());
173
174                 String errorMessage = e.getMessage();
175                 if (errorMessage == null) {
176                     errorMessage = "Error connecting (UNKNOWN ERROR)";
177                 }
178                 if (errorMessage.contains("Command timed out") || errorMessage.contains("Error connecting")) {
179                     routeros.stop();
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");
184                 } else {
185                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
186                 }
187             }
188         } else {
189             // We're connected - do a usual polling cycle
190             performRefresh();
191         }
192     }
193
194     private void performRefresh() {
195         var routeros = this.routeros;
196         if (routeros == null) {
197             logger.error("RouterOS device is null in performRefresh");
198             return;
199         }
200         try {
201             logger.debug("Refreshing RouterOS caches for {}", getThing().getUID());
202             routeros.refresh();
203             // refresh own channels
204             for (Channel channel : getThing().getChannels()) {
205                 try {
206                     refreshChannel(channel.getUID());
207                 } catch (RuntimeException e) {
208                     throw new ChannelUpdateException(getThing().getUID(), channel.getUID(), e);
209                 }
210             }
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();
216                 }
217             });
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);
222             routeros.stop();
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());
227         }
228     }
229
230     @Override
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);
238                 } else {
239                     logger.warn("Ignoring command = {} for channel = {} as it is not yet supported", command,
240                             channelUID);
241                 }
242             }
243         }
244     }
245
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();
252         }
253         State oldState = currentState.getOrDefault(channelID, UnDefType.NULL);
254         State newState = oldState;
255
256         if (rbRes == null) {
257             newState = UnDefType.NULL;
258         } else {
259             switch (channelID) {
260                 case MikrotikBindingConstants.CHANNEL_UP_SINCE:
261                     newState = StateUtil.timeOrNull(rbRes.getUptimeStart());
262                     break;
263                 case MikrotikBindingConstants.CHANNEL_FREE_SPACE:
264                     newState = StateUtil.qtyBytesOrNull(rbRes.getFreeSpace());
265                     break;
266                 case MikrotikBindingConstants.CHANNEL_TOTAL_SPACE:
267                     newState = StateUtil.qtyBytesOrNull(rbRes.getTotalSpace());
268                     break;
269                 case MikrotikBindingConstants.CHANNEL_USED_SPACE:
270                     newState = StateUtil.qtyPercentOrNull(rbRes.getSpaceUse());
271                     break;
272                 case MikrotikBindingConstants.CHANNEL_FREE_MEM:
273                     newState = StateUtil.qtyBytesOrNull(rbRes.getFreeMem());
274                     break;
275                 case MikrotikBindingConstants.CHANNEL_TOTAL_MEM:
276                     newState = StateUtil.qtyBytesOrNull(rbRes.getTotalMem());
277                     break;
278                 case MikrotikBindingConstants.CHANNEL_USED_MEM:
279                     newState = StateUtil.qtyPercentOrNull(rbRes.getMemUse());
280                     break;
281                 case MikrotikBindingConstants.CHANNEL_CPU_LOAD:
282                     newState = StateUtil.qtyPercentOrNull(rbRes.getCpuLoad());
283                     break;
284             }
285         }
286
287         if (!newState.equals(oldState)) {
288             updateState(channelID, newState);
289             currentState.put(channelID, newState);
290         }
291     }
292 }