diff --git a/bundles/org.openhab.binding.sleepiq/.classpath b/bundles/org.openhab.binding.sleepiq/.classpath
index a5d95095ccaaf..9465c8288ee24 100644
--- a/bundles/org.openhab.binding.sleepiq/.classpath
+++ b/bundles/org.openhab.binding.sleepiq/.classpath
@@ -11,11 +11,13 @@
-
+
+
+
+
-
diff --git a/bundles/org.openhab.binding.sleepiq/NOTICE b/bundles/org.openhab.binding.sleepiq/NOTICE
index 2713faafa7fb6..75446268e8643 100644
--- a/bundles/org.openhab.binding.sleepiq/NOTICE
+++ b/bundles/org.openhab.binding.sleepiq/NOTICE
@@ -14,12 +14,11 @@ https://github.com/openhab/openhab-addons
== Third-party Content
-libsleepiq-java
+The sources of libsleepiq-java are included in src/3rdparty/java and have been slightly adapted.
* License: Apache 2.0 License
* Project: https://github.com/syphr42/libsleepiq-java
* Source: https://github.com/syphr42/libsleepiq-java
-provider-gson
-* License: EPL 1.0 License
-* Project: https://github.com/hstaudacher/osgi-jax-rs-connector
-* Source: https://github.com/hstaudacher/osgi-jax-rs-connector
+This binding includes a class from Jersey in the src/3rdparty/java folder.
+* License: CDDL License
+* Project: https://eclipse-ee4j.github.io/jersey/
diff --git a/bundles/org.openhab.binding.sleepiq/pom.xml b/bundles/org.openhab.binding.sleepiq/pom.xml
index f6ff6ce2f8aa5..698d1850e10dd 100644
--- a/bundles/org.openhab.binding.sleepiq/pom.xml
+++ b/bundles/org.openhab.binding.sleepiq/pom.xml
@@ -20,17 +20,45 @@
- org.syphr
- sleepiq-api
- 0.3.0
- compile
-
-
- com.eclipsesource.jaxrs
- provider-gson
- 2.3
- compile
+ junit
+ junit
+ 4.12
+ test
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+
+ add-source
+
+ generate-sources
+
+
+
+
+
+
+
+ add-test-source
+ generate-sources
+
+ add-test-source
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/BedNotFoundException.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/BedNotFoundException.java
new file mode 100644
index 0000000000000..3eba856849511
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/BedNotFoundException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import org.openhab.binding.sleepiq.api.model.Failure;
+
+public class BedNotFoundException extends SleepIQException
+{
+ private static final long serialVersionUID = 1L;
+
+ public BedNotFoundException(Failure failure)
+ {
+ super(failure);
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/Configuration.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/Configuration.java
new file mode 100644
index 0000000000000..cb0fd28ba4a7d
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/Configuration.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import java.net.URI;
+import java.util.logging.Level;
+
+/**
+ * This class represents configuration parameters for using {@link SleepIQ}.
+ *
+ * @author Gregory Moyer
+ */
+public class Configuration
+{
+ private String username;
+ private String password;
+
+ private URI baseUri = URI.create("https://api.sleepiq.sleepnumber.com/rest");
+
+ private boolean logging = false;
+
+ /**
+ * Get the username on the account.
+ *
+ * @return the username
+ */
+ public String getUsername()
+ {
+ return username;
+ }
+
+ /**
+ * Set the username on the account. This should be the username used to
+ * register with SleepIQ.
+ *
+ * @param username
+ * the value to set
+ */
+ public void setUsername(String username)
+ {
+ this.username = username;
+ }
+
+ /**
+ * Set the username on the account. This should be the username used to
+ * register with SleepIQ.
+ *
+ * @param username
+ * the value to set
+ * @return this configuration instance
+ */
+ public Configuration withUsername(String username)
+ {
+ setUsername(username);
+ return this;
+ }
+
+ /**
+ * Get the password on the account.
+ *
+ * @return the password
+ */
+ public String getPassword()
+ {
+ return password;
+ }
+
+ /**
+ * Set the password on the account. This should be the password used to
+ * register with SleepIQ.
+ *
+ * @param password
+ * the value to set
+ */
+ public void setPassword(String password)
+ {
+ this.password = password;
+ }
+
+ /**
+ * Set the password on the account. This should be the password used to
+ * register with SleepIQ.
+ *
+ * @param password
+ * the value to set
+ * @return this configuration instance
+ */
+ public Configuration withPassword(String password)
+ {
+ setPassword(password);
+ return this;
+ }
+
+ /**
+ * Get the base URI of the SleepIQ cloud service.
+ *
+ * @return the base URI
+ */
+ public URI getBaseUri()
+ {
+ return baseUri;
+ }
+
+ /**
+ * Set the base URI of the SleepIQ cloud service. It is unlikely that this
+ * will need to be changed from its default value.
+ *
+ * @param baseUri
+ * the value to set
+ */
+ public void setBaseUri(URI baseUri)
+ {
+ this.baseUri = baseUri;
+ }
+
+ /**
+ * Set the base URI of the SleepIQ cloud service. It is unlikely that this
+ * will need to be changed from its default value.
+ *
+ * @param baseUri
+ * the value to set
+ * @return this configuration instance
+ */
+ public Configuration withBaseUri(URI baseUri)
+ {
+ setBaseUri(baseUri);
+ return this;
+ }
+
+ /**
+ * Get the logging flag.
+ *
+ * @return the logging flag
+ */
+ public boolean isLogging()
+ {
+ return logging;
+ }
+
+ /**
+ * Set the logging flag. When this is set to true, all requests
+ * and responses will be logged at the {@link Level#INFO} level. This
+ * includes usernames and passwords!
+ *
+ * @param logging
+ * the value to set
+ */
+ public void setLogging(boolean logging)
+ {
+ this.logging = logging;
+ }
+
+ /**
+ * Set the logging flag. When this is set to true, all requests
+ * and responses will be logged at the {@link Level#INFO} level. This
+ * includes usernames and passwords!
+ *
+ * @param logging
+ * the value to set
+ * @return this configuration instance
+ */
+ public Configuration withLogging(boolean logging)
+ {
+ setLogging(logging);
+ return this;
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/LoginException.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/LoginException.java
new file mode 100644
index 0000000000000..6480ae4ffd083
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/LoginException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import org.openhab.binding.sleepiq.api.model.Failure;
+
+public class LoginException extends SleepIQException
+{
+ private static final long serialVersionUID = 1L;
+
+ public LoginException(Failure failure)
+ {
+ super(failure);
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQ.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQ.java
new file mode 100644
index 0000000000000..dcb01b80f7040
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQ.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import java.util.List;
+
+import org.openhab.binding.sleepiq.api.impl.SleepIQImpl;
+import org.openhab.binding.sleepiq.api.model.Bed;
+import org.openhab.binding.sleepiq.api.model.FamilyStatus;
+import org.openhab.binding.sleepiq.api.model.LoginInfo;
+import org.openhab.binding.sleepiq.api.model.PauseMode;
+import org.openhab.binding.sleepiq.api.model.Sleeper;
+
+/**
+ * This interface is the main API to access the SleepIQ system.
+ *
+ * @author Gregory Moyer
+ */
+public interface SleepIQ
+{
+ /**
+ * Login to the {@link Configuration configured} account. This method is not
+ * required to be called before other methods because all methods must
+ * ensure login before acting. However, when the only desired action is to
+ * login and not retrieve other data, this method is the most efficient
+ * option.
+ *
+ * @return basic information about the logged in user
+ * @throws UnauthorizedException
+ * if the credentials provided are not valid
+ * @throws LoginException
+ * if the login request fails for any reason other than bad
+ * credentials (including missing credentials)
+ */
+ public LoginInfo login() throws LoginException;
+
+ /**
+ * Get a list of beds connected to the account.
+ *
+ * @return the list of beds
+ */
+ public List getBeds();
+
+ /**
+ * Get a list of people registered to this account for beds or bed positions
+ * (left or right side).
+ *
+ * @return the list of sleepers
+ */
+ public List getSleepers();
+
+ /**
+ * Get the status of all beds and all air chambers registered to this
+ * account.
+ *
+ * @return the complete status of beds on the account
+ */
+ public FamilyStatus getFamilyStatus();
+
+ /**
+ * Get the status of "pause mode" (disabling SleepIQ data upload) for a
+ * specific bed. A bed in pause mode will send no information to the SleepIQ
+ * cloud services. For example, if a sleeper is in bed and disables SleepIQ
+ * (enables pause mode), the service will continue to report that the bed is
+ * occupied even after the sleeper exits the bed until pause mode is
+ * disabled.
+ *
+ * @param bedId
+ * the unique identifier of the bed to query
+ * @return the status of pause mode for the specified bed
+ * @throws BedNotFoundException
+ * if the bed identifier was not found on the account
+ */
+ public PauseMode getPauseMode(String bedId) throws BedNotFoundException;
+
+ /**
+ * Create a default implementation instance of this interface. Each call to
+ * this method will create a new object.
+ *
+ * @param config
+ * the configuration to use for the new instance
+ * @return a concrete implementation of this interface
+ */
+ public static SleepIQ create(Configuration config)
+ {
+ return new SleepIQImpl(config);
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQException.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQException.java
new file mode 100644
index 0000000000000..55935c1f7d2e5
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/SleepIQException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import org.openhab.binding.sleepiq.api.model.Failure;
+
+public class SleepIQException extends Exception
+{
+ private static final long serialVersionUID = 1L;
+
+ private final Failure failure;
+
+ public SleepIQException(Failure failure)
+ {
+ super(failure.getError().getMessage());
+ this.failure = failure;
+ }
+
+ public Failure getFailure()
+ {
+ return failure;
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/UnauthorizedException.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/UnauthorizedException.java
new file mode 100644
index 0000000000000..5a809005fa785
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/UnauthorizedException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api;
+
+import org.openhab.binding.sleepiq.api.model.Failure;
+
+public class UnauthorizedException extends LoginException
+{
+ private static final long serialVersionUID = 1L;
+
+ public UnauthorizedException(Failure failure)
+ {
+ super(failure);
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/filter/LoggingFilter.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/filter/LoggingFilter.java
new file mode 100644
index 0000000000000..5ce38e5ef3903
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/filter/LoggingFilter.java
@@ -0,0 +1,355 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright (c) 2011-2015 Oracle and/or its affiliates. All rights reserved.
+ *
+ * The contents of this file are subject to the terms of either the GNU
+ * General Public License Version 2 only ("GPL") or the Common Development
+ * and Distribution License("CDDL") (collectively, the "License"). You
+ * may not use this file except in compliance with the License. You can
+ * obtain a copy of the License at
+ * http://glassfish.java.net/public/CDDL+GPL_1_1.html
+ * or packager/legal/LICENSE.txt. See the License for the specific
+ * language governing permissions and limitations under the License.
+ *
+ * When distributing the software, include this License Header Notice in each
+ * file and include the License file at packager/legal/LICENSE.txt.
+ *
+ * GPL Classpath Exception:
+ * Oracle designates this particular file as subject to the "Classpath"
+ * exception as provided by Oracle in the GPL Version 2 section of the License
+ * file that accompanied this code.
+ *
+ * Modifications:
+ * If applicable, add the following below the License Header, with the fields
+ * enclosed by brackets [] replaced by your own identifying information:
+ * "Portions Copyright [year] [name of copyright owner]"
+ *
+ * Contributor(s):
+ * If you wish your version of this file to be governed by only the CDDL or
+ * only the GPL Version 2, indicate your decision by adding "[Contributor]
+ * elects to include this software in this distribution under the [CDDL or GPL
+ * Version 2] license." If you don't indicate a single choice of license, a
+ * recipient has the option to distribute your version of this file under
+ * either the CDDL, the GPL Version 2 or to extend the choice of license to
+ * its licensees as provided above. However, if you add GPL Version 2 code
+ * and therefore, elected the GPL Version 2 license, then the option applies
+ * only if the new code is made subject to such option by the copyright
+ * holder.
+ */
+package org.openhab.binding.sleepiq.api.filter;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+import javax.annotation.Priority;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.ClientResponseContext;
+import javax.ws.rs.client.ClientResponseFilter;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.container.ContainerResponseContext;
+import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.container.PreMatching;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.WriterInterceptor;
+import javax.ws.rs.ext.WriterInterceptorContext;
+
+/**
+ * Universal logging filter.
+ *
+ * Can be used on client or server side. Has the highest priority.
+ *
+ * @author Pavel Bucek (pavel.bucek at oracle.com)
+ * @author Martin Matula
+ */
+@PreMatching
+@Priority(Integer.MIN_VALUE)
+public final class LoggingFilter implements ContainerRequestFilter, ClientRequestFilter, ContainerResponseFilter,
+ ClientResponseFilter, WriterInterceptor {
+
+ public static final Charset UTF8 = Charset.forName("UTF-8");
+
+ private static final Logger LOGGER = Logger.getLogger(LoggingFilter.class.getName());
+ private static final String NOTIFICATION_PREFIX = "* ";
+ private static final String REQUEST_PREFIX = "> ";
+ private static final String RESPONSE_PREFIX = "< ";
+ private static final String ENTITY_LOGGER_PROPERTY = LoggingFilter.class.getName() + ".entityLogger";
+ private static final String LOGGING_ID_PROPERTY = LoggingFilter.class.getName() + ".id";
+
+ private static final Comparator>> COMPARATOR = new Comparator>>() {
+
+ @Override
+ public int compare(final Map.Entry> o1, final Map.Entry> o2) {
+ return o1.getKey().compareToIgnoreCase(o2.getKey());
+ }
+ };
+
+ private static final int DEFAULT_MAX_ENTITY_SIZE = 8 * 1024;
+
+ //
+ private final Logger logger;
+ private final AtomicLong _id = new AtomicLong(0);
+ private final boolean printEntity;
+ private final int maxEntitySize;
+
+ /**
+ * Create a logging filter logging the request and response to a default JDK
+ * logger, named as the fully qualified class name of this class. Entity
+ * logging is turned off by default.
+ */
+ public LoggingFilter() {
+ this(LOGGER, false);
+ }
+
+ /**
+ * Create a logging filter with custom logger and custom settings of entity
+ * logging.
+ *
+ * @param logger the logger to log requests and responses.
+ * @param printEntity if true, entity will be logged as well up to the default maxEntitySize, which is 8KB
+ */
+ public LoggingFilter(final Logger logger, final boolean printEntity) {
+ this.logger = logger;
+ this.printEntity = printEntity;
+ this.maxEntitySize = DEFAULT_MAX_ENTITY_SIZE;
+ }
+
+ /**
+ * Creates a logging filter with custom logger and entity logging turned on, but potentially limiting the size
+ * of entity to be buffered and logged.
+ *
+ * @param logger the logger to log requests and responses.
+ * @param maxEntitySize maximum number of entity bytes to be logged (and buffered) - if the entity is larger,
+ * logging filter will print (and buffer in memory) only the specified number of bytes
+ * and print "...more..." string at the end. Negative values are interpreted as zero.
+ */
+ public LoggingFilter(final Logger logger, final int maxEntitySize) {
+ this.logger = logger;
+ this.printEntity = true;
+ this.maxEntitySize = Math.max(0, maxEntitySize);
+ }
+
+ private void log(final StringBuilder b) {
+ if (logger != null) {
+ logger.info(b.toString());
+ }
+ }
+
+ private StringBuilder prefixId(final StringBuilder b, final long id) {
+ b.append(Long.toString(id)).append(" ");
+ return b;
+ }
+
+ private void printRequestLine(final StringBuilder b, final String note, final long id, final String method,
+ final URI uri) {
+ prefixId(b, id).append(NOTIFICATION_PREFIX).append(note).append(" on thread ")
+ .append(Thread.currentThread().getName()).append("\n");
+ prefixId(b, id).append(REQUEST_PREFIX).append(method).append(" ").append(uri.toASCIIString()).append("\n");
+ }
+
+ private void printResponseLine(final StringBuilder b, final String note, final long id, final int status) {
+ prefixId(b, id).append(NOTIFICATION_PREFIX).append(note).append(" on thread ")
+ .append(Thread.currentThread().getName()).append("\n");
+ prefixId(b, id).append(RESPONSE_PREFIX).append(Integer.toString(status)).append("\n");
+ }
+
+ private void printPrefixedHeaders(final StringBuilder b, final long id, final String prefix,
+ final MultivaluedMap headers) {
+ for (final Map.Entry> headerEntry : getSortedHeaders(headers.entrySet())) {
+ final List> val = headerEntry.getValue();
+ final String header = headerEntry.getKey();
+
+ if (val.size() == 1) {
+ prefixId(b, id).append(prefix).append(header).append(": ").append(val.get(0)).append("\n");
+ } else {
+ final StringBuilder sb = new StringBuilder();
+ boolean add = false;
+ for (final Object s : val) {
+ if (add) {
+ sb.append(',');
+ }
+ add = true;
+ sb.append(s);
+ }
+ prefixId(b, id).append(prefix).append(header).append(": ").append(sb.toString()).append("\n");
+ }
+ }
+ }
+
+ private Set>> getSortedHeaders(final Set>> headers) {
+ final TreeSet>> sortedHeaders = new TreeSet>>(
+ COMPARATOR);
+ sortedHeaders.addAll(headers);
+ return sortedHeaders;
+ }
+
+ private InputStream logInboundEntity(final StringBuilder b, InputStream stream, final Charset charset)
+ throws IOException {
+ if (!stream.markSupported()) {
+ stream = new BufferedInputStream(stream);
+ }
+ stream.mark(maxEntitySize + 1);
+ final byte[] entity = new byte[maxEntitySize + 1];
+ final int entitySize = stream.read(entity);
+ b.append(new String(entity, 0, Math.min(entitySize, maxEntitySize), charset));
+ if (entitySize > maxEntitySize) {
+ b.append("...more...");
+ }
+ b.append('\n');
+ stream.reset();
+ return stream;
+ }
+
+ @Override
+ public void filter(final ClientRequestContext context) throws IOException {
+ final long id = _id.incrementAndGet();
+ context.setProperty(LOGGING_ID_PROPERTY, id);
+
+ final StringBuilder b = new StringBuilder();
+
+ printRequestLine(b, "Sending client request", id, context.getMethod(), context.getUri());
+ printPrefixedHeaders(b, id, REQUEST_PREFIX, context.getStringHeaders());
+
+ if (printEntity && context.hasEntity()) {
+ final OutputStream stream = new LoggingStream(b, context.getEntityStream());
+ context.setEntityStream(stream);
+ context.setProperty(ENTITY_LOGGER_PROPERTY, stream);
+ // not calling log(b) here - it will be called by the interceptor
+ } else {
+ log(b);
+ }
+ }
+
+ @Override
+ public void filter(final ClientRequestContext requestContext, final ClientResponseContext responseContext)
+ throws IOException {
+ final Object requestId = requestContext.getProperty(LOGGING_ID_PROPERTY);
+ final long id = requestId != null ? (Long) requestId : _id.incrementAndGet();
+
+ final StringBuilder b = new StringBuilder();
+
+ printResponseLine(b, "Client response received", id, responseContext.getStatus());
+ printPrefixedHeaders(b, id, RESPONSE_PREFIX, responseContext.getHeaders());
+
+ if (printEntity && responseContext.hasEntity()) {
+ responseContext.setEntityStream(
+ logInboundEntity(b, responseContext.getEntityStream(), getCharset(responseContext.getMediaType())));
+ }
+
+ log(b);
+ }
+
+ @Override
+ public void filter(final ContainerRequestContext context) throws IOException {
+ final long id = _id.incrementAndGet();
+ context.setProperty(LOGGING_ID_PROPERTY, id);
+
+ final StringBuilder b = new StringBuilder();
+
+ printRequestLine(b, "Server has received a request", id, context.getMethod(),
+ context.getUriInfo().getRequestUri());
+ printPrefixedHeaders(b, id, REQUEST_PREFIX, context.getHeaders());
+
+ if (printEntity && context.hasEntity()) {
+ context.setEntityStream(logInboundEntity(b, context.getEntityStream(), getCharset(context.getMediaType())));
+ }
+
+ log(b);
+ }
+
+ @Override
+ public void filter(final ContainerRequestContext requestContext, final ContainerResponseContext responseContext)
+ throws IOException {
+ final Object requestId = requestContext.getProperty(LOGGING_ID_PROPERTY);
+ final long id = requestId != null ? (Long) requestId : _id.incrementAndGet();
+
+ final StringBuilder b = new StringBuilder();
+
+ printResponseLine(b, "Server responded with a response", id, responseContext.getStatus());
+ printPrefixedHeaders(b, id, RESPONSE_PREFIX, responseContext.getStringHeaders());
+
+ if (printEntity && responseContext.hasEntity()) {
+ final OutputStream stream = new LoggingStream(b, responseContext.getEntityStream());
+ responseContext.setEntityStream(stream);
+ requestContext.setProperty(ENTITY_LOGGER_PROPERTY, stream);
+ // not calling log(b) here - it will be called by the interceptor
+ } else {
+ log(b);
+ }
+ }
+
+ @Override
+ public void aroundWriteTo(final WriterInterceptorContext writerInterceptorContext)
+ throws IOException, WebApplicationException {
+ final LoggingStream stream = (LoggingStream) writerInterceptorContext.getProperty(ENTITY_LOGGER_PROPERTY);
+ writerInterceptorContext.proceed();
+ if (stream != null) {
+ log(stream.getStringBuilder(getCharset(writerInterceptorContext.getMediaType())));
+ }
+ }
+
+ private class LoggingStream extends FilterOutputStream {
+
+ private final StringBuilder b;
+ private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ LoggingStream(final StringBuilder b, final OutputStream inner) {
+ super(inner);
+
+ this.b = b;
+ }
+
+ StringBuilder getStringBuilder(final Charset charset) {
+ // write entity to the builder
+ final byte[] entity = baos.toByteArray();
+
+ b.append(new String(entity, 0, Math.min(entity.length, maxEntitySize), charset));
+ if (entity.length > maxEntitySize) {
+ b.append("...more...");
+ }
+ b.append('\n');
+
+ return b;
+ }
+
+ @Override
+ public void write(final int i) throws IOException {
+ if (baos.size() <= maxEntitySize) {
+ baos.write(i);
+ }
+ out.write(i);
+ }
+ }
+
+ /**
+ * Get the character set from a media type.
+ *
+ * The character set is obtained from the media type parameter "charset".
+ * If the parameter is not present the {@link #UTF8} charset is utilized.
+ *
+ * @param m the media type.
+ * @return the character set.
+ */
+ public static Charset getCharset(MediaType m) {
+ String name = (m == null) ? null : m.getParameters().get(MediaType.CHARSET_PARAMETER);
+ return (name == null) ? UTF8 : Charset.forName(name);
+ }
+
+}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/AbstractClient.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/AbstractClient.java
new file mode 100644
index 0000000000000..7f5e3cbce948b
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/AbstractClient.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api.impl;
+
+import javax.ws.rs.client.Client;
+
+import com.google.gson.Gson;
+
+public abstract class AbstractClient
+{
+ private volatile Client client;
+ private volatile Gson gson;
+
+ protected Client getClient()
+ {
+ if (client == null)
+ {
+ synchronized (this)
+ {
+ if (client == null)
+ {
+ client = createClient();
+ }
+ }
+ }
+
+ return client;
+ }
+
+ protected Gson getGson()
+ {
+ if (gson == null)
+ {
+ synchronized (this)
+ {
+ if (gson == null)
+ {
+ gson = createGson();
+ }
+ }
+ }
+
+ return gson;
+ }
+
+ protected abstract Client createClient();
+
+ protected Gson createGson()
+ {
+ return GsonGenerator.create();
+ }
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/Endpoints.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/Endpoints.java
new file mode 100644
index 0000000000000..a1e8f348e4c3b
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/Endpoints.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api.impl;
+
+public class Endpoints
+{
+ private static final String LOGIN = "login";
+ private static final String BED = "bed";
+ private static final String SLEEPER = "sleeper";
+ private static final String FAMILY_STATUS = "familyStatus";
+ private static final String PAUSE_MODE = "pauseMode";
+
+ public static String login()
+ {
+ return LOGIN;
+ }
+
+ public static String bed()
+ {
+ return BED;
+ }
+
+ public static String sleeper()
+ {
+ return SLEEPER;
+ }
+
+ public static String familyStatus()
+ {
+ return FAMILY_STATUS;
+ }
+
+ public static String pauseMode()
+ {
+ return PAUSE_MODE;
+ }
+
+ // @formatter:off
+ private Endpoints() {}
+ // @formatter:on
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/GsonGenerator.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/GsonGenerator.java
new file mode 100644
index 0000000000000..fb67e9b0ac83f
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/GsonGenerator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api.impl;
+
+import org.openhab.binding.sleepiq.api.impl.typeadapters.JSR310TypeAdapters;
+import org.openhab.binding.sleepiq.api.impl.typeadapters.TimeSinceTypeAdapter;
+import org.openhab.binding.sleepiq.api.model.TimeSince;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public class GsonGenerator
+{
+ public static Gson create()
+ {
+ return create(false);
+ }
+
+ public static Gson create(boolean prettyPrint)
+ {
+ GsonBuilder builder = new GsonBuilder();
+
+ // add Java 8 Time API support
+ JSR310TypeAdapters.registerJSR310TypeAdapters(builder);
+
+ builder.registerTypeAdapter(TimeSince.class, new TimeSinceTypeAdapter());
+
+ if (prettyPrint)
+ {
+ builder.setPrettyPrinting();
+ }
+
+ return builder.create();
+ }
+
+ // @formatter:off
+ private GsonGenerator() {}
+ // @formatter:on
+}
diff --git a/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/SleepIQImpl.java b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/SleepIQImpl.java
new file mode 100644
index 0000000000000..666eb93388d92
--- /dev/null
+++ b/bundles/org.openhab.binding.sleepiq/src/3rdparty/java/org/openhab/binding/sleepiq/api/impl/SleepIQImpl.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2017 Gregory Moyer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openhab.binding.sleepiq.api.impl;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.openhab.binding.sleepiq.api.BedNotFoundException;
+import org.openhab.binding.sleepiq.api.Configuration;
+import org.openhab.binding.sleepiq.api.LoginException;
+import org.openhab.binding.sleepiq.api.SleepIQ;
+import org.openhab.binding.sleepiq.api.UnauthorizedException;
+import org.openhab.binding.sleepiq.api.filter.LoggingFilter;
+import org.openhab.binding.sleepiq.api.model.Bed;
+import org.openhab.binding.sleepiq.api.model.BedsResponse;
+import org.openhab.binding.sleepiq.api.model.Failure;
+import org.openhab.binding.sleepiq.api.model.FamilyStatus;
+import org.openhab.binding.sleepiq.api.model.LoginInfo;
+import org.openhab.binding.sleepiq.api.model.LoginRequest;
+import org.openhab.binding.sleepiq.api.model.PauseMode;
+import org.openhab.binding.sleepiq.api.model.Sleeper;
+import org.openhab.binding.sleepiq.api.model.SleepersResponse;
+import org.openhab.binding.sleepiq.internal.GsonProvider;
+
+public class SleepIQImpl extends AbstractClient implements SleepIQ
+{
+ protected static final String PARAM_KEY = "_k";
+
+ protected static final String DATA_BED_ID = "bedId";
+
+ protected final Configuration config;
+
+ private volatile LoginInfo loginInfo;
+
+ public SleepIQImpl(Configuration config)
+ {
+ this.config = config;
+ }
+
+ @Override
+ public LoginInfo login() throws LoginException
+ {
+ if (loginInfo == null)
+ {
+ synchronized (this)
+ {
+ if (loginInfo == null)
+ {
+ Response response = getClient().target(config.getBaseUri())
+ .path(Endpoints.login())
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .put(Entity.json(new LoginRequest().withLogin(config.getUsername())
+ .withPassword(config.getPassword())));
+
+ if (isUnauthorized(response))
+ {
+ throw new UnauthorizedException(response.readEntity(Failure.class));
+ }
+
+ if (!Status.Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily()))
+ {
+ throw new LoginException(response.readEntity(Failure.class));
+ }
+
+ // add the received cookies to all future requests
+ getClient().register(new ClientRequestFilter()
+ {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException
+ {
+ List