Listener Implementation Issue

First post here and first attempt at developing a binding. Thank you in advance for reading. The binding is for my swimming pool automation and communicates with the manufacturer’s server to pull back config and status data via XML files. I have the XML decoding tucked away in the connection class, but need to call the a public interface/listener in the discovery service to add things. Maybe this isn’t the correct approach as the XML data comes right back and I don’t really need a call back, but is the only method I’m finding in the bindings I’ve examined. The issue I’m pulling my hair out over is as follows:

In the Bridgehandler initialize routine I’m attempting to add a listener with the following call:

  connection.addHaywardHandlerListener(this);

but receive the following error…

The method addHaywardHandlerListener(HaywardHandlerListener) in the type HaywardOmniLogixConnection is not applicable for the arguments (HaywardOmniLogixBridgeHandler)

Out of all of the example bindings I’ve reviewed, I just can’t seem to find out what I’m doing different that is causing the error.


package org.openhab.binding.haywardomnilogix.internal.handler;

/**
 * The {@link HaywardHandlerListener} is notified when a light status has changed or a light has been removed or added.
 *
 * @author Matt Myers - Initial contribution
 */
// @NonNullByDefault
public interface HaywardHandlerListener {

    void onFilterDiscovered(int systemID, String label);

    void onHeaterDiscovered(int systemID, String label);
}

/**
 * Copyright (c) 2014-2017 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.haywardomnilogix.internal.handler;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler;
import org.eclipse.smarthome.core.types.Command;
import org.openhab.binding.haywardomnilogix.internal.HaywardOmniLogixBindingConstants;
import org.openhab.binding.haywardomnilogix.internal.config.HaywardOmniLogixConfig;
import org.openhab.binding.haywardomnilogix.internal.hayward.HaywardOmniLogixConnection;
import org.openhab.binding.haywardomnilogix.internal.hayward.HaywardTypeToRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link ElkM1BridgeHandler} is responsible for handling commands, which are
 * sent to one of the channels.
 *
 * @author Matt Myers - Initial contribution
 */
public class HaywardOmniLogixBridgeHandler extends BaseBridgeHandler {
    private final Logger logger = LoggerFactory.getLogger(HaywardOmniLogixBridgeHandler.class);
    private List<HaywardHandlerListener> listeners = new ArrayList<HaywardHandlerListener>();
    private HaywardOmniLogixConnection connection;


     public HaywardOmniLogixBridgeHandler(Bridge thing) {
         super(thing);
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        // Needed to instantiate class
    }

    /**
     * Initialize the bridge to do stuff.
     */
    @Override
    public void initialize() {

        // Long running initialization should be done asynchronously in background.
        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
                "Opening connection to Hayward's Server");

        // Load up the config and then get the connection to Hayward setup.
        // messageFactory = new ElkMessageFactory();
        HaywardOmniLogixConfig config = getConfigAs(HaywardOmniLogixConfig.class);
        connection = new HaywardOmniLogixConnection(config);
        connection.addHaywardHandlerListener(this);

        if (connection.initialize()) {

            // TODO Need to return pass/fail
            connection.getConfig();

            updateStatus(ThingStatus.ONLINE, ThingStatusDetail.CONFIGURATION_PENDING, "Logged into Hayward server");
        } else {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                    "Unable to open connection to Hayward's server");
        }

    }
/**
 * Copyright (c) 2014-2017 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.haywardomnilogix.internal.hayward;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.openhab.binding.haywardomnilogix.internal.config.HaywardOmniLogixConfig;
import org.openhab.binding.haywardomnilogix.internal.handler.HaywardHandlerListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * The connection to the Hayward server.
 *
 * @author Matt Myers
 */
public class HaywardOmniLogixConnection {
    private final HaywardOmniLogixConfig config;
    private final Logger logger = LoggerFactory.getLogger(HaywardOmniLogixConnection.class);
    private HttpClient httpClient = new HttpClient();
    private List<HaywardHandlerListener> listeners = new ArrayList<HaywardHandlerListener>();

    // private HaywardOmniLogixBridgeHandler bridge;

    /**
     * Create the connection to the server.
     *
     * @param config The configuration of the Hayward connection
     */
    public HaywardOmniLogixConnection(HaywardOmniLogixConfig config) {
        this.config = config;
        // this.response = response;
    }

    /**
     * Adds the elk listener into the list of things listening for messages.
     */
    public void addHaywardHandlerListener(HaywardHandlerListener listener) {
        synchronized (listeners) {
            listeners.add(listener);
        }
    }


    public void removeHaywardHandlerListener(HaywardHandlerListener listener) {
        synchronized (listeners) {
            listeners.remove(listener);
        }
    }

    /**
     * Initializes the connection
     *
     * @return true if successfully initialized.
     */
    public boolean initialize() {
        String xmlStr;
        String status;
        String statusMessage;

        try {

            // *****Login to Hayward server
            String url = "http://209.114.50.126/MobileInterface/MobileInterface.ashx";
            String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request xmlns=\"http://nextgen.hayward.com/api\"><Name>Login</Name><Paramenters><Parameter name=\"UserName\" dataType=\"String\">"
                    + config.username + "</Parameter><Parameter name=\"Password\" dataType=\"String\">"
                    + config.password + "</Parameter></Paramenters></Request>";

            xmlStr = httpXmlResponse(url, urlParameters);

            status = (evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlStr)).get(0);
            statusMessage = (evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlStr))
                    .get(0);

            if (status.equals("0")) {
                // System.out.println(evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlStr));
                config.token = (evaluateXPath("/Response/Parameters//Parameter[@name='Token']/text()", xmlStr)).get(0);
                config.UserID = (evaluateXPath("/Response/Parameters//Parameter[@name='UserID']/text()", xmlStr))
                        .get(0);
                config.firstName = (evaluateXPath("/Response/Parameters//Parameter[@name='Firstname']/text()", xmlStr))
                        .get(0);
                config.lastName = (evaluateXPath("/Response/Parameters//Parameter[@name='Lastname']/text()", xmlStr))
                        .get(0);
                config.roleType = (evaluateXPath("/Response/Parameters//Parameter[@name='RoleType']/text()", xmlStr))
                        .get(0);
            } else {
                logger.info("Login response: {}", statusMessage);
                return false;

            }

            // *****Get MSP
            url = "http://209.114.50.126/MobileInterface/MobileInterface.ashx";
            urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request xmlns=\"http://nextgen.hayward.com/api\"><Name>GetMspList</Name><Paramenters>"
                    + "<Parameter name=\"Token\" dataType=\"String\">" + config.token
                    + "</Parameter><Parameter name=\"OwnerID\" dataType=\"String\">" + config.UserID
                    + "</Parameter></Paramenters></Request>";

            xmlStr = httpXmlResponse(url, urlParameters);

            status = (evaluateXPath("/Response/Parameters//Parameter[@name='Status']/text()", xmlStr)).get(0);
            statusMessage = (evaluateXPath("/Response/Parameters//Parameter[@name='StatusMessage']/text()", xmlStr))
                    .get(0);

            if (status.equals("0")) {
                config.mspSystemID = (evaluateXPath(
                        "/Response/Parameters/Parameter/Item//Property[@name='MspSystemID']/text()", xmlStr)).get(0);
                config.backyardName = (evaluateXPath(
                        "/Response/Parameters/Parameter/Item//Property[@name='BackyardName']/text()", xmlStr)).get(0);
                config.address = (evaluateXPath("/Response/Parameters/Parameter/Item//Property[@name='Address']/text()",
                        xmlStr)).get(0);
            } else {
                logger.info("Login response: {}", statusMessage);
                return false;
            }

        } catch (Exception e) {
            logger.error("Unable to open connection to Hayward's server {}:{}", config.hostname, config.username, e);
            return false;
        }

        return true;
    }

    public boolean getConfig() {
        String status;
        String statusMessage;
        String xmlResponse;
        List<String> systemIDs = new ArrayList<>();

        try {

            // *****Login to Hayward server
            String url = "http://209.114.50.126/MobileInterface/MobileInterface.ashx";
            String urlParameters = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request xmlns=\"http://nextgen.hayward.com/api\"><Name>RequestConfiguration</Name><Paramenters>"
                    + "<Parameter name=\"Token\" dataType=\"String\">" + config.token + "</Parameter>"
                    + "<Parameter name=\"MspSystemID\" dataType=\"int\">" + config.mspSystemID
                    + "</Parameter></Paramenters></Request>";

            xmlResponse = httpXmlResponse(url, urlParameters);

            if (xmlResponse.length() > 0) {

                systemIDs = (evaluateXPath("/MSPConfig/Backyard/Body-of-water/System-Id/text()", xmlResponse));

                for (int i = 0; i < systemIDs.size(); i++) {
                    systemIDs.get(i);

                    // Once we have a description, see if this zone exists.
                    // Thing thing = thing.getThingForType("BOW", 31);
                    // if (thing == null) {
                    for (HaywardHandlerListener listener : this.listeners) {
                        listener.onFilterDiscovered(32, "Test 32");
                    }
                    
                    HaywardOmniLogixDiscoveryHandler.
                    // }

                }

                System.out.println(
                        evaluateXPath("/MSPConfig/Backyard/Body-of-water/Filter/System-Id/text()", xmlResponse));

                System.out.println(
                        evaluateXPath("/MSPConfig/Backyard/Body-of-water/Heater/System-Id/text()", xmlResponse));

                System.out.println(
                        evaluateXPath("/MSPConfig/Backyard/Body-of-water/Chlorinator/System-Id/text()", xmlResponse));

                System.out.println(
                        evaluateXPath("/MSPConfig/Backyard/Body-of-water/Relay/System-Id/text()", xmlResponse));

                System.out.println(evaluateXPath("/MSPConfig/Backyard/Body-of-water/ColorLogic-Light/System-Id/text()",
                        xmlResponse));

                System.out.println(
                        evaluateXPath("/MSPConfig/Backyard/Body-of-water/Sensor/System-Id/text()", xmlResponse));

            } else

            {
                logger.info("GetMsp Returned null");
                return false;

            }

        } catch (Exception e) {
            logger.error("Unable to open connection to Hayward's server {}:{}", config.hostname, config.username, e);
            return false;
        }

        return true;
    }

    private static List<String> evaluateXPath(String xpathExp, String xmlStr) throws Exception {
        List<String> values = new ArrayList<>();
        try {
            InputSource inputXML = new InputSource(new StringReader(xmlStr));
            XPath xPath = XPathFactory.newInstance().newXPath();
            NodeList nodes = (NodeList) xPath.evaluate(xpathExp, inputXML, XPathConstants.NODESET);

            for (int i = 0; i < nodes.getLength(); i++) {
                values.add(nodes.item(i).getNodeValue());
            }

        } catch (XPathExpressionException e) {
            e.printStackTrace();
        }
        return values;
    }

    private String httpXmlResponse(String url, String urlParameters) {
        int status;
        try {

            if (httpClient.isStarted()) {
                httpClient.stop();
            }
            httpClient.start();

            String urlParameters_length = Integer.toString(urlParameters.length());

            ContentResponse httpResponse = sendRequestBuilder(url, HttpMethod.POST)
                    .content(new StringContentProvider(urlParameters), "text/xml; charset=utf-8")
                    .header(HttpHeader.CONTENT_LENGTH, urlParameters_length).send();

            status = httpResponse.getStatus();
            String xmlStr = httpResponse.getContentAsString();
            if (status == 200) {
                return xmlStr;
            } else {
                logger.info("Login response: {}", httpResponse.getContentAsString());
                return null;
            }

        } catch (Exception e) {
            logger.error("Unable to open connection to Hayward's server {}:{}", config.hostname, config.username, e);
            return null;
        }

    }

    private Request sendRequestBuilder(String url, HttpMethod method) {
        return httpClient.newRequest(url).agent("NextGenForIPhone/16565 CFNetwork/887 Darwin/17.0.0").method(method)
                .header(HttpHeader.ACCEPT_LANGUAGE, "en-us").header(HttpHeader.ACCEPT, "*/*")
                .header(HttpHeader.ACCEPT_ENCODING, "gzip, deflate").version(HttpVersion.HTTP_1_1)
                .header(HttpHeader.CONNECTION, "keep-alive").header(HttpHeader.HOST, "www.haywardomnilogic.com:80")
                // .header(HttpHeader.CONTENT_TYPE, "text/xml; charset=utf-8")
                .timeout(5, TimeUnit.SECONDS);
    }

}

As the error suggests your problem is right here:

public class HaywardOmniLogixBridgeHandler extends BaseBridgeHandler {

Your HaywardOmniLogixBridgeHandler class does not actually implement HaywardHandlerListener so cannot be passed to HaywardOmniLogixConnection#addHaywardHandlerListener(HaywardHandlerListener)

Thanks Ross! That solved the immediate issue, but still leaves me with a problem. The discoveryhandler class holds the logic to create the thing, not the bridgehandler. What is the best way to call the onFilterDiscovered routine in the discoveryHandler from the connection class? Sorry, this is probably trivial but is twisting my melon.

Thanks Again!!
Matt

I’m not really the one to ask for “best way” as I’ve only just made my own first binding (not merged with openhab-addons).

I think you are doing something similar to me where the “bridge” is a connection with a given account to some external API. I guess your problem is “how do I get the things I have discovered in my bridge into the discovery service?”

The technique I used to solve this I think came from looking at the nest binding (look at NestHandlerFactory):

You do not need to annotate your discovery service with @Component and let it be created with OSGi magic. You can manually create your discovery service and “register” it inside your HandlerFactory like this:

final ServiceRegistration<?> serviceReg = bundleContext.registerService(
		DiscoveryService.class.getName(),
		handler.getDiscoveryService(),
		new Hashtable<>()
);

This lets you either:

  • Create a discovery service and give it a reference to your BridgeHandler (like nest)
    OR
  • Get your BridgeHandler to provide its own discovery service for you to register (like I do)

N.B. You have to keep track of when a bridge is removed in your ThingHandlerFactory and unregister their related services so you don’t end up with a load of old discovery services sticking around and doing weird stuff.