2 * Copyright (c) 2010-2022 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.paradoxalarm.internal.handlers;
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;
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;
53 * The {@link ParadoxIP150BridgeHandler} This is the handler that takes care of communication to/from Paradox alarm
56 * @author Konstantin Polihronov - Initial contribution
58 @SuppressWarnings("null")
60 public class ParadoxIP150BridgeHandler extends BaseBridgeHandler
61 implements IDataUpdateListener, ISocketTimeOutListener {
63 private static final String RESET_COMMAND = "RESET";
65 private static final int ONLINE_WAIT_TRESHOLD_MILLIS = 10000;
67 private static final int INITIAL_SCHEDULE_DELAY_SECONDS = 5;
69 private final Logger logger = LoggerFactory.getLogger(ParadoxIP150BridgeHandler.class);
71 private IParadoxCommunicator communicator;
72 private ParadoxPanel panel = new ParadoxPanel();
74 private ParadoxIP150BridgeConfiguration config;
75 private @Nullable ScheduledFuture<?> refreshCacheUpdateSchedule;
77 private long timeStamp = 0;
79 private ScheduledFuture<?> resetScheduleFuture;
81 public ParadoxIP150BridgeHandler(Bridge bridge) {
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);
92 scheduler.execute(this::initializeCommunicator);
93 logger.debug("Finished initialize().");
96 private synchronized void initializeCommunicator() {
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();
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);
114 logger.debug("Phase1 - Auto discover communicator");
115 IParadoxInitialLoginCommunicator initialCommunicator = new GenericCommunicator(ipAddress, tcpPort,
116 ip150Password, pcPassword, scheduler, useEncryption);
117 initialCommunicator.startLoginSequence();
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);
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...");
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.");
149 byte[] panelInfoBytes = initialCommunicator.getPanelInfoBytes();
151 PanelType panelType = ParadoxInformationConstants.parsePanelType(panelInfoBytes);
152 logger.info("Found {} panel type.", panelType);
153 CommunicationState.logout(initialCommunicator);
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);
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());
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();
173 panel.setCommunicator(communicator);
175 Collection<IDataUpdateListener> listeners = Arrays.asList(panel, this);
176 communicator.setListeners(listeners);
177 communicator.setStoListener(this);
178 logger.debug("Listeners set to: {}", listeners);
180 communicator.startLoginSequence();
182 timeStamp = System.currentTimeMillis();
183 doPostOnlineFinalCommunicatorJob();
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...");
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);
203 logger.debug("Communicator created successfully.");
204 updateStatus(ThingStatus.ONLINE);
208 public void dispose() {
209 cancelSchedule(refreshCacheUpdateSchedule);
210 CommunicationState.logout(communicator);
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);
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);
231 public void update() {
232 updateStatusChannel();
234 Bridge bridge = getThing();
235 if (bridge.getStatus() == ThingStatus.OFFLINE) {
236 if (communicator.isOnline()) {
237 updateStatus(ThingStatus.ONLINE);
239 logger.debug("Bridge {} triggered update but is OFFLINE", bridge.getUID());
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);
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());
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.");
272 communicator.executeCommand(commandAsString);
277 if (communicator != null && communicator.isOnline()) {
278 logger.debug("Communicator is online");
279 communicator.refreshMemoryMap();
280 updateStatus(ThingStatus.ONLINE);
282 logger.debug("Communicator is null or not online");
283 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Device is offline");
287 private void resetCommunicator() {
288 updateStatus(ThingStatus.OFFLINE);
290 synchronized (communicator) {
291 if (communicator != null) {
292 CommunicationState.logout(communicator);
295 initializeCommunicator();
298 resetScheduleFuture = null;
302 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
303 updateStatusChannel();
305 super.updateStatus(status, statusDetail, description);
307 if (status.equals(ThingStatus.ONLINE)) {
308 if (refreshCacheUpdateSchedule == null || refreshCacheUpdateSchedule.isDone()) {
312 cancelSchedule(refreshCacheUpdateSchedule);
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);
323 public IParadoxCommunicator getCommunicator() {
327 public ParadoxPanel getPanel() {
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());
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(),
345 public String toString() {
346 return "ParadoxIP150BridgeHandler [" + getThing().getUID().getAsString() + "]";