/** * 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.fritzbox.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.Dictionary; import java.util.HashMap; import java.util.Objects; import org.apache.commons.lang.StringUtils; import org.apache.commons.net.telnet.TelnetClient; import org.openhab.binding.fritzbox.FritzboxBindingProvider; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.binding.BindingProvider; import org.openhab.core.events.EventPublisher; import org.openhab.core.items.Item; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.items.StringItem; import org.openhab.core.library.items.SwitchItem; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; import org.openhab.library.tel.types.CallType; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; 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; /** * The FritzBox binding connects to a AVM FritzBox on the monitor port 1012 and * listens to event notifications from this box. There are event for incoming * and outgoing calls, as well as for connections and disconnections. * * @author Kai Kreuzer * @author Jan N. Klug * @since 0.7.0 */ public class FritzboxBinding extends AbstractActiveBinding<FritzboxBindingProvider> implements ManagedService { private static HashMap<String, String> commandMap = new HashMap<String, String>(); private static HashMap<String, String> queryMap = new HashMap<String, String>(); // TODO: configurable? // daily cron schedule private final String cronSchedule = "0 0 0 * * ?"; static { commandMap.put(FritzboxBindingProvider.TYPE_DECT, "ctlmgr_ctl w dect settings/enabled"); commandMap.put(FritzboxBindingProvider.TYPE_WLAN, "ctlmgr_ctl w wlan settings/ap_enabled"); commandMap.put(FritzboxBindingProvider.TYPE_GUEST_WLAN, "ctlmgr_ctl w wlan settings/guest_ap_enabled"); queryMap.put(FritzboxBindingProvider.TYPE_DECT, "ctlmgr_ctl r dect settings/enabled"); queryMap.put(FritzboxBindingProvider.TYPE_WLAN, "ctlmgr_ctl r wlan settings/ap_enabled"); queryMap.put(FritzboxBindingProvider.TYPE_GUEST_WLAN, "ctlmgr_ctl r wlan settings/guest_ap_enabled"); } @Override public void bindingChanged(BindingProvider provider, String itemName) { super.bindingChanged(provider, itemName); conditionalDeActivate(); } private void conditionalDeActivate() { logger.info("Fritzbox conditional deActivate: {}", bindingsExist()); if (bindingsExist()) { activate(); } else { deactivate(); } } private static final Logger logger = LoggerFactory.getLogger(FritzboxBinding.class); protected static final int MONITOR_PORT = 1012; /** the current thread instance that is listening to the FritzBox */ protected static MonitorThread monitorThread = null; /* The IP address to connect to */ protected static String ip; /* The password of the FritzBox to access via Telnet */ protected static String password; /* The username, if used for telnet connections */ protected static String username; /** * Reference to this instance to be used with the reconnection job which is * static. */ private static FritzboxBinding INSTANCE; public FritzboxBinding() { INSTANCE = this; } @Override public void activate() { super.activate(); setProperlyConfigured(true); // if bundle is already configured, launch the monitor thread right away if (ip != null) { reconnect(); } } @Override public void deactivate() { if (monitorThread != null) { monitorThread.interrupt(); } monitorThread = null; } @Override public void internalReceiveCommand(String itemName, Command command) { if (password != null && !password.isEmpty()) { String type = null; for (FritzboxBindingProvider provider : providers) { type = provider.getType(itemName); if (type != null) { break; } } logger.info("Fritzbox type: {}", type); if (type == null) { return; } TelnetCommandThread thread = new TelnetCommandThread(type, command); thread.start(); } } protected void addBindingProvider(FritzboxBindingProvider bindingProvider) { super.addBindingProvider(bindingProvider); } protected void removeBindingProvider(FritzboxBindingProvider bindingProvider) { super.removeBindingProvider(bindingProvider); } /** * {@inheritDoc} */ @Override @SuppressWarnings("rawtypes") public void updated(Dictionary config) throws ConfigurationException { if (config != null) { String ip = Objects.toString(config.get("ip"), null); if (StringUtils.isNotBlank(ip)) { if (!ip.equals(FritzboxBinding.ip)) { // only do something if the ip has changed FritzboxBinding.ip = ip; conditionalDeActivate(); // schedule a daily reconnection as sometimes the FritzBox // stops sending data // and thus blocks the monitor thread try { Scheduler sched = StdSchedulerFactory.getDefaultScheduler(); JobKey jobKey = jobKey("Reconnect", "FritzBox"); TriggerKey triggerKey = triggerKey("Reconnect", "FritzBox"); if (sched.checkExists(jobKey)) { logger.debug("Daily reconnection job already exists"); } else { CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronSchedule); JobDetail job = newJob(ReconnectJob.class).withIdentity(jobKey).build(); CronTrigger trigger = newTrigger().withIdentity(triggerKey).withSchedule(scheduleBuilder) .build(); sched.scheduleJob(job, trigger); logger.debug("Scheduled a daily reconnection to FritzBox on {}:{}", ip, MONITOR_PORT); } } catch (SchedulerException e) { logger.warn("Could not create daily reconnection job", e); } } } String password = Objects.toString(config.get("password"), null); if (StringUtils.isNotBlank(password)) { FritzboxBinding.password = password; } String username = Objects.toString(config.get("user"), null); if (StringUtils.isNotBlank(username)) { FritzboxBinding.username = username; } } } protected void reconnect() { if (monitorThread != null) { // let's end the old thread monitorThread.interrupt(); monitorThread = null; } // create a new thread for listening to the FritzBox monitorThread = new MonitorThread(this.eventPublisher, this.providers); monitorThread.start(); } private static class TelnetCommandThread extends Thread { private static HashMap<String, String> commandMap = new HashMap<String, String>(); static { commandMap.put(FritzboxBindingProvider.TYPE_DECT, "ctlmgr_ctl w dect settings/enabled"); commandMap.put(FritzboxBindingProvider.TYPE_WLAN, "ctlmgr_ctl w wlan settings/ap_enabled"); commandMap.put(FritzboxBindingProvider.TYPE_GUEST_WLAN, "ctlmgr_ctl w wlan settings/guest_ap_enabled"); } public TelnetCommandThread(String type, Command command) { super(); this.type = type; this.command = command; } private String type; private Command command; @Override public void run() { try { TelnetClient client = new TelnetClient(); client.connect(ip); int state = 0; if (command == OnOffType.ON) { state = 1; } String cmdString = null; if (commandMap.containsKey(type)) { cmdString = commandMap.get(type) + " " + state; } else if (type.startsWith("tam")) { cmdString = "ctlmgr_ctl w tam settings/" + type.toUpperCase() + "/Active " + state; } else if (type.startsWith("cmd")) { int on = type.indexOf("ON="); int off = type.indexOf("OFF="); if (state == 0) { cmdString = type.substring(off + 4, on < off ? type.length() : on); } else { cmdString = type.substring(on + 3, off < on ? type.length() : off); } cmdString = cmdString.trim(); } /* * This is a approach with receive/send in serial way. This * could be done via a sperate thread but for just sending one * command it is not necessary */ if (username != null) { receive(client); // user: send(client, username); } receive(client); // password: send(client, password); receive(client); // welcome text send(client, cmdString); Thread.sleep(1000L); // response not needed - may be interesting // for reading status client.disconnect(); } catch (Exception e) { logger.warn("Error processing command", e); } } private void send(TelnetClient client, String data) { logger.trace("Sending data ({})...", data); try { data += "\r\n"; client.getOutputStream().write(data.getBytes()); client.getOutputStream().flush(); } catch (IOException e) { logger.warn("Error sending data", e); } } private String receive(TelnetClient client) { StringBuffer strBuffer; try { strBuffer = new StringBuffer(); byte[] buf = new byte[4096]; int len = 0; Thread.sleep(750L); while ((len = client.getInputStream().read(buf)) != 0) { strBuffer.append(new String(buf, 0, len)); Thread.sleep(750L); if (client.getInputStream().available() == 0) { break; } } return strBuffer.toString(); } catch (Exception e) { logger.warn("Error receiving data", e); } return null; } } /** * This is the thread that does the real work * * @author Kai Kreuzer * */ private static class MonitorThread extends Thread { /** the active TCP connection */ private Socket connection; /** flag to notify the thread to terminate */ private boolean interrupted = false; /** retry interval in ms, if connection fails */ private long waitBeforeRetry = 60000L; private EventPublisher eventPublisher; private Collection<FritzboxBindingProvider> providers; public MonitorThread(EventPublisher eventPublisher, Collection<FritzboxBindingProvider> providers) { this.eventPublisher = eventPublisher; this.providers = providers; } /** * Notifies the thread to terminate itself. The current connection will * be closed. */ @Override public void interrupt() { this.interrupted = true; if (connection != null) { try { connection.close(); } catch (IOException e) { logger.warn("Existing connection to FritzBox cannot be closed", e); } } } /** * {@inheritDoc} */ @Override public void run() { while (!interrupted) { while (ip == null) { // if we don't have an IP, let's wait try { sleep(1000L); } catch (InterruptedException e) { interrupted = true; break; } } if (ip != null) { BufferedReader reader = null; try { logger.info("Attempting connection to FritzBox on {}:{}...", ip, MONITOR_PORT); connection = new Socket(ip, MONITOR_PORT); reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); // reset the retry interval waitBeforeRetry = 60000L; } catch (Exception e) { logger.warn("Error attempting to connect to FritzBox. Retrying in " + waitBeforeRetry / 1000L + "s.", e); try { Thread.sleep(waitBeforeRetry); } catch (InterruptedException ex) { interrupted = true; } // wait another more minute the next time waitBeforeRetry += 60000L; } if (reader != null) { logger.info("Connected to FritzBox on {}:{}", ip, MONITOR_PORT); while (!interrupted) { try { String line = reader.readLine(); if (line != null) { MonitorEvent event = parseMonitorEvent(line); processMonitorEvent(event); 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; } } } } } } /** * Parses the string that was received from the FritzBox * * @param line * the received string * @return the parse result */ private MonitorEvent parseMonitorEvent(String line) { String[] sections = line.split(";"); MonitorEvent event = new MonitorEvent(); event.timestamp = sections[0]; event.eventType = sections[1]; event.connectionId = sections[2]; if (event.eventType.equals("RING")) { event.externalNo = sections[3]; event.internalNo = sections[4]; event.connectionType = sections[5]; } else if (event.eventType.equals("CONNECT")) { event.line = sections[3]; event.externalNo = sections[4]; } else if (event.eventType.equals("CALL")) { event.line = sections[3]; event.internalNo = sections[4]; event.externalNo = sections[5]; event.connectionType = sections[6]; } return event; } /** * Processes a monitor event. * * @param event * the event to process */ private void processMonitorEvent(MonitorEvent event) { if (event.eventType.equals("RING")) { handleEventType(event, FritzboxBindingProvider.TYPE_INBOUND); } if (event.eventType.equals("CALL")) { handleEventType(event, FritzboxBindingProvider.TYPE_OUTBOUND); } if (event.eventType.equals("CONNECT") || event.eventType.equals("DISCONNECT")) { handleEventType(event, FritzboxBindingProvider.TYPE_INBOUND); handleEventType(event, FritzboxBindingProvider.TYPE_ACTIVE); handleEventType(event, FritzboxBindingProvider.TYPE_OUTBOUND); } } /** * Processes a monitor event for a given binding type * * @param event * the monitor event to process * @param bindingType * the binding type of the items to process */ private void handleEventType(MonitorEvent event, String bindingType) { for (FritzboxBindingProvider provider : providers) { for (String itemName : provider.getItemNamesForType(bindingType)) { Class<? extends Item> itemType = provider.getItemType(itemName); org.openhab.core.types.State state = null; if (event.eventType.equals("DISCONNECT")) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.OFF : CallType.EMPTY; } else if (event.eventType.equals("CONNECT")) { if (bindingType.equals(FritzboxBindingProvider.TYPE_ACTIVE)) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(event.externalNo, event.line); } else { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.OFF : CallType.EMPTY; } } else if (event.eventType.equals("RING") && bindingType.equals(FritzboxBindingProvider.TYPE_INBOUND)) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(event.externalNo, event.internalNo); } else if (event.eventType.equals("CALL") && bindingType.equals(FritzboxBindingProvider.TYPE_OUTBOUND)) { state = itemType.isAssignableFrom(SwitchItem.class) ? OnOffType.ON : new CallType(event.internalNo, event.externalNo); } if (state != null) { eventPublisher.postUpdate(itemName, state); } } } } /** * Class representing a monitor event received from the FritzBox. Not * all attributes are used for the moment, but might be useful for * future extensions. * * @author Kai Kreuzer * */ @SuppressWarnings("unused") private static class MonitorEvent { public String timestamp; public String eventType; public String connectionId; public String externalNo; public String internalNo; public String connectionType; public String line; } } /** * 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 { INSTANCE.conditionalDeActivate(); } } @Override protected void execute() { if (password == null) { return; } else if (password.trim().isEmpty()) { return; } try { TelnetClient client = null; for (FritzboxBindingProvider provider : providers) { for (String item : provider.getItemNames()) { String query = null; String type = provider.getType(item); if (queryMap.containsKey(type)) { query = queryMap.get(type); } else if (type.startsWith("tam")) { query = "ctlmgr_ctl r tam settings/" + type.toUpperCase() + "/Active"; } else if (type.startsWith("query")) { query = type.substring(type.indexOf(":") + 1).trim(); } else { continue; } if (client == null) { client = new TelnetClient(); client.connect(ip); if (username != null) { receive(client); send(client, username); } receive(client); send(client, password); receive(client); } send(client, query); String answer = receive(client); String[] lines = answer.split("\r\n"); if (lines.length >= 2) { answer = lines[1].trim(); } Class<? extends Item> itemType = provider.getItemType(item); org.openhab.core.types.State state = null; if (itemType.isAssignableFrom(SwitchItem.class)) { if (answer.equals("1")) { state = OnOffType.ON; } else { state = OnOffType.OFF; } } else if (itemType.isAssignableFrom(NumberItem.class)) { state = new DecimalType(answer); } else if (itemType.isAssignableFrom(StringItem.class)) { state = new StringType(answer); } if (state != null) { eventPublisher.postUpdate(item, state); } } } if (client != null) { client.disconnect(); } } catch (Exception e) { logger.warn("Could not get item state ", e); } } @Override protected long getRefreshInterval() { return 60000L; } @Override protected String getName() { return "FritzBox refresh Service"; } /** * Send line via Telnet to FritzBox * * @param client * the telnet client * @param data * the data to send */ private static void send(TelnetClient client, String data) { try { data += "\r\n"; client.getOutputStream().write(data.getBytes()); client.getOutputStream().flush(); } catch (IOException e) { logger.warn("Error sending data", e); } } /** * Receive answer from FritzBox - careful! This blocks if there is no answer * from FritzBox * * @param client * the telnet client * @return */ private static String receive(TelnetClient client) { StringBuffer strBuffer; try { strBuffer = new StringBuffer(); byte[] buf = new byte[4096]; int len = 0; Thread.sleep(750L); while ((len = client.getInputStream().read(buf)) != 0) { strBuffer.append(new String(buf, 0, len)); Thread.sleep(750L); if (client.getInputStream().available() == 0) { break; } } return strBuffer.toString(); } catch (Exception e) { logger.warn("Error receiving data", e); } return null; } }