]> git.basschouten.com Git - openhab-addons.git/blob
58265b0d78f26f567e1d3aab802fe9cf982b9d56
[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.paradoxalarm.internal.handlers;
14
15 import java.io.IOException;
16 import java.net.UnknownHostException;
17 import java.util.Arrays;
18 import java.util.Collection;
19 import java.util.List;
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.paradoxalarm.internal.communication.CommunicationState;
26 import org.openhab.binding.paradoxalarm.internal.communication.GenericCommunicator;
27 import org.openhab.binding.paradoxalarm.internal.communication.ICommunicatorBuilder;
28 import org.openhab.binding.paradoxalarm.internal.communication.IDataUpdateListener;
29 import org.openhab.binding.paradoxalarm.internal.communication.IP150Command;
30 import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator;
31 import org.openhab.binding.paradoxalarm.internal.communication.IParadoxInitialLoginCommunicator;
32 import org.openhab.binding.paradoxalarm.internal.communication.ISocketTimeOutListener;
33 import org.openhab.binding.paradoxalarm.internal.communication.ParadoxBuilderFactory;
34 import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException;
35 import org.openhab.binding.paradoxalarm.internal.model.PanelType;
36 import org.openhab.binding.paradoxalarm.internal.model.ParadoxInformationConstants;
37 import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel;
38 import org.openhab.core.library.types.StringType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandler;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link ParadoxIP150BridgeHandler} This is the handler that takes care of communication to/from Paradox alarm
54  * system.
55  *
56  * @author Konstantin Polihronov - Initial contribution
57  */
58 @SuppressWarnings("null")
59 @NonNullByDefault({})
60 public class ParadoxIP150BridgeHandler extends BaseBridgeHandler
61         implements IDataUpdateListener, ISocketTimeOutListener {
62
63     private static final String RESET_COMMAND = "RESET";
64
65     private static final int ONLINE_WAIT_TRESHOLD_MILLIS = 10000;
66
67     private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
68
69     private final Logger logger = LoggerFactory.getLogger(ParadoxIP150BridgeHandler.class);
70
71     private IParadoxCommunicator communicator;
72     private ParadoxPanel panel = new ParadoxPanel();
73
74     private ParadoxIP150BridgeConfiguration config;
75     private @Nullable ScheduledFuture<?> refreshCacheUpdateSchedule;
76
77     private long timeStamp = 0;
78
79     private ScheduledFuture<?> resetScheduleFuture;
80
81     public ParadoxIP150BridgeHandler(Bridge bridge) {
82         super(bridge);
83     }
84
85     @Override
86     public void initialize() {
87         logger.debug("Start initialize()...");
88         updateStatus(ThingStatus.UNKNOWN);
89         logger.debug("Starting creation of communicator.");
90         config = getConfigAs(ParadoxIP150BridgeConfiguration.class);
91
92         scheduler.execute(this::initializeCommunicator);
93         logger.debug("Finished initialize().");
94     }
95
96     private synchronized void initializeCommunicator() {
97         try {
98             String ipAddress = config.getIpAddress();
99             int tcpPort = config.getPort();
100             String ip150Password = config.getIp150Password();
101             String pcPassword = config.getPcPassword();
102             boolean useEncryption = config.isEncrypt();
103
104             // Early exit. If panel type is configured and known to the binding skip auto-detection. Saves one full
105             // initial login process to detect the panel type.
106             PanelType configuredPanelType = PanelType.from(config.getPanelType());
107             if (configuredPanelType != PanelType.UNKNOWN) {
108                 logger.debug("Configuration file has pannelType={}. Skipping Phase1 (Autodiscovery)",
109                         configuredPanelType);
110                 scheduler.schedule(() -> createDiscoveredCommunicatorJob(configuredPanelType), 3, TimeUnit.SECONDS);
111                 return;
112             }
113
114             logger.debug("Phase1 - Auto discover communicator");
115             IParadoxInitialLoginCommunicator initialCommunicator = new GenericCommunicator(ipAddress, tcpPort,
116                     ip150Password, pcPassword, scheduler, useEncryption);
117             initialCommunicator.startLoginSequence();
118
119             timeStamp = System.currentTimeMillis();
120             scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
121         } catch (UnknownHostException e) {
122             logger.warn("Error while starting socket communication. {}", e.getMessage());
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124                     "Unknown host. Probably misconfiguration or DNS issue.");
125             throw new ParadoxRuntimeException(e);
126         } catch (IOException e) {
127             logger.warn("Error while starting socket communication. {}", e.getMessage());
128             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
129                     "Error while starting socket communication.");
130             throw new ParadoxRuntimeException(e);
131         }
132     }
133
134     private synchronized void doPostOnlineTask(IParadoxInitialLoginCommunicator initialCommunicator) {
135         if (!initialCommunicator.isOnline()) {
136             if (System.currentTimeMillis() - timeStamp <= ONLINE_WAIT_TRESHOLD_MILLIS) {
137                 scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
138                 logger.debug("Communicator not yet online. Rescheduling...");
139             } else {
140                 logger.warn(
141                         "Initial communicator not coming up online for {} seconds. Probably there is something wrong with communication.",
142                         ONLINE_WAIT_TRESHOLD_MILLIS);
143                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
144                         "Error while starting socket communication.");
145             }
146             return;
147         }
148
149         byte[] panelInfoBytes = initialCommunicator.getPanelInfoBytes();
150
151         PanelType panelType = ParadoxInformationConstants.parsePanelType(panelInfoBytes);
152         logger.info("Found {} panel type.", panelType);
153         CommunicationState.logout(initialCommunicator);
154
155         // Wait 3 seconds before we create the discovered communicator so we ensure that the socket is closed
156         // successfully from both ends
157         scheduler.schedule(() -> createDiscoveredCommunicatorJob(panelType), 3, TimeUnit.SECONDS);
158     }
159
160     protected void createDiscoveredCommunicatorJob(PanelType panelType) {
161         // If not detected properly, use the value from config
162         if (panelType == PanelType.UNKNOWN) {
163             panelType = PanelType.from(config.getPanelType());
164         }
165
166         logger.debug("Phase2 - Creating communicator for panel {}", panelType);
167         ICommunicatorBuilder builder = new ParadoxBuilderFactory().createBuilder(panelType);
168         communicator = builder.withIp150Password(config.getIp150Password()).withPcPassword(config.getPcPassword())
169                 .withIpAddress(config.getIpAddress()).withTcpPort(config.getPort())
170                 .withMaxPartitions(config.getMaxPartitions()).withMaxZones(config.getMaxZones())
171                 .withScheduler(scheduler).withEncryption(config.isEncrypt()).build();
172
173         panel.setCommunicator(communicator);
174
175         Collection<IDataUpdateListener> listeners = Arrays.asList(panel, this);
176         communicator.setListeners(listeners);
177         communicator.setStoListener(this);
178         logger.debug("Listeners set to: {}", listeners);
179
180         communicator.startLoginSequence();
181
182         timeStamp = System.currentTimeMillis();
183         doPostOnlineFinalCommunicatorJob();
184     }
185
186     private void doPostOnlineFinalCommunicatorJob() {
187         if (!communicator.isOnline()) {
188             if (System.currentTimeMillis() - timeStamp <= ONLINE_WAIT_TRESHOLD_MILLIS) {
189                 scheduler.schedule(this::doPostOnlineFinalCommunicatorJob, 1, TimeUnit.SECONDS);
190                 logger.debug("Communicator not yet online. Rescheduling...");
191                 return;
192             } else {
193                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                         "Error while starting socket communication.");
195                 ParadoxRuntimeException exception = new ParadoxRuntimeException(
196                         "Communicator didn't go online in defined treshold time. " + ONLINE_WAIT_TRESHOLD_MILLIS
197                                 + "sec. You can still try to reset the bridge with command RESET.");
198                 logger.debug("Problem with communication", exception);
199                 throw exception;
200             }
201         }
202
203         logger.debug("Communicator created successfully.");
204         updateStatus(ThingStatus.ONLINE);
205     }
206
207     @Override
208     public void dispose() {
209         cancelSchedule(refreshCacheUpdateSchedule);
210         CommunicationState.logout(communicator);
211         panel.dispose();
212         super.dispose();
213     }
214
215     private void scheduleRefresh() {
216         logger.debug("Scheduling cache update. Refresh interval: {}s. Starts after: {}s.", config.getRefresh(),
217                 INITIAL_SCHEDULE_DELAY_SECONDS);
218         refreshCacheUpdateSchedule = scheduler.scheduleWithFixedDelay(communicator::refreshMemoryMap,
219                 INITIAL_SCHEDULE_DELAY_SECONDS, config.getRefresh(), TimeUnit.SECONDS);
220     }
221
222     private void cancelSchedule(@Nullable ScheduledFuture<?> schedule) {
223         if (schedule != null && !schedule.isCancelled()) {
224             boolean cancelingResult = schedule.cancel(true);
225             String cancelingSuccessful = cancelingResult ? "successful" : "failed";
226             logger.debug("Canceling schedule of {} is {}", schedule, cancelingSuccessful);
227         }
228     }
229
230     @Override
231     public void update() {
232         updateStatusChannel();
233
234         Bridge bridge = getThing();
235         if (bridge.getStatus() == ThingStatus.OFFLINE) {
236             if (communicator.isOnline()) {
237                 updateStatus(ThingStatus.ONLINE);
238             } else {
239                 logger.debug("Bridge {} triggered update but is OFFLINE", bridge.getUID());
240                 return;
241             }
242         }
243
244         List<Thing> things = bridge.getThings();
245         for (Thing thing : things) {
246             ThingHandler handler = thing.getHandler();
247             Channel bridgeChannel = bridge
248                     .getChannel(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID);
249             if (handler != null && bridgeChannel != null) {
250                 handler.handleCommand(bridgeChannel.getUID(), RefreshType.REFRESH);
251             }
252         }
253     }
254
255     @Override
256     public void handleCommand(ChannelUID channelUID, Command command) {
257         logger.debug("Received command {}", command);
258         if (ThingStatus.OFFLINE == getThing().getStatus() && command instanceof RefreshType) {
259             logger.debug("Bridge {} is OFFLINE. Cannot handle refresh commands.", getThing().getUID());
260             return;
261         }
262
263         if (ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID.equals(channelUID.getId())) {
264             logger.debug("Command is instance of {}", command.getClass());
265             if (command instanceof StringType) {
266                 String commandAsString = command.toFullString();
267                 if (commandAsString.equals(RESET_COMMAND)) {
268                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
269                             "Bringing bridge offline due to reinitialization of communicator.");
270                     resetCommunicator();
271                 } else {
272                     communicator.executeCommand(commandAsString);
273                 }
274             }
275         }
276
277         if (communicator != null && communicator.isOnline()) {
278             logger.debug("Communicator is online");
279             communicator.refreshMemoryMap();
280             updateStatus(ThingStatus.ONLINE);
281         } else {
282             logger.debug("Communicator is null or not online");
283             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Device is offline");
284         }
285     }
286
287     private void resetCommunicator() {
288         updateStatus(ThingStatus.OFFLINE);
289
290         synchronized (communicator) {
291             if (communicator != null) {
292                 CommunicationState.logout(communicator);
293             }
294
295             initializeCommunicator();
296         }
297
298         resetScheduleFuture = null;
299     }
300
301     @Override
302     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
303         updateStatusChannel();
304
305         super.updateStatus(status, statusDetail, description);
306
307         if (status.equals(ThingStatus.ONLINE)) {
308             if (refreshCacheUpdateSchedule == null || refreshCacheUpdateSchedule.isDone()) {
309                 scheduleRefresh();
310             }
311         } else {
312             cancelSchedule(refreshCacheUpdateSchedule);
313         }
314     }
315
316     private void updateStatusChannel() {
317         StringType communicatorStatus = communicator != null && communicator.isOnline()
318                 ? ParadoxAlarmBindingConstants.STATE_ONLINE
319                 : ParadoxAlarmBindingConstants.STATE_OFFLINE;
320         updateState(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_STATE_CHANNEL_UID, communicatorStatus);
321     }
322
323     public IParadoxCommunicator getCommunicator() {
324         return communicator;
325     }
326
327     public ParadoxPanel getPanel() {
328         return panel;
329     }
330
331     @Override
332     public void onSocketTimeOutOccurred(IOException exception) {
333         logger.warn("TIMEOUT! {} received message for socket timeout. ", this, exception);
334         if (resetScheduleFuture == null) {
335             communicator.executeCommand(IP150Command.LOGOUT.name());
336
337             logger.warn("Reset task is null. Will schedule new task to reset communicator in {} seconds.",
338                     config.getReconnectWaitTime());
339             resetScheduleFuture = scheduler.schedule(this::resetCommunicator, config.getReconnectWaitTime(),
340                     TimeUnit.SECONDS);
341         }
342     }
343
344     @Override
345     public String toString() {
346         return "ParadoxIP150BridgeHandler [" + getThing().getUID().getAsString() + "]";
347     }
348 }