org.openhab.binding.fritzboxtr064.internal.Tr064Comm.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.fritzboxtr064.internal.Tr064Comm.java

Source

/**
 * Copyright (c) 2010-2019 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 java.util.Collections.singleton;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.soap.Detail;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.Name;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang.WordUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Controls communication and parsing for TR064 communication with FritzBox.
 *
 * @author gitbock
 * @version 1.8.0
 */
public class Tr064Comm {
    private static final Logger logger = LoggerFactory.getLogger(Tr064Comm.class);
    private static final String DEFAULTUSER = "dslf-config"; // is used when no username is provided.
    private static final String TR064DOWNLOADFILE = "tr64desc.xml"; // filename of all available TR064 on fbox

    // access details for fbox
    private String _url = null;
    private String _user = null;
    private String _pw = null;

    // all services fbox offers mapped by service id
    private Map<String, Tr064Service> _allServices = null;

    // mappig table for mapping item command to tr064 parameters
    private Map<String, ItemMap> _allItemMap = null;

    // http client object used to communicate with fbox (needed for reading/writing
    // soap requests)
    private CloseableHttpClient _httpClient = null;
    private HttpClientContext _httpClientContext = null; // for reusing auth (?)

    public Tr064Comm(String _url, String user, String pass) { // base URL from config
        this._url = _url;
        this._user = user;
        this._pw = pass;
        _allServices = new HashMap<>();
        _allItemMap = new HashMap<>();
        init();
    }

    public String getUrl() {
        return _url;
    }

    public void setUrl(String _url) {
        this._url = _url;
    }

    public String getUser() {
        return _user;
    }

    public void setUser(String _user) {
        this._user = _user;
    }

    public String getPw() {
        return _pw;
    }

    public void setPw(String _pw) {
        this._pw = _pw;
    }

    /**
     * Makes sure all values are set properly before starting communications.
     */
    private void init() {
        if (_user == null) {
            _user = DEFAULTUSER;
        }
        if (_httpClient == null) {
            _httpClient = createTr064HttpClient(_url); // create http client used for communication
        }
        if (_allServices.isEmpty()) { // no services are known yet?
            readAllServices(); // can be done w/out item mappings and w/out auth
        }
        if (_allItemMap.isEmpty()) { // no mappings present yet?
            generateItemMappings();
        }
    }

    public String getTr064Value(ItemConfiguration request) {
        Map<ItemConfiguration, String> values = getTr064Values(singleton(request));
        return values.get(request);
    }

    /**
     * Fetches the values for the given item configurations from the FritzBox. Calls
     * the FritzBox SOAP services delivering the values for the item configurations.
     * The resulting map contains the values of all item configurations returned by
     * the invoked services. This can be more items than were given as parameter.
     *
     * @param request
     *            string from config including the command and optional parameters
     * @return Parsed values for all item configurations returned by the invoked
     *         services.
     */
    public Map<ItemConfiguration, String> getTr064Values(Collection<ItemConfiguration> itemConfigurations) {
        Map<ItemConfiguration, String> values = new HashMap<>();

        for (ItemConfiguration itemConfiguration : itemConfigurations) {

            String itemCommand = itemConfiguration.getItemCommand();

            if (values.containsKey(itemCommand)) {
                // item value already read by earlier MultiItemMap
                continue;
            }

            // search for proper item Mapping
            ItemMap itemMap = determineItemMappingByItemCommand(itemCommand);

            if (itemMap == null) {
                logger.warn("No item mapping found for {}. Function not implemented by your FritzBox (?)",
                        itemConfiguration);
                continue;
            }

            // determine which url etc. to connect to for accessing required value
            Tr064Service tr064service = determineServiceByItemMapping(itemMap);

            // construct soap Body which is added to soap msg later
            SOAPBodyElement bodyData = null; // holds data to be sent to fbox
            try {
                MessageFactory mf = MessageFactory.newInstance();
                SOAPMessage msg = mf.createMessage(); // empty message
                SOAPBody body = msg.getSOAPBody(); // std. SAOP body
                QName bodyName = new QName(tr064service.getServiceType(), itemMap.getReadServiceCommand(), "u"); // header
                                                                                                                 // for
                                                                                                                 // body
                                                                                                                 // element
                bodyData = body.addBodyElement(bodyName);
                // only if input parameter is present
                if (itemMap instanceof ParametrizedItemMap) {
                    for (InputArgument inputArgument : ((ParametrizedItemMap) itemMap)
                            .getConfigInputArguments(itemConfiguration)) {
                        String dataInName = inputArgument.getName();
                        String dataInValue = inputArgument.getValue();
                        QName dataNode = new QName(dataInName);
                        SOAPElement beDataNode = bodyData.addChildElement(dataNode);
                        // if input is mac address, replace "-" with ":" as fbox wants
                        if (itemConfiguration.getItemCommand().equals("maconline")) {
                            dataInValue = dataInValue.replaceAll("-", ":");
                        }
                        beDataNode.addTextNode(dataInValue); // add data which should be requested from fbox for this
                                                             // service
                    }

                }
                logger.trace("Raw SOAP Request to be sent to FritzBox: {}", soapToString(msg));

            } catch (Exception e) {
                logger.warn("Error constructing request SOAP msg for getting parameter. {}", e.getMessage());
                logger.debug("Request was: {}", itemConfiguration);
            }

            if (bodyData == null) {
                logger.warn("Could not determine data to be sent to FritzBox!");
                return null;
            }

            SOAPMessage smTr064Request = constructTr064Msg(bodyData); // construct entire msg with body element
            String soapActionHeader = tr064service.getServiceType() + "#" + itemMap.getReadServiceCommand(); // needed
                                                                                                             // to be
                                                                                                             // sent
                                                                                                             // with
                                                                                                             // request
                                                                                                             // (not
                                                                                                             // in body
                                                                                                             // ->
                                                                                                             // header)
            SOAPMessage response = readSoapResponse(soapActionHeader, smTr064Request,
                    _url + tr064service.getControlUrl());
            logger.trace("Raw SOAP Response from FritzBox: {}", soapToString(response));
            if (response == null) {
                logger.warn("Error retrieving SOAP response from FritzBox");
                continue;
            }

            values.putAll(
                    itemMap.getSoapValueParser().parseValuesFromSoapMessage(response, itemMap, itemConfiguration));
        }

        return values;
    }

    /**
     * Sets a parameter in fbox. Called from event bus.
     *
     * @param request
     *            config string from itemconfig
     * @param cmd
     *            command to set
     */
    public void setTr064Value(ItemConfiguration request, Command cmd) {
        String itemCommand = request.getItemCommand();

        // search for proper item Mapping
        ItemMap itemMapForCommand = determineItemMappingByItemCommand(itemCommand);

        if (!(itemMapForCommand instanceof WritableItemMap)) {
            logger.warn("Item command {} does not support setting values", itemCommand);
            return;
        }
        WritableItemMap itemMap = (WritableItemMap) itemMapForCommand;

        Tr064Service tr064service = determineServiceByItemMapping(itemMap);

        // determine which url etc. to connect to for accessing required value
        // construct soap Body which is added to soap msg later
        SOAPBodyElement bodyData = null; // holds data to be sent to fbox
        try {
            MessageFactory mf = MessageFactory.newInstance();
            SOAPMessage msg = mf.createMessage(); // empty message
            SOAPBody body = msg.getSOAPBody(); // std. SAOP body
            QName bodyName = new QName(tr064service.getServiceType(), itemMap.getWriteServiceCommand(), "u"); // header
                                                                                                              // for
                                                                                                              // body
                                                                                                              // element
            bodyData = body.addBodyElement(bodyName);

            List<InputArgument> writeInputArguments = new ArrayList<>();
            writeInputArguments.add(itemMap.getWriteInputArgument(cmd));
            if (itemMap instanceof ParametrizedItemMap) {
                writeInputArguments.addAll(((ParametrizedItemMap) itemMap).getConfigInputArguments(request));
            }

            for (InputArgument inputArgument : writeInputArguments) {
                QName dataNode = new QName(inputArgument.getName());
                SOAPElement beDataNode = bodyData.addChildElement(dataNode);
                beDataNode.addTextNode(inputArgument.getValue());
            }

            logger.debug("SOAP Msg to send to FritzBox for setting data: {}", soapToString(msg));

        } catch (Exception e) {
            logger.error("Error constructing request SOAP msg for setting parameter. {}", e.getMessage());
            logger.debug("Request was: {}. Command was: {}.", request, cmd.toString());
        }

        if (bodyData == null) {
            logger.error("Could not determine data to be sent to FritzBox!");
            return;
        }

        SOAPMessage smTr064Request = constructTr064Msg(bodyData); // construct entire msg with body element
        String soapActionHeader = tr064service.getServiceType() + "#" + itemMap.getWriteServiceCommand(); // needed to
                                                                                                          // be sent
                                                                                                          // with
                                                                                                          // request
                                                                                                          // (not in
                                                                                                          // body ->
                                                                                                          // header)
        SOAPMessage response = readSoapResponse(soapActionHeader, smTr064Request,
                _url + tr064service.getControlUrl());
        if (response == null) {
            logger.error("Error retrieving SOAP response from FritzBox");
            return;
        }
        logger.debug("SOAP response from FritzBox: {}", soapToString(response));

        // Check if error received
        try {
            if (response.getSOAPBody().getFault() != null) {
                logger.error("Error received from FritzBox while trying to set parameter");
                logger.debug("Soap Response was: {}", soapToString(response));
            }
        } catch (SOAPException e) {
            logger.error("Could not parse soap response from FritzBox while setting parameter. {}", e.getMessage());
            logger.debug("Soap Response was: {}", soapToString(response));
        }

    }

    /**
     * Creates an Apache HTTP Client object, ignoring SSL Exceptions like self signed
     * certificates, and sets Auth. Scheme to Digest Auth.
     *
     * @param fboxUrl
     *            the URL from config file of fbox to connect to
     * @return the ready-to-use httpclient for tr064 requests
     */
    private synchronized CloseableHttpClient createTr064HttpClient(String fboxUrl) {
        CloseableHttpClient hc = null;
        // Convert URL String from config in easy explotable URI object
        URIBuilder uriFbox = null;
        try {
            uriFbox = new URIBuilder(fboxUrl);
        } catch (URISyntaxException e) {
            logger.error("Invalid FritzBox URL! {}", e.getMessage());
            return null;
        }
        // Create context of the http client
        _httpClientContext = HttpClientContext.create();
        CookieStore cookieStore = new BasicCookieStore();
        _httpClientContext.setCookieStore(cookieStore);

        // SETUP AUTH
        // Auth is specific for this target
        HttpHost target = new HttpHost(uriFbox.getHost(), uriFbox.getPort(), uriFbox.getScheme());
        // Add digest authentication with username/pw from global config
        CredentialsProvider credp = new BasicCredentialsProvider();
        credp.setCredentials(new AuthScope(target.getHostName(), target.getPort()),
                new UsernamePasswordCredentials(_user, _pw));
        // Create AuthCache instance. Manages authentication based on server response
        AuthCache authCache = new BasicAuthCache();
        // Generate DIGEST scheme object, initialize it and add it to the local auth
        // cache. Digeste is standard for fbox auth SOAP
        DigestScheme digestAuth = new DigestScheme();
        digestAuth.overrideParamter("realm", "HTTPS Access"); // known from fbox specification
        digestAuth.overrideParamter("nonce", ""); // never known at first request
        authCache.put(target, digestAuth);
        // Add AuthCache to the execution context
        _httpClientContext.setAuthCache(authCache);

        // SETUP SSL TRUST
        SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
        SSLConnectionSocketFactory sslsf = null;
        try {
            sslContextBuilder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); // accept self signed certs
            // dont verify hostname against cert CN
            sslsf = new SSLConnectionSocketFactory(sslContextBuilder.build(), null, null,
                    new NoopHostnameVerifier());
        } catch (Exception ex) {
            logger.error(ex.getMessage());
        }

        // Set timeout values
        RequestConfig rc = RequestConfig.copy(RequestConfig.DEFAULT).setSocketTimeout(4000).setConnectTimeout(4000)
                .setConnectionRequestTimeout(4000).build();

        // BUILDER
        // setup builder with parameters defined before
        hc = HttpClientBuilder.create().setSSLSocketFactory(sslsf) // set the SSL options which trust every self signed
                // cert
                .setDefaultCredentialsProvider(credp) // set auth options using digest
                .setDefaultRequestConfig(rc) // set the request config specifying timeout
                .build();

        return hc;
    }

    /**
     * Converts SOAP message into string.
     *
     * @param sm
     * @return
     */
    public static String soapToString(SOAPMessage sm) {
        String strMsg = "";
        try {
            ByteArrayOutputStream xmlStream = new ByteArrayOutputStream();
            sm.writeTo(xmlStream);
            strMsg = new String(xmlStream.toByteArray());
        } catch (Exception e) {
            logger.debug("Not able to parse SOAP message: {}", sm, e);
        }
        return strMsg;
    }

    /**
     *
     * @param soapActionHeader
     *            String in HTTP Header. specific for each TR064 service
     * @param request
     *            the SOAPMEssage Object to send to fbox as request
     * @param serviceUrl
     *            URL to sent the SOAP Message to (service specific)
     * @return
     */
    private SOAPMessage readSoapResponse(String soapActionHeader, SOAPMessage request, String serviceUrl) {
        SOAPMessage response = null;

        // Soap Body to post
        HttpPost postSoap = new HttpPost(serviceUrl); // url is service specific
        postSoap.addHeader("SOAPAction", soapActionHeader); // add the Header specific for this request
        HttpEntity entBody = null;
        HttpResponse resp = null; // stores raw response from fbox
        boolean exceptionOccurred = false;
        try {
            entBody = new StringEntity(soapToString(request), ContentType.create("text/xml", "UTF-8")); // add body
            postSoap.setEntity(entBody);
            synchronized (_httpClient) {
                resp = _httpClient.execute(postSoap, _httpClientContext);
            }

            // Fetch content data
            StatusLine slResponse = resp.getStatusLine();
            HttpEntity heResponse = resp.getEntity();

            // Check for (auth-) error
            if (slResponse.getStatusCode() == 401) {
                logger.warn(
                        "Could not read response from FritzBox. Unauthorized! Check user and password in config. "
                                + "Verify configured user for tr064 requests. Reason from Fritzbox was: {}",
                        slResponse.getReasonPhrase());

                postSoap.releaseConnection();
                resetHttpClient();
                return null;
            }

            // Parse response into SOAP Message
            response = MessageFactory.newInstance().createMessage(null, heResponse.getContent());

        } catch (UnsupportedEncodingException e) {
            logger.error("Encoding not supported: {}", e.getMessage().toString());
            exceptionOccurred = true;
        } catch (ClientProtocolException e) {
            logger.error("Client Protocol not supported: {}", e.getMessage().toString());
            exceptionOccurred = true;
        } catch (IOException e) {
            logger.error("Cannot send/receive: {}", e.getMessage().toString());
            exceptionOccurred = true;
        } catch (UnsupportedOperationException e) {
            logger.error("Operation unsupported: {}", e.getMessage().toString());
            response = null;
            exceptionOccurred = true;
        } catch (SOAPException e) {
            logger.error("SOAP Error: {}", e.getMessage().toString());
            exceptionOccurred = true;
        } finally {
            // Make sure connection is released. If error occurred make sure to print in log
            if (exceptionOccurred) {
                logger.warn("Releasing connection to FritzBox because of error.");
                resetHttpClient();
            } else {
                logger.debug("Releasing connection");
            }
            postSoap.releaseConnection();
        }

        return response;

    }

    /**
     * In case of failure, reset the authentication state, close connection and init
     * again.
     */
    private void resetHttpClient() {
        logger.trace("Drop client for fritzbox and setup connection again.");
        if (_httpClientContext.getTargetAuthState() != null) {
            _httpClientContext.getTargetAuthState().reset();
        }
        try {
            _httpClient.close();
        } catch (IOException e) {
            logger.debug(
                    "Failed to close connection to fritzbox at {}. "
                            + "This might result in still open, but dead connections waiting for a timeout.",
                    _url, e);
        }
        _httpClient = createTr064HttpClient(_url);
    }

    /**
     * Sets all required namespaces and prepares the SOAP message to send.
     * Creates skeleton + body data.
     *
     * @param bodyData
     *            is attached to skeleton to form entire SOAP message
     * @return ready to send SOAP message
     */
    private SOAPMessage constructTr064Msg(SOAPBodyElement bodyData) {
        SOAPMessage soapMsg = null;

        try {
            MessageFactory msgFac;
            msgFac = MessageFactory.newInstance();
            soapMsg = msgFac.createMessage();
            soapMsg.setProperty(SOAPMessage.WRITE_XML_DECLARATION, "true");
            soapMsg.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, "UTF-8");
            SOAPPart part = soapMsg.getSOAPPart();

            // valid for entire SOAP msg
            String namespace = "s";

            // create suitable fbox envelope
            SOAPEnvelope envelope = part.getEnvelope();
            envelope.setPrefix(namespace);
            envelope.removeNamespaceDeclaration("SOAP-ENV"); // delete standard namespace which was already set
            envelope.addNamespaceDeclaration(namespace, "http://schemas.xmlsoap.org/soap/envelope/");
            Name nEncoding = envelope.createName("encodingStyle", namespace,
                    "http://schemas.xmlsoap.org/soap/encoding/");
            envelope.addAttribute(nEncoding, "http://schemas.xmlsoap.org/soap/encoding/");

            // create empty header
            SOAPHeader header = envelope.getHeader();
            header.setPrefix(namespace);

            // create body with command based on parameter
            SOAPBody body = envelope.getBody();
            body.setPrefix(namespace);
            body.addChildElement(bodyData); // bodyData already prepared. Needs only be added

        } catch (Exception e) {
            logger.error("Error creating SOAP message for fbox request with data {}", bodyData);
            e.printStackTrace();
        }

        return soapMsg;
    }

    /**
     * Looks for the proper item mapping for the item command given from item file.
     *
     * @param itemCommand
     *            String item command
     * @return found itemMap object if found, or null
     */
    private ItemMap determineItemMappingByItemCommand(String itemCommand) {
        ItemMap foundMapping = _allItemMap.get(itemCommand);

        if (foundMapping == null) {
            logger.error("No mapping found for item command {}", itemCommand);
        }
        return foundMapping;
    }

    /**
     * Determines Service including which URL to connect to for value request.
     *
     * @param the
     *            itemmap for which the service is searched
     * @return the found service or null
     */
    private Tr064Service determineServiceByItemMapping(ItemMap mapping) {
        Tr064Service foundService = _allServices.get(mapping.getServiceId());

        if (foundService == null) {
            logger.warn("No tr064 service found for service id {}", mapping.getServiceId());
        }
        return foundService;
    }

    /**
     * Connects to fbox service xml to get a list of all services which are offered
     * by TR064. Saves it into local list.
     */
    private void readAllServices() {
        Document xml = getFboxXmlResponse(_url + "/" + TR064DOWNLOADFILE);
        if (xml == null) {
            logger.error("Could not read xml response services");
            return;
        }
        NodeList nlServices = xml.getElementsByTagName("service"); // get all service nodes
        Node currentNode = null;
        XPath xPath = XPathFactory.newInstance().newXPath();
        for (int i = 0; i < nlServices.getLength(); i++) { // iterate over all services fbox offered us
            currentNode = nlServices.item(i);
            Tr064Service trS = new Tr064Service();
            try {
                trS.setControlUrl((String) xPath.evaluate("controlURL", currentNode, XPathConstants.STRING));
                trS.setEventSubUrl((String) xPath.evaluate("eventSubURL", currentNode, XPathConstants.STRING));
                trS.setScpdurl((String) xPath.evaluate("SCPDURL", currentNode, XPathConstants.STRING));
                trS.setServiceId((String) xPath.evaluate("serviceId", currentNode, XPathConstants.STRING));
                trS.setServiceType((String) xPath.evaluate("serviceType", currentNode, XPathConstants.STRING));
            } catch (XPathExpressionException e) {
                logger.debug("Could not parse service {}", currentNode.getTextContent());
                e.printStackTrace();
            }
            _allServices.put(trS.getServiceId(), trS);
        }
    }

    /**
     * Populates local static mapping table.
     * Sets the parser based on the itemcommand -> soap value parser "svp"
     * anonymous method for each mapping.
     */
    // TODO: refactor to read from config file later?
    private void generateItemMappings() {
        // services available from fbox. Needed for e.g. wifi select 5GHz/Guest Wifi
        if (_allServices.isEmpty()) { // no services are known yet?
            readAllServices();
        }

        // Mac Online Checker
        SingleItemMap imMacOnline = SingleItemMap.builder().itemCommand("maconline")
                .serviceId("urn:LanDeviceHosts-com:serviceId:Hosts1").itemArgumentName("NewActive")
                .configArgumentNames("NewMACAddress").readServiceCommand("GetSpecificHostEntry")
                .soapValueParser(new SoapValueParser() {

                    @Override
                    protected String parseValueFromSoapFault(ItemConfiguration itemConfiguration,
                            SOAPFault soapFault, ItemMap mapping) {
                        String value = null;
                        Detail detail = soapFault.getDetail();
                        if (detail != null) {
                            NodeList nlErrorCode = detail.getElementsByTagName("errorCode");
                            Node nErrorCode = nlErrorCode.item(0);
                            String errorCode = nErrorCode.getTextContent();
                            if (errorCode.equals("714")) {
                                value = "MAC not known to FritzBox!";
                                logger.debug(value);
                            }
                        }

                        if (value == null) {
                            value = super.parseValueFromSoapFault(itemConfiguration, soapFault, mapping);
                        }

                        return value;
                    }
                }).build();
        addItemMap(imMacOnline);

        addItemMap(new MultiItemMap(
                Arrays.asList("modelName", "manufacturerName", "softwareVersion", "serialNumber"), "GetInfo",
                "urn:DeviceInfo-com:serviceId:DeviceInfo1", name -> "New" + WordUtils.capitalize(name)));
        addItemMap(SingleItemMap.builder().itemCommand("wanip")
                .serviceId("urn:WANPPPConnection-com:serviceId:WANPPPConnection1")
                .itemArgumentName("NewExternalIPAddress").readServiceCommand("GetExternalIPAddress").build());
        addItemMap(SingleItemMap.builder().itemCommand("externalWanip")
                .serviceId("urn:WANIPConnection-com:serviceId:WANIPConnection1")
                .itemArgumentName("NewExternalIPAddress").readServiceCommand("GetExternalIPAddress").build());

        // WAN Status
        addItemMap(new MultiItemMap(
                Arrays.asList("wanWANAccessType", "wanLayer1UpstreamMaxBitRate", "wanLayer1DownstreamMaxBitRate",
                        "wanPhysicalLinkStatus"),
                "GetCommonLinkProperties", "urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1",
                name -> name.replace("wan", "New")));
        addItemMap(SingleItemMap.builder().itemCommand("wanTotalBytesSent")
                .serviceId("urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1")
                .itemArgumentName("NewTotalBytesSent").readServiceCommand("GetTotalBytesSent").build());
        addItemMap(SingleItemMap.builder().itemCommand("wanTotalBytesReceived")
                .serviceId("urn:WANCIfConfig-com:serviceId:WANCommonInterfaceConfig1")
                .itemArgumentName("NewTotalBytesReceived").readServiceCommand("GetTotalBytesReceived").build());

        // DSL Status
        addItemMap(new MultiItemMap(
                Arrays.asList("dslEnable", "dslStatus", "dslUpstreamCurrRate", "dslDownstreamCurrRate",
                        "dslUpstreamMaxRate", "dslDownstreamMaxRate", "dslUpstreamNoiseMargin",
                        "dslDownstreamNoiseMargin", "dslUpstreamAttenuation", "dslDownstreamAttenuation"),
                "GetInfo", "urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1",
                name -> name.replace("dsl", "New")));
        addItemMap(new MultiItemMap(Arrays.asList("dslFECErrors", "dslHECErrors", "dslCRCErrors"),
                "GetStatisticsTotal", "urn:WANDSLIfConfig-com:serviceId:WANDSLInterfaceConfig1",
                name -> name.replace("dsl", "New")));

        // Wifi 2,4GHz
        SingleItemMap imWifi24Switch = SingleItemMap.builder().itemCommand("wifi24Switch")
                .serviceId("urn:WLANConfiguration-com:serviceId:WLANConfiguration1").itemArgumentName("NewEnable")
                .readServiceCommand("GetInfo").writeServiceCommand("SetEnable").build();
        addItemMap(imWifi24Switch);

        // wifi 5GHz
        SingleItemMap imWifi50Switch = SingleItemMap.builder().itemCommand("wifi50Switch")
                .serviceId("urn:WLANConfiguration-com:serviceId:WLANConfiguration2").itemArgumentName("NewEnable")
                .readServiceCommand("GetInfo").writeServiceCommand("SetEnable").build();

        // guest wifi
        SingleItemMap imWifiGuestSwitch = SingleItemMap.builder().itemCommand("wifiGuestSwitch")
                .serviceId("urn:WLANConfiguration-com:serviceId:WLANConfiguration3").itemArgumentName("NewEnable")
                .readServiceCommand("GetInfo").writeServiceCommand("SetEnable").build();

        // check if 5GHz wifi and/or guest wifi is available.
        Tr064Service svc5GHzWifi = determineServiceByItemMapping(imWifi50Switch);
        Tr064Service svcGuestWifi = determineServiceByItemMapping(imWifiGuestSwitch);

        if (svc5GHzWifi != null && svcGuestWifi != null) { // WLANConfiguration3+2 present -> guest wifi + 5Ghz present
            // prepared properly, only needs to be added
            addItemMap(imWifi50Switch);
            addItemMap(imWifiGuestSwitch);
            logger.debug("Found 2,4 Ghz, 5Ghz and Guest Wifi");
        }

        if (svc5GHzWifi != null && svcGuestWifi == null) { // WLANConfiguration3 not present but 2 -> no 5Ghz Wifi
                                                           // available but Guest Wifi
                                                           // remap itemMap for Guest Wifi from 3 to 2
            imWifiGuestSwitch.setServiceId("urn:WLANConfiguration-com:serviceId:WLANConfiguration2");
            addItemMap(imWifiGuestSwitch);// only add guest wifi, no 5Ghz
            logger.debug("Found 2,4 Ghz and Guest Wifi");
        }
        if (svc5GHzWifi == null && svcGuestWifi == null) { // WLANConfiguration3+2 not present > no 5Ghz Wifi or Guest
                                                           // Wifi
            logger.debug("Found 2,4 Ghz Wifi");
        }

        // Phonebook Download
        // itemcommand is dummy: not a real item
        ItemMap imPhonebook = SingleItemMap.builder().itemCommand("phonebook")
                .serviceId("urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1").configArgumentNames("NewPhonebookID")
                .itemArgumentName("NewPhonebookURL").readServiceCommand("GetPhonebook").build();
        addItemMap(imPhonebook);

        // TAM (telephone answering machine) Switch
        SingleItemMap imTamSwitch = SingleItemMap.builder().itemCommand("tamSwitch")
                .serviceId("urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1").configArgumentNames("NewIndex")
                .itemArgumentName("NewEnable").readServiceCommand("GetInfo").writeServiceCommand("SetEnable")
                .build();
        addItemMap(imTamSwitch);

        // New Messages per TAM ID
        // two requests needed: First gets URL to download tam info from, 2nd contains
        // info of messages
        SingleItemMap imTamNewMessages = SingleItemMap.builder().itemCommand("tamNewMessages")
                .serviceId("urn:X_AVM-DE_TAM-com:serviceId:X_AVM-DE_TAM1").configArgumentNames("NewIndex")
                .itemArgumentName("NewURL").readServiceCommand("GetMessageList")
                .soapValueParser(new SoapValueParser() {

                    @Override
                    protected String parseValueFromSoapBody(ItemConfiguration itemConfiguration, SOAPBody soapBody,
                            ItemMap mapping) {
                        String value = null;

                        // extract URL from soap response
                        String url = super.parseValueFromSoapBody(itemConfiguration, soapBody, mapping);

                        if (url != null) {
                            Document xmlTamInfo = getFboxXmlResponse(url);
                            if (xmlTamInfo != null) {
                                logger.debug("Parsing xml message TAM info {}",
                                        Helper.documentToString(xmlTamInfo));
                                NodeList nlNews = xmlTamInfo.getElementsByTagName("New"); // get all Nodes containing
                                                                                          // "new", indicating message
                                                                                          // was not listened to

                                // When <new> contains 1 -> message is new, when 0, message not new -> Counting
                                // 1s
                                int newMessages = 0;
                                for (int i = 0; i < nlNews.getLength(); i++) {
                                    if (nlNews.item(i).getTextContent().equals("1")) {
                                        newMessages++;
                                    }
                                }
                                value = Integer.toString(newMessages);
                                logger.debug("Parsed new messages as: {}", value);
                            } else {
                                logger.warn("Failed to read TAM info from URL {}", url);
                                // cause was already logged earlier
                            }
                        }

                        return value;
                    }
                }).build();
        addItemMap(imTamNewMessages);

        // Missed calls
        // two requests: 1st fetches URL to download call list, 2nd fetches xml call
        // list
        SingleItemMap imMissedCalls = SingleItemMap.builder().itemCommand("missedCallsInDays")
                .serviceId("urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1").itemArgumentName("NewCallListURL")
                .readServiceCommand("GetCallList").configArgumentNames("NewDays")
                .soapValueParser(new SoapValueParser() {

                    @Override
                    protected String parseValueFromSoapBody(ItemConfiguration itemConfiguration, SOAPBody soapBody,
                            ItemMap mapping) {
                        String value = null;

                        // extract URL from soap response
                        String url = super.parseValueFromSoapBody(itemConfiguration, soapBody, mapping);

                        // extract how many days of call list should be examined for missed calls
                        String days = "3"; // default
                        if (!itemConfiguration.getArgumentValues().isEmpty()) {
                            days = itemConfiguration.getArgumentValues().get(0); // set the days as defined in item
                                                                                 // config.
                                                                                 // Otherwise default value of 3 is used
                        }

                        if (url != null) {
                            // only get missed calls of the last x days
                            url = url + "&days=" + days;
                            logger.debug("Downloading call list using url {}", url);
                            Document callListInfo = getFboxXmlResponse(url); // download call list
                            if (callListInfo != null) {
                                logger.debug("Parsing xml message call list info {}",
                                        Helper.documentToString(callListInfo));
                                NodeList nlTypes = callListInfo.getElementsByTagName("Type"); // get all Nodes
                                                                                              // containing "Type". Type
                                                                                              // 2 => missed

                                // When <type> contains 2 -> call was missed -> Counting only 2 entries
                                int missedCalls = 0;
                                for (int i = 0; i < nlTypes.getLength(); i++) {
                                    if (nlTypes.item(i).getTextContent().equals("2")) {
                                        missedCalls++;
                                    }
                                }
                                value = Integer.toString(missedCalls);
                                logger.debug("Parsed new messages as: {}", value);
                            } else {
                                logger.warn("Failed to read call list info from URL {}", url);
                                // cause was already logged earlier
                            }
                        }

                        return value;
                    }
                }).build();
        addItemMap(imMissedCalls);

        // call deflection
        SingleItemMap callDeflection = SingleItemMap.builder().itemCommand("callDeflectionSwitch")
                .serviceId("urn:X_AVM-DE_OnTel-com:serviceId:X_AVM-DE_OnTel1")
                .configArgumentNames("NewDeflectionId").itemArgumentName("NewEnable")
                .readServiceCommand("GetDeflection").writeServiceCommand("SetDeflectionEnable").build();
        addItemMap(callDeflection);
    }

    private void addItemMap(ItemMap itemMap) {
        for (String itemCommand : itemMap.getItemCommands()) {
            if (_allItemMap.containsKey(itemCommand)) {
                throw new IllegalStateException("ItemMap for itemCommand " + itemCommand + " already defined");
            }
            _allItemMap.put(itemCommand, itemMap);
        }
    }

    /**
     * Sets up a raw http(s) connection to Fbox and gets xml response as XML
     * Document, ready for parsing.
     *
     * @return
     */
    public Document getFboxXmlResponse(String url) {
        Document tr064response = null;
        HttpGet httpGet = new HttpGet(url);
        boolean exceptionOccurred = false;
        try {
            synchronized (_httpClient) {
                CloseableHttpResponse resp = _httpClient.execute(httpGet, _httpClientContext);
                int responseCode = resp.getStatusLine().getStatusCode();
                if (responseCode == 200) {
                    HttpEntity entity = resp.getEntity();
                    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
                    DocumentBuilder db = dbf.newDocumentBuilder();
                    tr064response = db.parse(entity.getContent());
                    EntityUtils.consume(entity);
                } else {
                    logger.error("Failed to receive valid response from httpGet");
                }
            }
        } catch (Exception e) {
            exceptionOccurred = true;
            logger.error("Failed to receive valid response from httpGet: {}", e.getMessage());
        } finally {
            // Make sure connection is released. If error occurred make sure to print in log
            if (exceptionOccurred) {
                logger.error("Releasing connection to FritzBox because of error!");
            } else {
                logger.debug("Releasing connection");
            }
            httpGet.releaseConnection();
        }
        return tr064response;
    }
}