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.resol.handler;
15 import java.io.IOException;
16 import java.net.InetAddress;
17 import java.util.Collection;
18 import java.util.Locale;
19 import java.util.Objects;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.resol.internal.ResolBindingConstants;
27 import org.openhab.binding.resol.internal.ResolBridgeConfiguration;
28 import org.openhab.binding.resol.internal.discovery.ResolDeviceDiscoveryService;
29 import org.openhab.core.i18n.LocaleProvider;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.binding.BaseBridgeHandler;
36 import org.openhab.core.thing.binding.ThingHandler;
37 import org.openhab.core.thing.binding.ThingHandlerService;
38 import org.openhab.core.types.Command;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import de.resol.vbus.Connection;
43 import de.resol.vbus.Connection.ConnectionState;
44 import de.resol.vbus.ConnectionAdapter;
45 import de.resol.vbus.Packet;
46 import de.resol.vbus.Specification;
47 import de.resol.vbus.SpecificationFile;
48 import de.resol.vbus.SpecificationFile.Language;
49 import de.resol.vbus.TcpDataSource;
50 import de.resol.vbus.TcpDataSourceProvider;
53 * The {@link ResolBridgeHandler} class handles the connection to the VBUS/LAN adapter.
55 * @author Raphael Mack - Initial contribution
58 public class ResolBridgeHandler extends BaseBridgeHandler {
60 private final Logger logger = LoggerFactory.getLogger(ResolBridgeHandler.class);
62 private String ipAddress = "";
63 private String password = "";
64 private int refreshInterval;
65 private boolean isConnected = false;
66 private String unconnectedReason = "";
68 // Background Runnable
69 private @Nullable ScheduledFuture<?> pollingJob;
71 private @Nullable Connection tcpConnection;
72 private final Specification spec;
74 // Managing Thing Discovery Service
75 private @Nullable ResolDeviceDiscoveryService discoveryService = null;
77 private boolean scanning;
79 private final @Nullable LocaleProvider localeProvider;
81 public ResolBridgeHandler(Bridge bridge, @Nullable LocaleProvider localeProvider) {
83 spec = Specification.getDefaultSpecification();
84 this.localeProvider = localeProvider;
87 public void updateStatus() {
89 updateStatus(ThingStatus.ONLINE);
91 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, unconnectedReason);
95 public void registerDiscoveryService(ResolDeviceDiscoveryService discoveryService) {
96 this.discoveryService = discoveryService;
99 public void unregisterDiscoveryService() {
100 discoveryService = null;
104 public Collection<Class<? extends ThingHandlerService>> getServices() {
105 return Set.of(ResolDeviceDiscoveryService.class);
108 public void registerResolThingListener(ResolEmuEMThingHandler resolEmuEMThingHandler) {
109 synchronized (this) {
110 Connection con = tcpConnection;
112 resolEmuEMThingHandler.useConnection(con);
117 private void pollingRunnable() {
119 synchronized (ResolBridgeHandler.this) {
120 Connection connection = tcpConnection;
121 /* first cleanup in case there is an old but failed TCP connection around */
123 if (connection != null) {
124 connection.disconnect();
126 getThing().getThings().stream().forEach(thing -> {
127 ThingHandler th = thing.getHandler();
128 if (th instanceof ResolEmuEMThingHandler resolEmuEMThingHandler) {
129 resolEmuEMThingHandler.stop();
134 tcpConnection = null;
136 } catch (IOException e) {
137 logger.warn("TCP disconnect failed: {}", e.getMessage());
139 TcpDataSource source = null;
140 /* now try to establish a new TCP connection */
142 source = TcpDataSourceProvider.fetchInformation(InetAddress.getByName(ipAddress), 500);
143 if (source != null) {
144 source.setLivePassword(password);
146 } catch (IOException e) {
148 unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
150 if (source != null) {
152 logger.debug("Opening a new connection to {} {} @{}", source.getProduct(),
153 source.getDeviceName(), source.getAddress());
154 connection = source.connectLive(0, 0x0020);
155 tcpConnection = connection;
156 } catch (Exception e) {
157 // this generic Exception catch is required, as TcpDataSource.connectLive throws this
160 unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
163 if (connection != null) {
164 // Add a listener to the Connection to monitor state changes and
165 // read incoming frames
166 connection.addListener(new ResolConnectorAdapter());
169 // Establish the connection
170 if (connection != null) {
172 connection.connect();
173 final Connection c = connection;
174 // now set the connection the thing handlers for the emulated EMs
176 getThing().getThings().stream().forEach(thing -> {
177 ThingHandler th = thing.getHandler();
178 if (th instanceof ResolEmuEMThingHandler resolEmuEMThingHandler) {
179 resolEmuEMThingHandler.useConnection(c);
182 } catch (IOException e) {
183 unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
190 logger.debug("Cannot establish connection to {} ({})", ipAddress, unconnectedReason);
192 unconnectedReason = "";
199 private synchronized void startAutomaticRefresh() {
200 ScheduledFuture<?> job = pollingJob;
201 if (job == null || job.isCancelled()) {
202 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval, TimeUnit.SECONDS);
206 public ThingStatus getStatus() {
207 return getThing().getStatus();
211 public void handleCommand(ChannelUID channelUID, Command command) {
212 // No commands supported - nothing to do
216 public void initialize() {
218 ResolBridgeConfiguration configuration = getConfigAs(ResolBridgeConfiguration.class);
219 ipAddress = configuration.ipAddress;
220 refreshInterval = configuration.refreshInterval;
221 password = configuration.password;
222 startAutomaticRefresh();
226 public void dispose() {
227 ScheduledFuture<?> job = pollingJob;
233 Connection connection = tcpConnection;
234 if (connection != null) {
235 connection.disconnect();
236 getThing().getThings().stream().forEach(thing -> {
237 ThingHandler th = thing.getHandler();
238 if (th instanceof ResolEmuEMThingHandler resolEmuEMThingHandler) {
239 resolEmuEMThingHandler.stop();
243 } catch (IOException ioe) {
244 // we don't care about exceptions on disconnect in dispose
249 if (localeProvider != null) {
250 return localeProvider.getLocale();
252 return Locale.getDefault();
256 /* adapter to react on connection state changes and handle received packets */
257 private class ResolConnectorAdapter extends ConnectionAdapter {
259 public void connectionStateChanged(@Nullable Connection connection) {
260 synchronized (ResolBridgeHandler.this) {
261 if (connection == null) {
264 ConnectionState connState = connection.getConnectionState();
265 if (ConnectionState.CONNECTED.equals(connState)) {
267 } else if (ConnectionState.DISCONNECTED.equals(connState)
268 || ConnectionState.INTERRUPTED.equals(connState)) {
271 logger.debug("Connection state changed to: {}", connState.toString());
274 unconnectedReason = "";
276 unconnectedReason = "TCP connection failed: " + connState.toString();
284 public void packetReceived(@Nullable Connection connection, @Nullable Packet packet) {
285 if (connection == null || packet == null) {
288 Language lang = SpecificationFile.getLanguageForLocale(getLocale());
289 boolean packetHandled = false;
290 String thingType = spec.getSourceDeviceSpec(packet).getName(); // use En here
292 thingType = thingType.replace(" [", "-");
293 thingType = thingType.replace("]", "");
294 thingType = thingType.replace(" #", "-");
295 thingType = thingType.replace(" ", "_");
296 thingType = thingType.replace("/", "_");
297 thingType = thingType.replaceAll("[^A-Za-z0-9_-]+", "_");
300 * It would be nice for the combination of MX and EM devices to filter only those with a peerAddress of
301 * 0x10, because the MX redelivers the data from the EM to the DFA.
302 * But the MX is the exception in this case and many other controllers do not redeliver data, so we keep it.
304 if (logger.isTraceEnabled()) {
305 logger.trace("Received Data from {} (0x{}/0x{}) naming it {}",
306 spec.getSourceDeviceSpec(packet).getName(lang),
307 Integer.toHexString(spec.getSourceDeviceSpec(packet).getSelfAddress()),
308 Integer.toHexString(spec.getSourceDeviceSpec(packet).getPeerAddress()), thingType);
311 for (Thing t : getThing().getThings()) {
312 ResolBaseThingHandler th = (ResolBaseThingHandler) t.getHandler();
313 boolean isEM = t instanceof ResolEmuEMThingHandler;
315 if (t.getUID().getId().contentEquals(thingType)
316 || (isEM && th != null && spec.getSourceDeviceSpec(packet)
317 .getPeerAddress() == ((ResolEmuEMThingHandler) th).getVbusAddress())) {
319 th.packetReceived(spec, lang, packet);
320 packetHandled = true;
324 ResolDeviceDiscoveryService discovery = discoveryService;
325 if (!packetHandled && scanning && discovery != null) {
326 // register the seen device
327 discovery.addThing(getThing().getUID(), ResolBindingConstants.THING_ID_DEVICE, thingType,
328 spec.getSourceDeviceSpec(packet).getName(lang));
333 public void startScan() {
337 public void stopScan() {