2 * Copyright (c) 2010-2023 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.tr064.internal;
15 import static org.openhab.binding.tr064.internal.Tr064BindingConstants.*;
17 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.tr064.internal.config.Tr064ChannelConfig;
26 import org.openhab.binding.tr064.internal.config.Tr064SubConfiguration;
27 import org.openhab.binding.tr064.internal.dto.scpd.root.SCPDDeviceType;
28 import org.openhab.binding.tr064.internal.soap.SOAPConnector;
29 import org.openhab.binding.tr064.internal.util.SCPDUtil;
30 import org.openhab.binding.tr064.internal.util.Util;
31 import org.openhab.core.cache.ExpiringCacheMap;
32 import org.openhab.core.thing.Bridge;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingStatusInfo;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.thing.binding.ThingHandlerCallback;
41 import org.openhab.core.thing.binding.builder.ThingBuilder;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.State;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link Tr064SubHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Jan N. Klug - Initial contribution
56 public class Tr064SubHandler extends BaseThingHandler {
57 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_SUBDEVICE,
58 THING_TYPE_SUBDEVICE_LAN);
59 private static final int RETRY_INTERVAL = 60;
61 private final Logger logger = LoggerFactory.getLogger(Tr064SubHandler.class);
63 private Tr064SubConfiguration config = new Tr064SubConfiguration();
65 private String deviceType = "";
66 private boolean isInitialized = false;
68 private final Map<ChannelUID, Tr064ChannelConfig> channels = new HashMap<>();
69 // caching is used to prevent excessive calls to the same action
70 private final ExpiringCacheMap<ChannelUID, State> stateCache = new ExpiringCacheMap<>(2000);
72 private @Nullable SOAPConnector soapConnector;
73 private @Nullable ScheduledFuture<?> connectFuture;
74 private @Nullable ScheduledFuture<?> pollFuture;
76 Tr064SubHandler(Thing thing) {
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 Tr064ChannelConfig channelConfig = channels.get(channelUID);
83 if (channelConfig == null) {
84 logger.trace("Channel {} not supported.", channelUID);
88 if (command instanceof RefreshType) {
89 final SOAPConnector soapConnector = this.soapConnector;
90 State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
91 : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
93 updateState(channelUID, state);
98 if (channelConfig.getChannelTypeDescription().getSetAction() == null) {
99 logger.debug("Discarding command {} to {}, read-only channel", command, channelUID);
102 scheduler.execute(() -> {
103 final SOAPConnector soapConnector = this.soapConnector;
104 if (soapConnector == null) {
105 logger.warn("Could not send command because connector not available");
107 soapConnector.sendChannelCommandToDevice(channelConfig, command);
113 public void initialize() {
114 config = getConfigAs(Tr064SubConfiguration.class);
115 if (!config.isValid()) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
117 "One or more mandatory configuration fields are empty");
121 final Bridge bridge = getBridge();
122 if (bridge != null && bridge.getStatus().equals(ThingStatus.ONLINE)) {
123 updateStatus(ThingStatus.UNKNOWN);
124 connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, 30, TimeUnit.SECONDS);
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
130 private void internalInitialize() {
131 final Bridge bridge = getBridge();
132 if (bridge == null) {
135 final Tr064RootHandler bridgeHandler = (Tr064RootHandler) bridge.getHandler();
136 if (bridgeHandler == null) {
137 logger.warn("Bridge-handler is null in thing {}", thing.getUID());
140 final SCPDUtil scpdUtil = bridgeHandler.getSCPDUtil();
141 if (scpdUtil == null) {
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
143 "Could not get device definitions");
146 final ThingHandlerCallback callback = getCallback();
147 if (callback == null) {
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Could not get callback");
151 if (checkProperties(scpdUtil)) {
152 // properties set, check channels
153 ThingBuilder thingBuilder = editThing();
154 thingBuilder.withoutChannels(thing.getChannels());
155 Util.checkAvailableChannels(thing, callback, thingBuilder, scpdUtil, config.uuid, deviceType, channels);
156 updateThing(thingBuilder.build());
158 // remove connect scheduler
159 removeConnectScheduler();
160 soapConnector = bridgeHandler.getSOAPConnector();
162 isInitialized = true;
164 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
168 private void removeConnectScheduler() {
169 final ScheduledFuture<?> connectFuture = this.connectFuture;
170 if (connectFuture != null) {
171 connectFuture.cancel(true);
172 this.connectFuture = null;
177 public void dispose() {
178 removeConnectScheduler();
182 isInitialized = false;
188 * poll remote device for channel values
190 private void poll() {
191 SOAPConnector soapConnector = this.soapConnector;
192 channels.forEach((channelUID, channelConfig) -> {
193 if (isLinked(channelUID)) {
194 State state = stateCache.putIfAbsentAndGet(channelUID, () -> soapConnector == null ? UnDefType.UNDEF
195 : soapConnector.getChannelStateFromDevice(channelConfig, channels, stateCache));
197 updateState(channelUID, state);
204 * get device properties from remote device
206 * @param scpdUtil the SCPD util of this device
207 * @return true if successfull
209 private boolean checkProperties(SCPDUtil scpdUtil) {
211 SCPDDeviceType device = scpdUtil.getDevice(config.uuid)
212 .orElseThrow(() -> new SCPDException("Could not find device " + config.uuid));
213 String deviceType = device.getDeviceType();
214 if (deviceType == null) {
215 throw new SCPDException("deviceType can't be null ");
217 this.deviceType = deviceType;
219 Map<String, String> properties = editProperties();
220 properties.put("deviceType", deviceType);
221 updateProperties(properties);
224 } catch (SCPDException e) {
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
226 "Failed to update device properties: " + e.getMessage());
233 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
234 if (!bridgeStatusInfo.getStatus().equals(ThingStatus.ONLINE)) {
235 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
236 removeConnectScheduler();
239 updateStatus(ThingStatus.ONLINE);
241 updateStatus(ThingStatus.UNKNOWN);
242 connectFuture = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0, RETRY_INTERVAL,
249 * uninstall update polling
251 private void uninstallPolling() {
252 final ScheduledFuture<?> pollFuture = this.pollFuture;
253 if (pollFuture != null) {
254 pollFuture.cancel(true);
255 this.pollFuture = null;
260 * install update polling
262 private void installPolling() {
264 pollFuture = scheduler.scheduleWithFixedDelay(this::poll, 0, config.refresh, TimeUnit.SECONDS);