]> git.basschouten.com Git - openhab-addons.git/blob
0cb587d11992d90a6992780e66b40745fc254004
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.resol.handler;
14
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;
20 import java.util.Set;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
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;
41
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;
51
52 /**
53  * The {@link ResolBridgeHandler} class handles the connection to the VBUS/LAN adapter.
54  *
55  * @author Raphael Mack - Initial contribution
56  */
57 @NonNullByDefault
58 public class ResolBridgeHandler extends BaseBridgeHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(ResolBridgeHandler.class);
61
62     private String ipAddress = "";
63     private String password = "";
64     private int refreshInterval;
65     private boolean isConnected = false;
66     private String unconnectedReason = "";
67
68     // Background Runnable
69     private @Nullable ScheduledFuture<?> pollingJob;
70
71     private @Nullable Connection tcpConnection;
72     private final Specification spec;
73
74     // Managing Thing Discovery Service
75     private @Nullable ResolDeviceDiscoveryService discoveryService = null;
76
77     private boolean scanning;
78
79     private final @Nullable LocaleProvider localeProvider;
80
81     public ResolBridgeHandler(Bridge bridge, @Nullable LocaleProvider localeProvider) {
82         super(bridge);
83         spec = Specification.getDefaultSpecification();
84         this.localeProvider = localeProvider;
85     }
86
87     public void updateStatus() {
88         if (isConnected) {
89             updateStatus(ThingStatus.ONLINE);
90         } else {
91             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, unconnectedReason);
92         }
93     }
94
95     public void registerDiscoveryService(ResolDeviceDiscoveryService discoveryService) {
96         this.discoveryService = discoveryService;
97     }
98
99     public void unregisterDiscoveryService() {
100         discoveryService = null;
101     }
102
103     @Override
104     public Collection<Class<? extends ThingHandlerService>> getServices() {
105         return Set.of(ResolDeviceDiscoveryService.class);
106     }
107
108     public void registerResolThingListener(ResolEmuEMThingHandler resolEmuEMThingHandler) {
109         synchronized (this) {
110             Connection con = tcpConnection;
111             if (con != null) {
112                 resolEmuEMThingHandler.useConnection(con);
113             }
114         }
115     }
116
117     private void pollingRunnable() {
118         if (!isConnected) {
119             synchronized (ResolBridgeHandler.this) {
120                 Connection connection = tcpConnection;
121                 /* first cleanup in case there is an old but failed TCP connection around */
122                 try {
123                     if (connection != null) {
124                         connection.disconnect();
125
126                         getThing().getThings().stream().forEach(thing -> {
127                             ThingHandler th = thing.getHandler();
128                             if (th instanceof ResolEmuEMThingHandler resolEmuEMThingHandler) {
129                                 resolEmuEMThingHandler.stop();
130                             }
131                         });
132
133                         connection = null;
134                         tcpConnection = null;
135                     }
136                 } catch (IOException e) {
137                     logger.warn("TCP disconnect failed: {}", e.getMessage());
138                 }
139                 TcpDataSource source = null;
140                 /* now try to establish a new TCP connection */
141                 try {
142                     source = TcpDataSourceProvider.fetchInformation(InetAddress.getByName(ipAddress), 500);
143                     if (source != null) {
144                         source.setLivePassword(password);
145                     }
146                 } catch (IOException e) {
147                     isConnected = false;
148                     unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
149                 }
150                 if (source != null) {
151                     try {
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
158                         // generic type
159                         isConnected = false;
160                         unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
161                     }
162
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());
167                     }
168                 }
169                 // Establish the connection
170                 if (connection != null) {
171                     try {
172                         connection.connect();
173                         final Connection c = connection;
174                         // now set the connection the thing handlers for the emulated EMs
175
176                         getThing().getThings().stream().forEach(thing -> {
177                             ThingHandler th = thing.getHandler();
178                             if (th instanceof ResolEmuEMThingHandler resolEmuEMThingHandler) {
179                                 resolEmuEMThingHandler.useConnection(c);
180                             }
181                         });
182                     } catch (IOException e) {
183                         unconnectedReason = Objects.requireNonNullElse(e.getMessage(), "");
184                         isConnected = false;
185                     }
186                 } else {
187                     isConnected = false;
188                 }
189                 if (!isConnected) {
190                     logger.debug("Cannot establish connection to {} ({})", ipAddress, unconnectedReason);
191                 } else {
192                     unconnectedReason = "";
193                 }
194                 updateStatus();
195             }
196         }
197     }
198
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);
203         }
204     }
205
206     public ThingStatus getStatus() {
207         return getThing().getStatus();
208     }
209
210     @Override
211     public void handleCommand(ChannelUID channelUID, Command command) {
212         // No commands supported - nothing to do
213     }
214
215     @Override
216     public void initialize() {
217         updateStatus();
218         ResolBridgeConfiguration configuration = getConfigAs(ResolBridgeConfiguration.class);
219         ipAddress = configuration.ipAddress;
220         refreshInterval = configuration.refreshInterval;
221         password = configuration.password;
222         startAutomaticRefresh();
223     }
224
225     @Override
226     public void dispose() {
227         ScheduledFuture<?> job = pollingJob;
228         if (job != null) {
229             job.cancel(true);
230             pollingJob = null;
231         }
232         try {
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();
240                     }
241                 });
242             }
243         } catch (IOException ioe) {
244             // we don't care about exceptions on disconnect in dispose
245         }
246     }
247
248     Locale getLocale() {
249         if (localeProvider != null) {
250             return localeProvider.getLocale();
251         } else {
252             return Locale.getDefault();
253         }
254     }
255
256     /* adapter to react on connection state changes and handle received packets */
257     private class ResolConnectorAdapter extends ConnectionAdapter {
258         @Override
259         public void connectionStateChanged(@Nullable Connection connection) {
260             synchronized (ResolBridgeHandler.this) {
261                 if (connection == null) {
262                     isConnected = false;
263                 } else {
264                     ConnectionState connState = connection.getConnectionState();
265                     if (ConnectionState.CONNECTED.equals(connState)) {
266                         isConnected = true;
267                     } else if (ConnectionState.DISCONNECTED.equals(connState)
268                             || ConnectionState.INTERRUPTED.equals(connState)) {
269                         isConnected = false;
270                     }
271                     logger.debug("Connection state changed to: {}", connState.toString());
272
273                     if (isConnected) {
274                         unconnectedReason = "";
275                     } else {
276                         unconnectedReason = "TCP connection failed: " + connState.toString();
277                     }
278                 }
279                 updateStatus();
280             }
281         }
282
283         @Override
284         public void packetReceived(@Nullable Connection connection, @Nullable Packet packet) {
285             if (connection == null || packet == null) {
286                 return;
287             }
288             Language lang = SpecificationFile.getLanguageForLocale(getLocale());
289             boolean packetHandled = false;
290             String thingType = spec.getSourceDeviceSpec(packet).getName(); // use En here
291
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_-]+", "_");
298
299             /*
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.
303              */
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);
309             }
310
311             for (Thing t : getThing().getThings()) {
312                 ResolBaseThingHandler th = (ResolBaseThingHandler) t.getHandler();
313                 boolean isEM = t instanceof ResolEmuEMThingHandler;
314
315                 if (t.getUID().getId().contentEquals(thingType)
316                         || (isEM && th != null && spec.getSourceDeviceSpec(packet)
317                                 .getPeerAddress() == ((ResolEmuEMThingHandler) th).getVbusAddress())) {
318                     if (th != null) {
319                         th.packetReceived(spec, lang, packet);
320                         packetHandled = true;
321                     }
322                 }
323             }
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));
329             }
330         }
331     }
332
333     public void startScan() {
334         scanning = true;
335     }
336
337     public void stopScan() {
338         scanning = false;
339     }
340 }