{"action":"created","comment":{"url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/comments/93948752","pull_request_review_id":14476931,"id":93948752,"diff_hunk":"@@ -0,0 +1,1141 @@\n+/**\n+ * Copyright (c) 2014-2015 openHAB UG (haftungsbeschraenkt) and others.\n+ * All rights reserved. This program and the accompanying materials\n+ * are made available under the terms of the Eclipse Public License v1.0\n+ * which accompanies this distribution, and is available at\n+ * http://www.eclipse.org/legal/epl-v10.html\n+ */\n+package org.openhab.binding.sony.internal.bravia;\n+\n+import java.io.IOException;\n+import java.util.HashMap;\n+import java.util.Map;\n+import java.util.Map.Entry;\n+import java.util.regex.Matcher;\n+import java.util.regex.Pattern;\n+\n+import org.apache.commons.lang.StringUtils;\n+import org.eclipse.smarthome.core.library.types.OnOffType;\n+import org.eclipse.smarthome.core.library.types.PercentType;\n+import org.eclipse.smarthome.core.library.types.StringType;\n+import org.eclipse.smarthome.core.thing.ThingStatus;\n+import org.eclipse.smarthome.core.thing.ThingStatusDetail;\n+import org.openhab.binding.sony.internal.net.SocketSession;\n+import org.openhab.binding.sony.internal.net.SocketSessionListener;\n+import org.slf4j.Logger;\n+import org.slf4j.LoggerFactory;\n+\n+/**\n+ * This is the protocol handler for the Bravia System. This handler will issue the protocol commands and will\n+ * process the responses from the Bravia system. The Bravia system is a little flacky and doesn't seem to handle\n+ * multiple commands in a single session. For this reason, we create a single {@link SocketSession} to listen for any\n+ * notifications (whose lifetime matches that of this handler) and then create separate {@link SocketSession} for each\n+ * request. Special care must be taken to differentiate between a Control request result and the Enquiry/Notification\n+ * results to avoid misinterpreting the result (the control \"success\" message will have all zeroes - which has a form\n+ * that matches some enquery/notification results (like volume could be interpreted as 0!).\n+ *\n+ * @author Tim Roberts\n+ * @version $Id: $Id\n+ *\n+ */\n+class BraviaProtocol implements SocketSessionListener {\n+    // Logger\n+    private Logger logger = LoggerFactory.getLogger(BraviaProtocol.class);\n+\n+    // Protocol Constants\n+    private final char TYPE_CONTROL = 'C';\n+    private final char TYPE_ENQUIRY = 'E';\n+\n+    private final static String IRCC = \"IRCC\";\n+    private final static String POWER = \"POWR\";\n+    private final static String VOLUME = \"VOLU\";\n+    private final static String AUDIO_MUTE = \"AMUT\";\n+    private final static String CHANNEL = \"CHNN\";\n+    private final static String TRIPLET_CHANNEL = \"TCHN\";\n+    private final static String INPUT_SOURCE = \"ISRC\";\n+    private final static String INPUT = \"INPT\";\n+    private final static String PICTURE_MUTE = \"PMUT\";\n+    private final static String TOGGLE_PICTURE_MUTE = \"TPMU\";\n+    private final static String PICTURE_IN_PICTURE = \"PIPI\";\n+    private final static String TOGGLE_PICTURE_IN_PICTURE = \"TPIP\";\n+    private final static String TOGGLE_PIP_POSITION = \"TPPP\";\n+    private final static String BROADCAST_ADDRESS = \"BADR\";\n+    private final static String MAC_ADDRESS = \"MADR\";\n+    private final static String NO_PARM = \"################\";\n+\n+    // Size of the parameter area as defined by the spec\n+    private final static int PARM_SIZE = 16;\n+\n+    // Response strings/patterns\n+    private final String RSP_SUCCESS = \"0000000000000000\";\n+    private final String RSP_ERROR = \"FFFFFFFFFFFFFFFF\";\n+    private final String RSP_NOSUCHTHING = \"NNNNNNNNNNNNNNNN\";\n+    private final Pattern RSP_NOTIFICATION = Pattern.compile(\"^\\\\*S([AN])(\\\\w{4})(.*{16})\");\n+\n+    /**\n+     * The {@link SocketSession} that will listen for notifications\n+     */\n+    private final SocketSession _listeningSession;\n+\n+    /**\n+     * The {@link BraviaConfig} for creating new {@link SocketSession}\n+     */\n+    private final BraviaConfig _config;\n+\n+    /**\n+     * The {@link BraviaHandlerCallback} that we can callback to set state and status\n+     */\n+    private final BraviaHandlerCallback _callback;\n+\n+    /**\n+     * Constant representing the TV input id\n+     */\n+    private final static int INPUT_TV = 0;\n+\n+    /**\n+     * Represents a lookup between the known input source types and their corresponding bravia identifier\n+     */\n+    private final static Map<Integer, String> _inputTypes = new HashMap<Integer, String>();\n+    static {\n+        _inputTypes.put(INPUT_TV, \"TV\");\n+        _inputTypes.put(10000, \"HDMI\");\n+        _inputTypes.put(20000, \"SCART\");\n+        _inputTypes.put(30000, \"Composite\");\n+        _inputTypes.put(40000, \"Component\");\n+        _inputTypes.put(50000, \"Screen Mirroring\");\n+        _inputTypes.put(60000, \"PC RGB Input\");\n+    }\n+\n+    /**\n+     * Constructs the protocol handler from given parameters. This constructor will create the\n+     * {@link #_listeningSession} to listen to notifications sent by the Bravia device (adding ourselfs as the\n+     * listener).\n+     *\n+     * @param config a non-null {@link BraviaConfig} (may be connected or disconnected)\n+     * @param callback a non-null {@link RioHandlerCallback} to callback\n+     * @throws IOException\n+     */\n+    BraviaProtocol(BraviaConfig config, BraviaHandlerCallback callback) throws IOException {\n+        _config = config;\n+        _callback = callback;\n+\n+        _listeningSession = new SocketSession(config.getIpAddress(), 20060);\n+        _listeningSession.addListener(this);\n+    }\n+\n+    /**\n+     * Attempts to log into the system. The login will connect the {@link #_listeningSession} and immediately call\n+     * {@link #postLogin()} since there is no authentication mechanisms\n+     *\n+     * @return always null to indicate a successful login\n+     * @throws IOException if an exception occurs trying to connect our {@link #_listeningSession}\n+     */\n+    String login() throws IOException {\n+        _listeningSession.connect();\n+        postLogin();\n+        return null;\n+    }\n+\n+    /**\n+     * Post successful login stuff - mark us online!\n+     */\n+    private void postLogin() {\n+        logger.info(\"Bravia TV System now connected\");\n+        _callback.statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);\n+    }\n+\n+    /**\n+     * Called to refresh some of the state of the bravia system (mainly state that either we won't get notifications for\n+     * or whose state may commonly change due to remote actions\n+     */\n+    void refreshState() {\n+        refreshVolume();\n+        refreshChannel();\n+        refreshTripletChannel();\n+        refreshInputSource();\n+        refreshPictureInPicture();\n+    }\n+\n+    /**\n+     * Sends the command and puts the thing into {@link ThingStatus#OFFLINE} if an IOException occurs. This method will\n+     * create a new {@link SocketSession} for the command and a anonymous {@link SocketSessionListener} to listen for\n+     * the result. The connection will then be closed/disposed of when a valid response is received (if the listener is\n+     * more than 10 seconds old, it will be disposed of to avoid memory leaks).\n+     *\n+     * @param type the type of command ({@link #TYPE_CONTROL} or {@link #TYPE_ENQUIRY})\n+     * @param command a non-null, non-empty command to send\n+     * @param parm the parameters for the command. Must be exactly {@link #PARM_SIZE} in length\n+     * @throws IllegalArgumentException if command is null or empty\n+     * @throws IllegalArgumentException if parm is null or not {@link #PARM_SIZE} in length\n+     */\n+    protected void sendCommand(final char type, final String command, String parm) {\n+        if (command == null || command.trim().length() == 0) {\n+            throw new IllegalArgumentException(\"command cannot be null or empty\");\n+        }\n+        if (parm == null || parm.length() != PARM_SIZE) {\n+            throw new IllegalArgumentException(\n+                    \"parm cannot be null and must be exactly \" + PARM_SIZE + \" in length: \" + parm);\n+        }\n+\n+        // Create our command\n+        final String cmd = \"*S\" + type + command + parm;\n+\n+        // Create the socket session for this request\n+        final SocketSession cmdSocket = new SocketSession(_config.getIpAddress(), 20060);\n+\n+        // Add our listener to it\n+        cmdSocket.addListener(new SocketSessionListener() {\n+            private long createTime = System.currentTimeMillis();\n+\n+            // Disconnects (must be in a different thread to avoid locking issues)\n+            private void disconnect(final SocketSessionListener listener) {\n+                new Thread(new Runnable() {\n+                    @Override\n+                    public void run() {\n+                        try {\n+                            cmdSocket.removeListener(listener);\n+                            cmdSocket.disconnect();\n+                        } catch (IOException e) {\n+\n+                        }\n+\n+                    }\n+                }).start();\n+            }\n+\n+            @Override\n+            public void responseReceived(String response) {\n+                // If request is more than 10 seconds, cancel this listener/socket\n+                if (createTime + 10000 < System.currentTimeMillis()) {\n+                    logger.debug(\"No valid response received within 10 seconds - removing listener\");\n+                    disconnect(this);\n+                    return;\n+                }\n+\n+                // See if the response is valid\n+                final Matcher m = RSP_NOTIFICATION.matcher(response);\n+                if (m.matches() && m.groupCount() == 3) {\n+\n+                    // make sure we only process responses for our command\n+                    if (m.group(1).equals(\"A\") && m.group(2).equals(command)) {\n+                        if (type == TYPE_CONTROL) {\n+                            handleResponse(m, response, cmd);\n+                        } else if (type == TYPE_ENQUIRY) {\n+                            handleNotification(m, response);\n+                        } else {\n+                            logger.error(\"Unknown command type: \" + type);\n+                        }\n+                        disconnect(this);\n+                    }\n+                } else {\n+                    logger.warn(\"Unparsable response '{}' to command '{}'\", response, cmd);\n+                }\n+\n+            }\n+\n+            @Override\n+            public void responseException(Exception e) {\n+                BraviaProtocol.this.responseException(e);\n+                disconnect(this);\n+            }\n+        });\n+\n+        // Connect to the socket and send out command\n+        try {\n+            cmdSocket.connect();\n+            cmdSocket.sendCommand(cmd);\n+        } catch (IOException e) {\n+            _callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,\n+                    \"Exception occurred sending command: \" + e);\n+        }\n+    }\n+\n+    /**\n+     * Pings the server with our ping command to keep the connection alive\n+     */\n+    void ping() {\n+        try {\n+            _listeningSession.sendCommand(\"*SEPING################\");\n+        } catch (IOException e) {\n+            _callback.statusChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,\n+                    \"Exception occurred sending command: PING\");\n+        }\n+    }\n+\n+    /**\n+     * Refreshes the power status\n+     */\n+    void refreshPower() {\n+        sendCommand(TYPE_ENQUIRY, POWER, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the volume status\n+     */\n+    void refreshVolume() {\n+        sendCommand(TYPE_ENQUIRY, VOLUME, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the audio mute status\n+     */\n+    void refreshAudioMute() {\n+        sendCommand(TYPE_ENQUIRY, AUDIO_MUTE, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the channel\n+     */\n+    void refreshChannel() {\n+        sendCommand(TYPE_ENQUIRY, CHANNEL, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the channel triplet\n+     */\n+    void refreshTripletChannel() {\n+        sendCommand(TYPE_ENQUIRY, TRIPLET_CHANNEL, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the input source\n+     */\n+    void refreshInputSource() {\n+        sendCommand(TYPE_ENQUIRY, INPUT_SOURCE, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the input\n+     */\n+    void refreshInput() {\n+        sendCommand(TYPE_ENQUIRY, INPUT, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the picture mute\n+     */\n+    void refreshPictureMute() {\n+        sendCommand(TYPE_ENQUIRY, PICTURE_MUTE, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the PIP\n+     */\n+    void refreshPictureInPicture() {\n+        sendCommand(TYPE_ENQUIRY, PICTURE_IN_PICTURE, NO_PARM);\n+    }\n+\n+    /**\n+     * Refreshes the broadcast address\n+     *\n+     * @param netInterface the possibly null network interface to inquire\n+     */\n+    void refreshBroadcastAddress(String netInterface) {\n+        if (netInterface == null) {\n+            netInterface = \"eth0\";\n+        }\n+        sendCommand(TYPE_ENQUIRY, BROADCAST_ADDRESS, StringUtils.rightPad(netInterface, PARM_SIZE, '#'));\n+    }\n+\n+    /**\n+     * Refreshes the mac address of the given network interface\n+     *\n+     * @param netInterface the possibly null mac interface to inquire\n+     */\n+    void refreshMacAddress(String interfce) {\n+        if (interfce == null) {\n+            interfce = \"eth0\";\n+        }\n+        sendCommand(TYPE_ENQUIRY, MAC_ADDRESS, StringUtils.rightPad(interfce, PARM_SIZE, '#'));\n+    }\n+\n+    /**\n+     * Sets the IRCC code\n+     *\n+     * @param ircc a non negative IR code\n+     * @throws IllegalArgumentException if ircc is negative\n+     */\n+    void setIR(int ircc) {\n+        if (ircc < 0) {\n+            throw new IllegalArgumentException(\"ircc must be positive\");\n+\n+        }\n+        sendCommand(TYPE_CONTROL, IRCC, StringUtils.leftPad(Integer.toString(ircc), PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Sets the power on/off to the device\n+     *\n+     * @param on true if on, false off\n+     */\n+    void setPower(boolean on) {\n+        sendCommand(TYPE_CONTROL, POWER, StringUtils.leftPad(on ? \"1\" : \"0\", PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Sets the volume level\n+     *\n+     * @param volume a volume between 0-100\n+     * @throws IllegalArgumentException if < 0 or > 100\n+     */\n+    void setAudioVolume(int volume) {\n+        if (volume < 0 || volume > 100) {\n+            throw new IllegalArgumentException(\"volume must be between 0-100\");\n+        }\n+        sendCommand(TYPE_CONTROL, VOLUME, StringUtils.leftPad(Integer.toString(volume), PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Sets the audio mute\n+     *\n+     * @param on true for muted, false otherwise\n+     */\n+    void setAudioMute(boolean on) {\n+        sendCommand(TYPE_CONTROL, AUDIO_MUTE, StringUtils.leftPad(on ? \"1\" : \"0\", PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Sets the channel. Channel must be in the form of \"x.x\" or simply \"x\" where x must be numeric\n+     *\n+     * @param channel the non-null, non-empty channel in the form of \"x.x\" or \"x\" (such as 50.1 or 6)\n+     * @throws IllegalArgumentException if channel is null or empty\n+     * @throws IllegalArgumentException if channel is not in the form of \"x.x\" or \"x\" where x is numeric\n+     */\n+    void setChannel(String channel) {\n+        if (channel == null || channel.trim().length() == 0) {\n+            throw new IllegalArgumentException(\"channel cannot be null or empty\");\n+        }\n+        final int period = channel.indexOf('.');\n+\n+        final String pre = period < 0 ? channel : channel.substring(0, period);\n+        final String post = period < 0 ? \"0\" : channel.substring(period + 1);\n+        try {\n+            final int preNum = Integer.parseInt(pre);\n+            final int postNum = Integer.parseInt(post);\n+            final String cmd = StringUtils.leftPad(Integer.toString(preNum), 8, '0') + \".\"\n+                    + StringUtils.rightPad(Integer.toString(postNum), 7, '0');\n+            sendCommand(TYPE_CONTROL, CHANNEL, cmd);\n+\n+        } catch (NumberFormatException e) {\n+            throw new IllegalArgumentException(\"channel could not be parsed: \" + channel);\n+        }\n+    }\n+\n+    /**\n+     * Sets the triplet channel. Channel must be in the form of \"x.x.x\" where x must be numeric\n+     *\n+     * @param channel the non-null, non-empty channel in the form of \"x.x.x\" (such as 32736.32736.1024)\n+     * @throws IllegalArgumentException if channel is null or empty\n+     * @throws IllegalArgumentException if channel is not in the form of \"x.x.x\"\n+     */\n+    void setTripletChannel(String channel) {\n+        if (channel == null || channel.trim().length() == 0) {\n+            throw new IllegalArgumentException(\"channel cannot be null or empty\");\n+        }\n+        final int firstPeriod = channel.indexOf('.');\n+        if (firstPeriod < 0) {\n+            throw new IllegalArgumentException(\n+                    \"Could not find the number of the first part of the triplet channel: \" + channel);\n+        }\n+\n+        final int secondPeriod = channel.indexOf(',', firstPeriod + 1);\n+        if (firstPeriod < 0) {\n+            throw new IllegalArgumentException(\n+                    \"Could not find the number of the second part of the triplet channel: \" + channel);\n+        }\n+\n+        final String first = channel.substring(0, firstPeriod);\n+        final String second = channel.substring(firstPeriod + 1, secondPeriod);\n+        final String third = channel.substring(secondPeriod + 1);\n+        try {\n+            final int firstNum = Integer.parseInt(first);\n+            final int secondNum = Integer.parseInt(second);\n+            final int thirdNum = Integer.parseInt(third);\n+\n+            final String firstHex = StringUtils.leftPad(Integer.toHexString(firstNum), 4, '0');\n+            final String secondHex = StringUtils.leftPad(Integer.toHexString(secondNum), 4, '0');\n+            final String thirdHex = StringUtils.leftPad(Integer.toHexString(thirdNum), 4, '0');\n+\n+            sendCommand(TYPE_CONTROL, CHANNEL, firstHex + secondHex + thirdHex + \"####\");\n+        } catch (NumberFormatException e) {\n+            throw new IllegalArgumentException(\"channel could not be parsed: \" + channel);\n+        }\n+    }\n+\n+    /**\n+     * Sets the input source and will refresh channel and triplet channel afterwards. This must be a valid string\n+     * recognized by the bravia device.\n+     *\n+     * @param source a non-null, non-empty input source\n+     * @throws IllegalArgumentException if source is null or empty\n+     */\n+    void setInputSource(String source) {\n+        if (source == null || source.trim().length() == 0) {\n+            throw new IllegalArgumentException(\"source cannot be null or empty\");\n+        }\n+        sendCommand(TYPE_CONTROL, INPUT_SOURCE, StringUtils.rightPad(source, PARM_SIZE, '#'));\n+    }\n+\n+    /**\n+     * Sets the input port and will refresh input source, channel and triplet channel afterwards. This must be a valid\n+     * in the form of \"xxxxyyyy\" where xxxx is the name of the input and yyyy is the port number (like \"hdmi1\"). The\n+     * valid input names are \"TV\", \"HDMI\", \"SCART\", \"Composite\", \"Component\", \"Screen Mirroring\", and \"PC RGB Input\"\n+     * (case doesn't matter). The port number does NOT apply to \"TV\".\n+     *\n+     * @param input a non-null, non-empty input port\n+     * @throws IllegalArgumentException if input is null or empty\n+     */\n+    void setInput(String input) {\n+        if (input == null || input.trim().length() == 0) {\n+            throw new IllegalArgumentException(\"input cannot be null or empty\");\n+        }\n+        int typeCode = -1;\n+        int portNbr = -1;\n+        final String lowerInput = input.toLowerCase();\n+        for (Entry<Integer, String> entry : _inputTypes.entrySet()) {\n+            if (lowerInput.startsWith(entry.getValue().toLowerCase())) {\n+                typeCode = entry.getKey();\n+                if (typeCode != INPUT_TV) {\n+                    try {\n+                        portNbr = Integer.parseInt(input.substring(entry.getValue().length()));\n+                    } catch (NumberFormatException e) {\n+                        throw new IllegalArgumentException(\n+                                \"The port number on the input is invalid (not an integer): \" + input);\n+                    }\n+                } else {\n+                    portNbr = 0;\n+                }\n+                break;\n+            }\n+        }\n+        if (typeCode == -1) {\n+            throw new IllegalArgumentException(\"Unknown input: \" + input);\n+        }\n+        sendCommand(TYPE_CONTROL, INPUT, StringUtils.leftPad(Integer.toString(typeCode), 12, '0')\n+                + StringUtils.leftPad(Integer.toString(portNbr), 4, '0'));\n+    }\n+\n+    /**\n+     * Sets the picture mute\n+     *\n+     * @param on true for muted, false otherwise\n+     */\n+    void setPictureMute(boolean on) {\n+        sendCommand(TYPE_CONTROL, PICTURE_MUTE, StringUtils.leftPad(on ? \"1\" : \"0\", PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Toggles the picture mute\n+     */\n+    void togglePictureMute() {\n+        sendCommand(TYPE_CONTROL, TOGGLE_PICTURE_MUTE, NO_PARM);\n+    }\n+\n+    /**\n+     * Sets the PIP enabling\n+     *\n+     * @param on true to enable, false otherwise\n+     */\n+    void setPictureInPicture(boolean on) {\n+        sendCommand(TYPE_CONTROL, PICTURE_IN_PICTURE, StringUtils.leftPad(on ? \"1\" : \"0\", PARM_SIZE, '0'));\n+    }\n+\n+    /**\n+     * Toggles PIP enabling\n+     */\n+    void togglePictureInPicture() {\n+        sendCommand(TYPE_CONTROL, TOGGLE_PICTURE_IN_PICTURE, NO_PARM);\n+    }\n+\n+    /**\n+     * Toggles the PIP position\n+     */\n+    void togglePipPosition() {\n+        sendCommand(TYPE_CONTROL, TOGGLE_PIP_POSITION, NO_PARM);\n+    }\n+\n+    /**\n+     * Handles control responses from commands (*SC->*SA)\n+     *\n+     * @param m a non-null matcher\n+     * @param resp a possibly null, possibly empty response\n+     * @param command the possibly null, possibly empty command that triggered this response\n+     */\n+    private void handleResponse(Matcher m, String response, String command) {\n+        if (m.groupCount() == 3) {\n+            final String cmd = m.group(2);\n+            final String parms = m.group(3);\n+\n+            if (IRCC.equalsIgnoreCase(cmd)) {\n+                handleIRResponse(parms);\n+            } else if (POWER.equalsIgnoreCase(cmd)) {\n+                handlePowerResponse(parms);\n+            } else if (VOLUME.equalsIgnoreCase(cmd)) {\n+                handleAudioVolumeResponse(parms);\n+            } else if (AUDIO_MUTE.equalsIgnoreCase(cmd)) {\n+                handleAudioMuteResponse(parms);\n+            } else if (CHANNEL.equalsIgnoreCase(cmd)) {\n+                handleChannelResponse(parms);\n+            } else if (TRIPLET_CHANNEL.equalsIgnoreCase(cmd)) {\n+                handleTripletChannelResponse(parms);\n+            } else if (INPUT_SOURCE.equalsIgnoreCase(cmd)) {\n+                handleInputSourceResponse(parms);\n+            } else if (INPUT.equalsIgnoreCase(cmd)) {\n+                handleInputResponse(parms);\n+            } else if (PICTURE_MUTE.equalsIgnoreCase(cmd)) {\n+                handlePictureMuteResponse(parms);\n+            } else if (TOGGLE_PICTURE_MUTE.equalsIgnoreCase(cmd)) {\n+                handleTogglePictureMuteResponse(parms);\n+            } else if (PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) {\n+                handlePictureInPictureResponse(parms);\n+            } else if (TOGGLE_PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) {\n+                handleTogglePictureInPictureResponse(parms);\n+            } else if (TOGGLE_PIP_POSITION.equalsIgnoreCase(cmd)) {\n+                handleTogglePIPPosition(parms);\n+            } else {\n+                logger.warn(\"Unknown command response '{}' to command '{}' \", response, command);\n+            }\n+        }\n+\n+    }\n+\n+    /**\n+     * Handles notification messages (*SN) and enquiry responses (*SE->*SA)\n+     *\n+     * @param m a non-null matcher\n+     * @param resp a possibly null, possibly empty response\n+     */\n+    private void handleNotification(Matcher m, String response) {\n+        if (m.groupCount() == 3) {\n+            final String cmd = m.group(2);\n+            final String parms = m.group(3);\n+\n+            if (POWER.equalsIgnoreCase(cmd)) {\n+                handlePowerNotification(parms);\n+            } else if (VOLUME.equalsIgnoreCase(cmd)) {\n+                handleAudioVolumeNotification(parms);\n+            } else if (AUDIO_MUTE.equalsIgnoreCase(cmd)) {\n+                handleAudioMuteNotification(parms);\n+            } else if (CHANNEL.equalsIgnoreCase(cmd)) {\n+                handleChannelNotification(parms);\n+            } else if (TRIPLET_CHANNEL.equalsIgnoreCase(cmd)) {\n+                handleTripletChannelNotification(parms);\n+            } else if (INPUT_SOURCE.equalsIgnoreCase(cmd)) {\n+                handleInputSourceNotification(parms);\n+            } else if (INPUT.equalsIgnoreCase(cmd)) {\n+                handleInputNotification(parms);\n+            } else if (PICTURE_MUTE.equalsIgnoreCase(cmd)) {\n+                handlePictureMuteNotification(parms);\n+            } else if (PICTURE_IN_PICTURE.equalsIgnoreCase(cmd)) {\n+                handlePictureInPictureNotification(parms);\n+            } else if (BROADCAST_ADDRESS.equalsIgnoreCase(cmd)) {\n+                handleBroadcastAddressResponse(parms);\n+            } else if (MAC_ADDRESS.equalsIgnoreCase(cmd)) {\n+                handleMacAddressResponse(parms);\n+            } else {\n+                logger.warn(\"Unknown notification: \" + response);\n+            }\n+        }\n+\n+    }\n+\n+    /**\n+     * Handles the IRCC commadn response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleIRResponse(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(IRCC + \" command failed: \" + parms);\n+        } else if (RSP_SUCCESS.equals(parms)) {\n+            // success!\n+        } else {\n+            logger.warn(\"Unknown \" + IRCC + \" response: \" + parms);\n+        }\n+    }\n+\n+    /**\n+     * Handles the POWR command response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handlePowerResponse(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(POWER + \" command failed: \" + parms);\n+        } else if (RSP_SUCCESS.equals(parms)) {\n+            // success!\n+        } else {\n+            logger.warn(\"Unknown \" + POWER + \" response: \" + parms);\n+        }\n+    }\n+\n+    /**\n+     * Handles the power notification/enquiry response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handlePowerNotification(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(\"Error issuing power\");\n+        } else {\n+            try {\n+                final int power = Integer.parseInt(parms);\n+                if (power == 0) {\n+                    _callback.stateChanged(BraviaConstants.CHANNEL_POWER, OnOffType.OFF);\n+                } else if (power == 1) {\n+                    _callback.stateChanged(BraviaConstants.CHANNEL_POWER, OnOffType.ON);\n+                } else {\n+                    logger.warn(\"Unknown power response: \" + parms);\n+                }\n+            } catch (NumberFormatException e) {\n+                logger.warn(\"Unparsable power response: \" + parms);\n+            }\n+\n+            refreshState();\n+        }\n+    }\n+\n+    /**\n+     * Handles the audio volume command response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleAudioVolumeResponse(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(VOLUME + \" command failed: \" + parms);\n+        } else if (RSP_SUCCESS.equals(parms)) {\n+            // success!\n+        } else {\n+            logger.warn(\"Unknown \" + VOLUME + \" response: \" + parms);\n+        }\n+    }\n+\n+    /**\n+     * Handles the audio volume notification/enquiry response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleAudioVolumeNotification(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(\"Error issuing audio volume\");\n+        } else {\n+            try {\n+                final int volume = Integer.parseInt(parms);\n+                _callback.stateChanged(BraviaConstants.CHANNEL_VOLUME, new PercentType(volume));\n+            } catch (NumberFormatException e) {\n+                logger.warn(\"Unparsable audio volume response: \" + parms);\n+            }\n+        }\n+    }\n+\n+    /**\n+     * Handles the audio mute command response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleAudioMuteResponse(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(AUDIO_MUTE + \" command failed: \" + parms);\n+        } else if (RSP_SUCCESS.equals(parms)) {\n+            // success!\n+        } else {\n+            logger.warn(\"Unknown \" + AUDIO_MUTE + \" response: \" + parms);\n+        }\n+    }\n+\n+    /**\n+     * Handles the audio mute notification/enquiry response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleAudioMuteNotification(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            logger.debug(\"Error issuing channel\");\n+        } else {\n+            try {\n+                final int mute = Integer.parseInt(parms);\n+                if (mute == 0) {\n+                    _callback.stateChanged(BraviaConstants.CHANNEL_AUDIOMUTE, OnOffType.OFF);\n+                } else if (mute == 1) {\n+                    _callback.stateChanged(BraviaConstants.CHANNEL_AUDIOMUTE, OnOffType.ON);\n+                } else {\n+                    logger.warn(\"Unknown audio mute response: \" + parms);\n+                }\n+            } catch (NumberFormatException e) {\n+                logger.warn(\"Unparsable audio mute response: \" + parms);\n+            }\n+        }\n+    }\n+\n+    /**\n+     * Handles the channel command response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleChannelResponse(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            _callback.stateChanged(BraviaConstants.CHANNEL_CHANNEL, new StringType(\"\"));\n+        } else if (RSP_SUCCESS.equals(parms)) {\n+            // success!\n+        } else if (RSP_NOSUCHTHING.equals(parms)) {\n+            _callback.stateChanged(BraviaConstants.CHANNEL_CHANNEL, new StringType(\"\"));\n+        } else {\n+            logger.warn(\"Unknown \" + CHANNEL + \" response: \" + parms);\n+        }\n+    }\n+\n+    /**\n+     * Handles the channel notification/enquiry response\n+     *\n+     * @param parms a possibly null, possibly empty response\n+     */\n+    private void handleChannelNotification(String parms) {\n+        if (RSP_ERROR.equals(parms)) {\n+            _callback.stateChanged(BraviaConstants.CHANNEL_CHANNEL, new StringType(\"\"));\n+        } else {\n+            try {\n+                final int pre = Integer.parseInt(parms.substring(0, 8));\n+                final int post = Integer.parseInt(StringUtils.stripEnd(parms.substring(9), \"0\"));\n+                _callback.stateChanged(BraviaConstants.CHANNEL_CHANNEL, new StringType(pre + \".\" + post));\n+                refreshTripletChannel();\n+            } catch (NumberFormatException e) {\n+                logger.warn(\"Unparsable channel response: \" + parms);","path":"addons/binding/org.openhab.binding.sony/src/main/java/org/openhab/binding/sony/internal/bravia/BraviaProtocol.java","position":802,"original_position":802,"commit_id":"e305e48a74f1abae37cb0d0ae791d7aee8555646","original_commit_id":"e305e48a74f1abae37cb0d0ae791d7aee8555646","user":{"login":"kaikreuzer","id":3244965,"avatar_url":"https://avatars.githubusercontent.com/u/3244965?v=3","gravatar_id":"","url":"https://api.github.com/users/kaikreuzer","html_url":"https://github.com/kaikreuzer","followers_url":"https://api.github.com/users/kaikreuzer/followers","following_url":"https://api.github.com/users/kaikreuzer/following{/other_user}","gists_url":"https://api.github.com/users/kaikreuzer/gists{/gist_id}","starred_url":"https://api.github.com/users/kaikreuzer/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kaikreuzer/subscriptions","organizations_url":"https://api.github.com/users/kaikreuzer/orgs","repos_url":"https://api.github.com/users/kaikreuzer/repos","events_url":"https://api.github.com/users/kaikreuzer/events{/privacy}","received_events_url":"https://api.github.com/users/kaikreuzer/received_events","type":"User","site_admin":false},"body":"change to parameterized logging","created_at":"2016-12-27T16:43:21Z","updated_at":"2016-12-27T16:51:03Z","html_url":"https://github.com/openhab/openhab2-addons/pull/1249#discussion_r93948752","pull_request_url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249","_links":{"self":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/comments/93948752"},"html":{"href":"https://github.com/openhab/openhab2-addons/pull/1249#discussion_r93948752"},"pull_request":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249"}}},"pull_request":{"url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249","id":86223350,"html_url":"https://github.com/openhab/openhab2-addons/pull/1249","diff_url":"https://github.com/openhab/openhab2-addons/pull/1249.diff","patch_url":"https://github.com/openhab/openhab2-addons/pull/1249.patch","issue_url":"https://api.github.com/repos/openhab/openhab2-addons/issues/1249","number":1249,"state":"open","locked":false,"title":"New sony binding - simpleip (bravia) and IRCC","user":{"login":"tmrobert8","id":20641634,"avatar_url":"https://avatars.githubusercontent.com/u/20641634?v=3","gravatar_id":"","url":"https://api.github.com/users/tmrobert8","html_url":"https://github.com/tmrobert8","followers_url":"https://api.github.com/users/tmrobert8/followers","following_url":"https://api.github.com/users/tmrobert8/following{/other_user}","gists_url":"https://api.github.com/users/tmrobert8/gists{/gist_id}","starred_url":"https://api.github.com/users/tmrobert8/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmrobert8/subscriptions","organizations_url":"https://api.github.com/users/tmrobert8/orgs","repos_url":"https://api.github.com/users/tmrobert8/repos","events_url":"https://api.github.com/users/tmrobert8/events{/privacy}","received_events_url":"https://api.github.com/users/tmrobert8/received_events","type":"User","site_admin":false},"body":"New Sony binding\n\nSigned-off-by: Tim Roberts troberts@bigfoot.com\n","created_at":"2016-09-21T19:11:23Z","updated_at":"2016-12-27T16:51:04Z","closed_at":null,"merged_at":null,"merge_commit_sha":"660f1f1061d4475e43b46e46e2ed276b2574d007","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249/commits","review_comments_url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249/comments","review_comment_url":"https://api.github.com/repos/openhab/openhab2-addons/pulls/comments{/number}","comments_url":"https://api.github.com/repos/openhab/openhab2-addons/issues/1249/comments","statuses_url":"https://api.github.com/repos/openhab/openhab2-addons/statuses/e305e48a74f1abae37cb0d0ae791d7aee8555646","head":{"label":"tmrobert8:1231-Sony","ref":"1231-Sony","sha":"e305e48a74f1abae37cb0d0ae791d7aee8555646","user":{"login":"tmrobert8","id":20641634,"avatar_url":"https://avatars.githubusercontent.com/u/20641634?v=3","gravatar_id":"","url":"https://api.github.com/users/tmrobert8","html_url":"https://github.com/tmrobert8","followers_url":"https://api.github.com/users/tmrobert8/followers","following_url":"https://api.github.com/users/tmrobert8/following{/other_user}","gists_url":"https://api.github.com/users/tmrobert8/gists{/gist_id}","starred_url":"https://api.github.com/users/tmrobert8/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmrobert8/subscriptions","organizations_url":"https://api.github.com/users/tmrobert8/orgs","repos_url":"https://api.github.com/users/tmrobert8/repos","events_url":"https://api.github.com/users/tmrobert8/events{/privacy}","received_events_url":"https://api.github.com/users/tmrobert8/received_events","type":"User","site_admin":false},"repo":{"id":68837270,"name":"openhab2-addons","full_name":"tmrobert8/openhab2-addons","owner":{"login":"tmrobert8","id":20641634,"avatar_url":"https://avatars.githubusercontent.com/u/20641634?v=3","gravatar_id":"","url":"https://api.github.com/users/tmrobert8","html_url":"https://github.com/tmrobert8","followers_url":"https://api.github.com/users/tmrobert8/followers","following_url":"https://api.github.com/users/tmrobert8/following{/other_user}","gists_url":"https://api.github.com/users/tmrobert8/gists{/gist_id}","starred_url":"https://api.github.com/users/tmrobert8/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tmrobert8/subscriptions","organizations_url":"https://api.github.com/users/tmrobert8/orgs","repos_url":"https://api.github.com/users/tmrobert8/repos","events_url":"https://api.github.com/users/tmrobert8/events{/privacy}","received_events_url":"https://api.github.com/users/tmrobert8/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/tmrobert8/openhab2-addons","description":"The next-generation open Home Automation Bus (openHAB)","fork":true,"url":"https://api.github.com/repos/tmrobert8/openhab2-addons","forks_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/forks","keys_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/teams","hooks_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/hooks","issue_events_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/issues/events{/number}","events_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/events","assignees_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/assignees{/user}","branches_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/branches{/branch}","tags_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/tags","blobs_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/git/refs{/sha}","trees_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/statuses/{sha}","languages_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/languages","stargazers_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/stargazers","contributors_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/contributors","subscribers_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/subscribers","subscription_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/subscription","commits_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/commits{/sha}","git_commits_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/git/commits{/sha}","comments_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/comments{/number}","issue_comment_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/issues/comments{/number}","contents_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/contents/{+path}","compare_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/merges","archive_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/downloads","issues_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/issues{/number}","pulls_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/pulls{/number}","milestones_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/milestones{/number}","notifications_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/labels{/name}","releases_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/releases{/id}","deployments_url":"https://api.github.com/repos/tmrobert8/openhab2-addons/deployments","created_at":"2016-09-21T16:39:11Z","updated_at":"2016-09-21T16:39:16Z","pushed_at":"2016-10-07T17:29:32Z","git_url":"git://github.com/tmrobert8/openhab2-addons.git","ssh_url":"git@github.com:tmrobert8/openhab2-addons.git","clone_url":"https://github.com/tmrobert8/openhab2-addons.git","svn_url":"https://github.com/tmrobert8/openhab2-addons","homepage":null,"size":48764,"stargazers_count":0,"watchers_count":0,"language":"Java","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"openhab:master","ref":"master","sha":"45ad8026654d839d8acf9dda627de6ad4466a6a1","user":{"login":"openhab","id":1007353,"avatar_url":"https://avatars.githubusercontent.com/u/1007353?v=3","gravatar_id":"","url":"https://api.github.com/users/openhab","html_url":"https://github.com/openhab","followers_url":"https://api.github.com/users/openhab/followers","following_url":"https://api.github.com/users/openhab/following{/other_user}","gists_url":"https://api.github.com/users/openhab/gists{/gist_id}","starred_url":"https://api.github.com/users/openhab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/openhab/subscriptions","organizations_url":"https://api.github.com/users/openhab/orgs","repos_url":"https://api.github.com/users/openhab/repos","events_url":"https://api.github.com/users/openhab/events{/privacy}","received_events_url":"https://api.github.com/users/openhab/received_events","type":"Organization","site_admin":false},"repo":{"id":19753195,"name":"openhab2-addons","full_name":"openhab/openhab2-addons","owner":{"login":"openhab","id":1007353,"avatar_url":"https://avatars.githubusercontent.com/u/1007353?v=3","gravatar_id":"","url":"https://api.github.com/users/openhab","html_url":"https://github.com/openhab","followers_url":"https://api.github.com/users/openhab/followers","following_url":"https://api.github.com/users/openhab/following{/other_user}","gists_url":"https://api.github.com/users/openhab/gists{/gist_id}","starred_url":"https://api.github.com/users/openhab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/openhab/subscriptions","organizations_url":"https://api.github.com/users/openhab/orgs","repos_url":"https://api.github.com/users/openhab/repos","events_url":"https://api.github.com/users/openhab/events{/privacy}","received_events_url":"https://api.github.com/users/openhab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/openhab/openhab2-addons","description":"The next-generation open Home Automation Bus (openHAB)","fork":false,"url":"https://api.github.com/repos/openhab/openhab2-addons","forks_url":"https://api.github.com/repos/openhab/openhab2-addons/forks","keys_url":"https://api.github.com/repos/openhab/openhab2-addons/keys{/key_id}","collaborators_url":"https://api.github.com/repos/openhab/openhab2-addons/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/openhab/openhab2-addons/teams","hooks_url":"https://api.github.com/repos/openhab/openhab2-addons/hooks","issue_events_url":"https://api.github.com/repos/openhab/openhab2-addons/issues/events{/number}","events_url":"https://api.github.com/repos/openhab/openhab2-addons/events","assignees_url":"https://api.github.com/repos/openhab/openhab2-addons/assignees{/user}","branches_url":"https://api.github.com/repos/openhab/openhab2-addons/branches{/branch}","tags_url":"https://api.github.com/repos/openhab/openhab2-addons/tags","blobs_url":"https://api.github.com/repos/openhab/openhab2-addons/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/openhab/openhab2-addons/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/openhab/openhab2-addons/git/refs{/sha}","trees_url":"https://api.github.com/repos/openhab/openhab2-addons/git/trees{/sha}","statuses_url":"https://api.github.com/repos/openhab/openhab2-addons/statuses/{sha}","languages_url":"https://api.github.com/repos/openhab/openhab2-addons/languages","stargazers_url":"https://api.github.com/repos/openhab/openhab2-addons/stargazers","contributors_url":"https://api.github.com/repos/openhab/openhab2-addons/contributors","subscribers_url":"https://api.github.com/repos/openhab/openhab2-addons/subscribers","subscription_url":"https://api.github.com/repos/openhab/openhab2-addons/subscription","commits_url":"https://api.github.com/repos/openhab/openhab2-addons/commits{/sha}","git_commits_url":"https://api.github.com/repos/openhab/openhab2-addons/git/commits{/sha}","comments_url":"https://api.github.com/repos/openhab/openhab2-addons/comments{/number}","issue_comment_url":"https://api.github.com/repos/openhab/openhab2-addons/issues/comments{/number}","contents_url":"https://api.github.com/repos/openhab/openhab2-addons/contents/{+path}","compare_url":"https://api.github.com/repos/openhab/openhab2-addons/compare/{base}...{head}","merges_url":"https://api.github.com/repos/openhab/openhab2-addons/merges","archive_url":"https://api.github.com/repos/openhab/openhab2-addons/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/openhab/openhab2-addons/downloads","issues_url":"https://api.github.com/repos/openhab/openhab2-addons/issues{/number}","pulls_url":"https://api.github.com/repos/openhab/openhab2-addons/pulls{/number}","milestones_url":"https://api.github.com/repos/openhab/openhab2-addons/milestones{/number}","notifications_url":"https://api.github.com/repos/openhab/openhab2-addons/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/openhab/openhab2-addons/labels{/name}","releases_url":"https://api.github.com/repos/openhab/openhab2-addons/releases{/id}","deployments_url":"https://api.github.com/repos/openhab/openhab2-addons/deployments","created_at":"2014-05-13T19:41:58Z","updated_at":"2016-12-27T08:06:42Z","pushed_at":"2016-12-27T16:15:10Z","git_url":"git://github.com/openhab/openhab2-addons.git","ssh_url":"git@github.com:openhab/openhab2-addons.git","clone_url":"https://github.com/openhab/openhab2-addons.git","svn_url":"https://github.com/openhab/openhab2-addons","homepage":null,"size":58491,"stargazers_count":460,"watchers_count":460,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":733,"mirror_url":null,"open_issues_count":181,"forks":733,"open_issues":181,"watchers":460,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249"},"html":{"href":"https://github.com/openhab/openhab2-addons/pull/1249"},"issue":{"href":"https://api.github.com/repos/openhab/openhab2-addons/issues/1249"},"comments":{"href":"https://api.github.com/repos/openhab/openhab2-addons/issues/1249/comments"},"review_comments":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249/comments"},"review_comment":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/openhab/openhab2-addons/pulls/1249/commits"},"statuses":{"href":"https://api.github.com/repos/openhab/openhab2-addons/statuses/e305e48a74f1abae37cb0d0ae791d7aee8555646"}}}}
{"actor":{"display_login":"kaikreuzer"}}