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.OFFLINE;
16 import static org.openhab.core.thing.ThingStatus.ONLINE;
17 import static org.openhab.core.thing.ThingStatusDetail.CONFIGURATION_ERROR;
18 import static org.openhab.core.types.RefreshType.REFRESH;
20 import java.lang.reflect.ParameterizedType;
21 import java.time.Duration;
22 import java.util.HashMap;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.mikrotik.internal.config.ConfigValidation;
30 import org.openhab.binding.mikrotik.internal.config.RouterosThingConfig;
31 import org.openhab.binding.mikrotik.internal.model.RouterosDevice;
32 import org.openhab.core.cache.ExpiringCache;
33 import org.openhab.core.thing.Bridge;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.ThingStatusInfo;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.State;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link MikrotikBaseThingHandler} is a base class for all other RouterOS things of map-value nature.
49 * It is responsible for handling commands, which are sent to one of the channels and emit channel updates
52 * @author Oleg Vivtash - Initial contribution
55 * @param <C> config - the config class used by this base thing handler
59 public abstract class MikrotikBaseThingHandler<C extends ConfigValidation> extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(MikrotikBaseThingHandler.class);
61 protected @Nullable C config;
62 private @Nullable ScheduledFuture<?> refreshJob;
63 protected ExpiringCache<Boolean> refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
64 protected Map<String, State> currentState = new HashMap<>();
66 // public static boolean supportsThingType(ThingTypeUID thingTypeUID) <- in subclasses
68 public MikrotikBaseThingHandler(Thing thing) {
72 protected @Nullable MikrotikRouterosBridgeHandler getVerifiedBridgeHandler() {
73 Bridge bridgeRef = getBridge();
74 if (bridgeRef != null && bridgeRef.getHandler() != null
75 && (bridgeRef.getHandler() instanceof MikrotikRouterosBridgeHandler)) {
76 return (MikrotikRouterosBridgeHandler) bridgeRef.getHandler();
81 protected final @Nullable RouterosDevice getRouterOs() {
82 MikrotikRouterosBridgeHandler bridgeHandler = getVerifiedBridgeHandler();
83 return bridgeHandler == null ? null : bridgeHandler.getRouteros();
87 public void handleCommand(ChannelUID channelUID, Command command) {
88 logger.debug("Handling command = {} for channel = {}", command, channelUID);
89 if (getThing().getStatus() == ONLINE) {
90 RouterosDevice routeros = getRouterOs();
91 if (routeros != null) {
92 if (command == REFRESH) {
93 refreshCache.getValue();
94 refreshChannel(channelUID);
97 executeCommand(channelUID, command);
98 } catch (RuntimeException e) {
99 logger.warn("Unexpected error handling command = {} for channel = {} : {}", command, channelUID,
107 @SuppressWarnings("unchecked")
109 public void initialize() {
111 if (getVerifiedBridgeHandler() == null) {
112 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "This thing requires a RouterOS bridge");
116 var superKlass = (ParameterizedType) getClass().getGenericSuperclass();
117 if (superKlass == null) {
118 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
119 "getGenericSuperclass failed for thing handler");
122 Class<?> klass = (Class<?>) (superKlass.getActualTypeArguments()[0]);
124 C localConfig = (C) getConfigAs(klass);
125 this.config = localConfig;
127 if (!localConfig.isValid()) {
128 updateStatus(OFFLINE, CONFIGURATION_ERROR, String.format("%s is invalid", klass.getSimpleName()));
132 updateStatus(ONLINE);
136 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
137 if (status == ONLINE || (status == OFFLINE && statusDetail == ThingStatusDetail.COMMUNICATION_ERROR)) {
138 scheduleRefreshJob();
139 } else if (status == OFFLINE
140 && (statusDetail == ThingStatusDetail.CONFIGURATION_ERROR || statusDetail == ThingStatusDetail.GONE)) {
144 // update the status only if it's changed
145 ThingStatusInfo statusInfo = ThingStatusInfoBuilder.create(status, statusDetail).withDescription(description)
147 if (!statusInfo.equals(getThing().getStatusInfo())) {
148 super.updateStatus(status, statusDetail, description);
152 @SuppressWarnings("null")
153 private void scheduleRefreshJob() {
154 synchronized (this) {
155 if (refreshJob == null) {
156 var bridgeHandler = getVerifiedBridgeHandler();
157 if (bridgeHandler == null) {
158 updateStatus(OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot obtain bridge handler");
161 RouterosThingConfig bridgeConfig = bridgeHandler.getBridgeConfig();
162 int refreshPeriod = bridgeConfig.refresh;
163 logger.debug("Scheduling refresh job every {}s", refreshPeriod);
165 this.refreshCache = new ExpiringCache<>(Duration.ofSeconds(refreshPeriod), this::verifiedRefreshModels);
166 refreshJob = scheduler.scheduleWithFixedDelay(this::scheduledRun, refreshPeriod, refreshPeriod,
172 private void cancelRefreshJob() {
173 synchronized (this) {
174 var job = this.refreshJob;
176 logger.debug("Cancelling refresh job");
178 this.refreshJob = null;
179 // Not setting to null as getValue() can potentially be called after
180 this.refreshCache = new ExpiringCache<>(Duration.ofDays(1), () -> false);
185 private void scheduledRun() {
186 MikrotikRouterosBridgeHandler bridgeHandler = getVerifiedBridgeHandler();
187 if (bridgeHandler == null) {
188 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Failed reaching out to RouterOS bridge");
191 Bridge bridge = getBridge();
192 if (bridge != null && bridge.getStatus() == OFFLINE) {
193 updateStatus(OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "The RouterOS bridge is currently offline");
197 if (getThing().getStatus() != ONLINE) {
198 updateStatus(ONLINE);
200 logger.debug("Refreshing all {} channels", getThing().getUID());
201 for (Channel channel : getThing().getChannels()) {
203 refreshChannel(channel.getUID());
204 } catch (RuntimeException e) {
205 logger.warn("Unhandled exception while refreshing the {} Mikrotik thing", getThing().getUID(), e);
206 updateStatus(OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
211 protected final void refresh() throws ChannelUpdateException {
212 if (getThing().getStatus() == ONLINE) {
213 if (getRouterOs() != null) {
214 refreshCache.getValue();
215 for (Channel channel : getThing().getChannels()) {
216 ChannelUID channelUID = channel.getUID();
218 refreshChannel(channelUID);
219 } catch (RuntimeException e) {
220 throw new ChannelUpdateException(getThing().getUID(), channelUID, e);
227 protected boolean verifiedRefreshModels() {
228 if (getRouterOs() != null && config != null) {
237 public void dispose() {
241 protected abstract void refreshModels();
243 protected abstract void refreshChannel(ChannelUID channelUID);
245 protected abstract void executeCommand(ChannelUID channelUID, Command command);