]> git.basschouten.com Git - openhab-addons.git/blob
08817ab175411441859d0937392a570290fbf5ec
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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
73     private static ParadoxIP150BridgeConfiguration config;
74     private @Nullable ScheduledFuture<?> refreshCacheUpdateSchedule;
75
76     private long timeStamp = 0;
77
78     private ScheduledFuture<?> resetScheduleFuture;
79
80     public ParadoxIP150BridgeHandler(Bridge bridge) {
81         super(bridge);
82     }
83
84     @Override
85     public void initialize() {
86         logger.debug("Start initialize()...");
87         updateStatus(ThingStatus.UNKNOWN);
88         logger.debug("Starting creation of communicator.");
89         config = getConfigAs(ParadoxIP150BridgeConfiguration.class);
90
91         scheduler.execute(this::initializeCommunicator);
92         logger.debug("Finished initialize().");
93     }
94
95     private synchronized void initializeCommunicator() {
96         try {
97             String ipAddress = config.getIpAddress();
98             int tcpPort = config.getPort();
99             String ip150Password = config.getIp150Password();
100             String pcPassword = config.getPcPassword();
101             boolean useEncryption = config.isEncrypt();
102
103             // Early exit. If panel type is configured and known to the binding skip auto-detection. Saves one full
104             // initial login process to detect the panel type.
105             PanelType configuredPanelType = PanelType.from(config.getPanelType());
106             if (configuredPanelType != PanelType.UNKNOWN) {
107                 logger.debug("Configuration file has pannelType={}. Skipping Phase1 (Autodiscovery)",
108                         configuredPanelType);
109                 scheduler.schedule(() -> createDiscoveredCommunicatorJob(configuredPanelType), 3, TimeUnit.SECONDS);
110                 return;
111             }
112
113             logger.debug("Phase1 - Auto discover communicator");
114             IParadoxInitialLoginCommunicator initialCommunicator = new GenericCommunicator(ipAddress, tcpPort,
115                     ip150Password, pcPassword, scheduler, useEncryption);
116             initialCommunicator.startLoginSequence();
117
118             timeStamp = System.currentTimeMillis();
119             scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
120         } catch (UnknownHostException e) {
121             logger.warn("Error while starting socket communication. {}", e.getMessage());
122             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123                     "Unknown host. Probably misconfiguration or DNS issue.");
124             throw new ParadoxRuntimeException(e);
125         } catch (IOException e) {
126             logger.warn("Error while starting socket communication. {}", e.getMessage());
127             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
128                     "Error while starting socket communication.");
129             throw new ParadoxRuntimeException(e);
130         }
131     }
132
133     private synchronized void doPostOnlineTask(IParadoxInitialLoginCommunicator initialCommunicator) {
134         if (!initialCommunicator.isOnline()) {
135             if (System.currentTimeMillis() - timeStamp <= ONLINE_WAIT_TRESHOLD_MILLIS) {
136                 scheduler.schedule(() -> doPostOnlineTask(initialCommunicator), 500, TimeUnit.MILLISECONDS);
137                 logger.debug("Communicator not yet online. Rescheduling...");
138             } else {
139                 logger.warn(
140                         "Initial communicator not coming up online for {} seconds. Probably there is something wrong with communication.",
141                         ONLINE_WAIT_TRESHOLD_MILLIS);
142                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
143                         "Error while starting socket communication.");
144             }
145             return;
146         }
147
148         byte[] panelInfoBytes = initialCommunicator.getPanelInfoBytes();
149
150         PanelType panelType = ParadoxInformationConstants.parsePanelType(panelInfoBytes);
151         logger.info("Found {} panel type.", panelType);
152         CommunicationState.logout(initialCommunicator);
153
154         // Wait 3 seconds before we create the discovered communicator so we ensure that the socket is closed
155         // successfully from both ends
156         scheduler.schedule(() -> createDiscoveredCommunicatorJob(panelType), 3, TimeUnit.SECONDS);
157     }
158
159     protected void createDiscoveredCommunicatorJob(PanelType panelType) {
160         // If not detected properly, use the value from config
161         if (panelType == PanelType.UNKNOWN) {
162             panelType = PanelType.from(config.getPanelType());
163         }
164
165         logger.debug("Phase2 - Creating communicator for panel {}", panelType);
166         ICommunicatorBuilder builder = new ParadoxBuilderFactory().createBuilder(panelType);
167         communicator = builder.withIp150Password(config.getIp150Password()).withPcPassword(config.getPcPassword())
168                 .withIpAddress(config.getIpAddress()).withTcpPort(config.getPort())
169                 .withMaxPartitions(config.getMaxPartitions()).withMaxZones(config.getMaxZones())
170                 .withScheduler(scheduler).withEncryption(config.isEncrypt()).build();
171
172         ParadoxPanel panel = ParadoxPanel.getInstance();
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         super.dispose();
212     }
213
214     private void scheduleRefresh() {
215         logger.debug("Scheduling cache update. Refresh interval: {}s. Starts after: {}s.", config.getRefresh(),
216                 INITIAL_SCHEDULE_DELAY_SECONDS);
217         refreshCacheUpdateSchedule = scheduler.scheduleWithFixedDelay(communicator::refreshMemoryMap,
218                 INITIAL_SCHEDULE_DELAY_SECONDS, config.getRefresh(), TimeUnit.SECONDS);
219     }
220
221     private void cancelSchedule(@Nullable ScheduledFuture<?> schedule) {
222         if (schedule != null && !schedule.isCancelled()) {
223             boolean cancelingResult = schedule.cancel(true);
224             String cancelingSuccessful = cancelingResult ? "successful" : "failed";
225             logger.debug("Canceling schedule of {} is {}", schedule, cancelingSuccessful);
226         }
227     }
228
229     @Override
230     public void update() {
231         updateStatusChannel();
232
233         Bridge bridge = getThing();
234         if (bridge.getStatus() == ThingStatus.OFFLINE) {
235             if (communicator.isOnline()) {
236                 updateStatus(ThingStatus.ONLINE);
237             } else {
238                 logger.debug("Bridge {} triggered update but is OFFLINE", bridge.getUID());
239                 return;
240             }
241         }
242
243         List<Thing> things = bridge.getThings();
244         for (Thing thing : things) {
245             ThingHandler handler = thing.getHandler();
246             Channel bridgeChannel = bridge
247                     .getChannel(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID);
248             if (handler != null && bridgeChannel != null) {
249                 handler.handleCommand(bridgeChannel.getUID(), RefreshType.REFRESH);
250             }
251         }
252     }
253
254     @Override
255     public void handleCommand(ChannelUID channelUID, Command command) {
256         logger.debug("Received command {}", command);
257         if (ThingStatus.OFFLINE == getThing().getStatus() && command instanceof RefreshType) {
258             logger.debug("Bridge {} is OFFLINE. Cannot handle refresh commands.", getThing().getUID());
259             return;
260         }
261
262         if (ParadoxAlarmBindingConstants.IP150_COMMUNICATION_COMMAND_CHANNEL_UID.equals(channelUID.getId())) {
263             logger.debug("Command is instance of {}", command.getClass());
264             if (command instanceof StringType) {
265                 String commandAsString = command.toFullString();
266                 if (commandAsString.equals(RESET_COMMAND)) {
267                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
268                             "Bringing bridge offline due to reinitialization of communicator.");
269                     resetCommunicator();
270                 } else {
271                     communicator.executeCommand(commandAsString);
272                 }
273             }
274         }
275
276         if (communicator != null && communicator.isOnline()) {
277             logger.debug("Communicator is online");
278             communicator.refreshMemoryMap();
279             updateStatus(ThingStatus.ONLINE);
280         } else {
281             logger.debug("Communicator is null or not online");
282             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Device is offline");
283         }
284     }
285
286     private void resetCommunicator() {
287         updateStatus(ThingStatus.OFFLINE);
288
289         synchronized (communicator) {
290             if (communicator != null) {
291                 CommunicationState.logout(communicator);
292             }
293
294             initializeCommunicator();
295         }
296
297         resetScheduleFuture = null;
298     }
299
300     @Override
301     protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
302         updateStatusChannel();
303
304         super.updateStatus(status, statusDetail, description);
305
306         if (status.equals(ThingStatus.ONLINE)) {
307             if (refreshCacheUpdateSchedule == null || refreshCacheUpdateSchedule.isDone()) {
308                 scheduleRefresh();
309             }
310         } else {
311             cancelSchedule(refreshCacheUpdateSchedule);
312         }
313     }
314
315     private void updateStatusChannel() {
316         StringType communicatorStatus = communicator != null && communicator.isOnline()
317                 ? ParadoxAlarmBindingConstants.STATE_ONLINE
318                 : ParadoxAlarmBindingConstants.STATE_OFFLINE;
319         updateState(ParadoxAlarmBindingConstants.IP150_COMMUNICATION_STATE_CHANNEL_UID, communicatorStatus);
320     }
321
322     public IParadoxCommunicator getCommunicator() {
323         return communicator;
324     }
325
326     @Override
327     public void onSocketTimeOutOccurred(IOException exception) {
328         logger.warn("TIMEOUT! {} received message for socket timeout. ", this, exception);
329         if (resetScheduleFuture == null) {
330             communicator.executeCommand(IP150Command.LOGOUT.name());
331
332             logger.warn("Reset task is null. Will schedule new task to reset communicator in {} seconds.",
333                     config.getReconnectWaitTime());
334             resetScheduleFuture = scheduler.schedule(this::resetCommunicator, config.getReconnectWaitTime(),
335                     TimeUnit.SECONDS);
336         }
337     }
338
339     @Override
340     public String toString() {
341         return "ParadoxIP150BridgeHandler [" + getThing().getUID().getAsString() + "]";
342     }
343 }