/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.fritzboxtr064.internal; import static org.quartz.JobBuilder.newJob; import static org.quartz.JobKey.jobKey; import static org.quartz.TriggerBuilder.newTrigger; import static org.quartz.TriggerKey.triggerKey; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openhab.binding.fritzboxtr064.FritzboxTr064BindingProvider; import org.openhab.binding.fritzboxtr064.internal.FritzboxTr064GenericBindingProvider.FritzboxTr064BindingConfig; import org.openhab.core.events.EventPublisher; import org.openhab.core.items.Item; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.OnOffType; import org.openhab.library.tel.types.CallType; import org.quartz.CronScheduleBuilder; import org.quartz.CronTrigger; import org.quartz.Job; import org.quartz.JobDetail; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /*** * Wrapper class which handles all data/comm. when call monitoing is used * Thread control class * * @author gitbock * @since 1.8.0 */ public class CallMonitor extends Thread { // port number to connect at fbox private final int _DEFAULT_MONITOR_PORT = 1012; // Event Publisher from parent Generic Binding // to be able to pass item updates within this class protected EventPublisher _eventPublisher; // Default openhab Logger protected final static Logger logger = LoggerFactory.getLogger(CallMonitor.class); // Main Monitor Thread receiving fbox messages protected CallMonitorThread _monitorThread; // ip and port to connect protected String _ip; protected int _port; // Phonebook Manager to resolve phone numbers in names protected PhonebookManager _pbm; // Providers to be able to extract all required items private Collection<FritzboxTr064BindingProvider> _providers; protected static CallMonitor _instance; /*** * * @param url from openhab.cfg to connect to fbox * @param ep eventPublisher to pass updates to items * @param providers all items relevant for this binding */ public CallMonitor(String url, EventPublisher ep, Collection<FritzboxTr064BindingProvider> providers, PhonebookManager pbm) { this._eventPublisher = ep; this._ip = parseIpFromUrl(url); this._port = _DEFAULT_MONITOR_PORT; this._providers = providers; this._pbm = pbm; _instance = this; } /*** * In Main Config only the TR064 URL is provided. Need IP for Socket connection. * Parses the IP from URL String * * @param url String * @return IP address from url */ private String parseIpFromUrl(String url) { String ip = ""; Pattern pat = Pattern.compile("(https?://)([^:^/]*)(:\\d*)?(.*)?"); Matcher m = pat.matcher(url); if (m.find()) { ip = m.group(2); } else { logger.error("Cannot get IP from FritzBox URL: {}", url); } return ip; } /*** * reset the connection to fbox periodically */ public void setupReconnectJob() { try { // String cronPattern = "0 0 0 * * ?"; //every day // String cronPattern = "0 * * * * ?"; //every minute String cronPattern = "0 0 0/2 * * ?"; // every 2 hrs Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); JobKey jobKey = jobKey("Reconnect", "FritzBox"); TriggerKey triggerKey = triggerKey("Reconnect", "FritzBox"); if (sched.checkExists(jobKey)) { logger.debug("reconnection job already exists"); } else { CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronPattern); JobDetail job = newJob(ReconnectJob.class).withIdentity(jobKey).build(); CronTrigger trigger = newTrigger().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); sched.scheduleJob(job, trigger); logger.debug("Scheduled reconnection job to FritzBox: {}", cronPattern); } } catch (SchedulerException e) { logger.warn("Could not create daily reconnection job", e); } } /*** * cancel the reconnect job */ public void shutdownReconnectJob() { Scheduler sched = null; try { sched = StdSchedulerFactory.getDefaultScheduler(); JobKey jobKey = jobKey("Reconnect", "FritzBox"); if (sched.checkExists(jobKey)) { logger.debug("Found reconnection job. Shutting down..."); sched.deleteJob(jobKey); } } catch (SchedulerException e) { logger.warn("Error shutting down reconnect job: {}", e.getLocalizedMessage()); } } /** * A quartz scheduler job to simply do a reconnection to the FritzBox. */ public static class ReconnectJob implements Job { @Override public void execute(JobExecutionContext arg0) throws JobExecutionException { Logger logger = LoggerFactory.getLogger(FritzboxTr064Binding.class); logger.info("Fritzbox Reconnect Job executed"); _instance.stopThread(); // create a new thread for listening to the FritzBox _instance._monitorThread = _instance.new CallMonitorThread(); // Wait before reconnect try { sleep(5000L); } catch (InterruptedException e) { } logger.debug("Reconnect Job starts new monitor Thread"); _instance._monitorThread.start(); } } /*** * thread for setting up socket to fbox, listening for messages, parsing them * and updating items. Most of this code is from Kai Kreuzers original * fritzbox binding! * * @author gitbock * */ public class CallMonitorThread extends Thread { /*** * Devnote: * Objects need to be set here, not in parent class! * Otherwise compiler can see them, but at runtime wrong values are given(?) */ // Socket to connect private Socket _socket; // Thread control flag private boolean _interrupted = false; // time to wait before reconnecting private long _reconnectTime = 60000L; public CallMonitorThread() { } @Override public void run() { logger.debug("Callmonitor Thread [{}] is interrupted: {}", Thread.currentThread().getId(), _interrupted); while (!_interrupted) { if (_ip != null) { BufferedReader reader = null; try { logger.info("Callmonitor Thread [{}] attempting connection to FritzBox on {}:{}..", Thread.currentThread().getId(), _ip, _port); _socket = new Socket(_ip, _port); reader = new BufferedReader(new InputStreamReader(_socket.getInputStream())); // reset the retry interval _reconnectTime = 60000L; } catch (Exception e) { logger.warn("Error attempting to connect to FritzBox. Retrying in {}s", _reconnectTime / 1000L, e); try { Thread.sleep(_reconnectTime); } catch (InterruptedException ex) { _interrupted = true; } // wait another more minute the next time _reconnectTime += 60000L; } if (reader != null) { logger.info("Connected to FritzBox on {}:{}", _ip, _port); while (!_interrupted) { try { String line = reader.readLine(); if (line != null) { logger.debug("Received raw call string from fbox: {}", line); CallEvent ce = new CallEvent(line); if (ce.parseRawEvent()) { handleCallEvent(ce); } else { logger.error("Call Event could not be parsed!"); } try { // wait a moment, so that rules can be // processed // see // http://knx-user-forum.de/openhab/25024-bug-im-fritzbox-binding.html sleep(100L); } catch (InterruptedException e) { } } } catch (IOException e) { if (_interrupted) { logger.info("Lost connection to Fritzbox because of interrupt"); } else { logger.error("Lost connection to FritzBox", e); } break; } finally { // allow a few seconds until reconnect. // needed for interrupt state to settle? try { sleep(5000L); } catch (InterruptedException e) { } } } } } } } /** * Handle call event and update item as required * * @param ce call event to process */ private void handleCallEvent(CallEvent ce) { // Always try to resolve number to name. If not wanted return number instead later // pbm can be null, if no item wanted resolving! String callerName = ""; if (_pbm != null) { // resolving caller name if external number is present in call event if (ce.getExternalNo() == null || ce.getExternalNo().isEmpty()) { logger.debug("no external number provided by fbox. Will not resolve name"); } else { logger.debug("resolving name for number {}", ce.getExternalNo()); callerName = _pbm.getNameFromNumber(ce.getExternalNo(), 7); if (callerName == null) { callerName = "Name not found for " + ce.getExternalNo(); // if no match was found, reset to // number } else { logger.debug("external number resolved to: {}", callerName); } } } // cycle through all items logger.debug("Searching item to pass call event: {}", ce.getCallType()); for (FritzboxTr064BindingProvider provider : _providers) { for (String itemName : provider.getItemNames()) { // check each item relevant for this binding FritzboxTr064BindingConfig conf = provider.getBindingConfigByItemName(itemName); // config object // for item Class<? extends Item> itemType = conf.getItemType(); // which type is this item? org.openhab.core.types.State state = null; String configString = conf.getConfigString(); String externalInfo = null; // either name or number as requested by item // number name resolving wanted? if (configString.startsWith("callmonitor") && configString.contains("resolveName")) { logger.debug("name resolving requested in item {}. Setting external no. to", itemName, callerName); externalInfo = callerName; } else { logger.debug("NO name resolving requested in item {}. Setting external no. to", itemName, callerName); externalInfo = ce.getExternalNo(); } if (ce.getCallType().equals("DISCONNECT")) { // 1.12.05⌴12:00:10;DISCONNECT;0;5; // reset states of callmonitor items to 0 for ALL items regardless of type if (configString.startsWith("callmonitor_ringing") || configString.startsWith("callmonitor_active") || configString.startsWith("callmonitor_outgoing")) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.OFF : CallType.EMPTY; } } else if (ce.getCallType().equals("RING")) { // first event when call is incoming // 1.12.05⌴12:00:15;RING;0;5551234;5556789;SIP0; if (configString.startsWith("callmonitor_ringing")) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(ce.getInternalNo(), externalInfo); } } else if (ce.getCallType().equals("CONNECT")) { // when call is answered/running // 1.12.05⌴12:00:05;CONNECT;0;0;0180537489269; if (configString.startsWith("callmonitor_active")) { // only for "active" items state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(externalInfo, ce.getInternalNo()); } } else if (ce.getCallType().equals("CALL")) { // outgoing call // 1.12.05⌴12:00:00;CALL;0;0;5557890;0180537489269;ISDN; if (configString.startsWith("callmonitor_outgoing")) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(externalInfo, ce.getInternalNo()); } } if (state != null) { logger.debug("Dispatching call type {} to item {} as {}", ce.getCallType(), itemName, state.toString()); _eventPublisher.postUpdate(itemName, state); } else { logger.debug("Could not determine state for item {}. Not relevant!", itemName); } } } } /** * Close socket and stop running thread */ @Override public void interrupt() { _interrupted = true; if (_socket != null) { try { _socket.close(); logger.debug("Socket to FritzBox closed"); } catch (IOException e) { logger.warn("Existing connection to FritzBox cannot be closed", e); } } else { logger.debug("Socket to FritzBox not open. Not closing."); } } } public void stopThread() { logger.debug("Stopping monitor Thread..."); if (_monitorThread != null) { _monitorThread.interrupt(); _monitorThread = null; } } public void startThread() { logger.debug("Starting monitor Thread..."); if (_monitorThread != null) { logger.warn("Old monitor Thread was still running"); // let's end the old thread _monitorThread.interrupt(); _monitorThread = null; } // create a new thread for listening to the FritzBox _monitorThread = new CallMonitorThread(); _monitorThread.start(); } }