2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.sleepiq.internal.handler;
15 import static org.openhab.binding.sleepiq.internal.SleepIQBindingConstants.THING_TYPE_CLOUD;
16 import static org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration.*;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.List;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import javax.ws.rs.client.ClientBuilder;
30 import org.openhab.binding.sleepiq.api.Configuration;
31 import org.openhab.binding.sleepiq.api.LoginException;
32 import org.openhab.binding.sleepiq.api.SleepIQ;
33 import org.openhab.binding.sleepiq.api.UnauthorizedException;
34 import org.openhab.binding.sleepiq.api.model.Bed;
35 import org.openhab.binding.sleepiq.api.model.BedStatus;
36 import org.openhab.binding.sleepiq.api.model.FamilyStatus;
37 import org.openhab.binding.sleepiq.internal.SleepIQBindingConstants;
38 import org.openhab.binding.sleepiq.internal.SleepIQConfigStatusMessage;
39 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
40 import org.openhab.core.cache.ExpiringCache;
41 import org.openhab.core.config.core.status.ConfigStatusMessage;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
49 import org.openhab.core.types.Command;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
55 * sent to one of the channels.
57 * @author Gregory Moyer - Initial contribution
59 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
60 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
62 private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
64 private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
66 private ExpiringCache<FamilyStatus> statusCache;
68 private ScheduledFuture<?> pollingJob;
70 private SleepIQ cloud;
72 private ClientBuilder clientBuilder;
74 public SleepIQCloudHandler(final Bridge bridge, ClientBuilder clientBuilder) {
76 this.clientBuilder = clientBuilder;
80 public void initialize() {
82 logger.debug("Configuring bed status cache");
83 statusCache = new ExpiringCache<>(TimeUnit.SECONDS.toMillis(getPollingInterval() / 2),
84 () -> cloud.getFamilyStatus());
86 createCloudConnection();
88 logger.debug("Setting SleepIQ cloud online");
89 updateListenerManagement();
90 updateStatus(ThingStatus.ONLINE);
91 } catch (UnauthorizedException e) {
92 logger.debug("SleepIQ cloud authentication failed", e);
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
94 } catch (LoginException e) {
95 logger.debug("SleepIQ cloud login failed", e);
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
97 "SleepIQ cloud login failed: " + e.getMessage());
98 } catch (Exception e) {
99 logger.debug("Unexpected error while communicating with SleepIQ cloud", e);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
101 "Unable to connect to SleepIQ cloud: " + e.getMessage());
106 * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
108 * @param clientBuilder2
110 * @throws LoginException if there is an error while authenticating to the service
112 private void createCloudConnection() throws LoginException {
113 logger.debug("Reading SleepIQ cloud binding configuration");
114 SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
116 logger.debug("Creating SleepIQ client");
117 Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
118 .withPassword(bindingConfig.password).withLogging(logger.isDebugEnabled());
119 cloud = SleepIQ.create(cloudConfig, clientBuilder);
121 logger.debug("Authenticating at the SleepIQ cloud service");
124 logger.info("Successfully authenticated at the SleepIQ cloud service");
128 public synchronized void dispose() {
129 logger.debug("Disposing SleepIQ cloud handler");
131 if (pollingJob != null && !pollingJob.isCancelled()) {
132 pollingJob.cancel(true);
138 * Start or stop a background polling job to look for bed status updates based on whether or not there are any
139 * listeners to notify.
141 private synchronized void updateListenerManagement() {
142 if (!bedStatusListeners.isEmpty() && (pollingJob == null || pollingJob.isCancelled())) {
143 int pollingInterval = getPollingInterval();
144 pollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval, pollingInterval,
146 } else if (bedStatusListeners.isEmpty() && pollingJob != null && !pollingJob.isCancelled()) {
147 pollingJob.cancel(true);
153 * Retrieve the polling interval for updating bed status.
155 * @return the polling interval in seconds
157 private int getPollingInterval() {
158 return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
162 * Retrieve the latest status on all beds and update all registered listeners.
164 public void refreshBedStatus() {
166 FamilyStatus status = statusCache.getValue();
167 updateStatus(ThingStatus.ONLINE);
169 for (BedStatus bedStatus : status.getBeds()) {
170 bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(cloud, bedStatus));
172 } catch (Exception e) {
173 logger.debug("Unexpected error while communicating with SleepIQ cloud", e);
174 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
175 "Unable to connect to SleepIQ cloud: " + e.getMessage());
180 * Register the given listener to receive bed status updates.
182 * @param listener the listener to register
184 public void registerBedStatusListener(final BedStatusListener listener) {
185 if (listener == null) {
189 bedStatusListeners.add(listener);
191 updateListenerManagement();
195 * Unregister the given listener from further bed status updates.
197 * @param listener the listener to unregister
198 * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
201 public boolean unregisterBedStatusListener(final BedStatusListener listener) {
202 boolean result = bedStatusListeners.remove(listener);
204 updateListenerManagement();
211 public void handleCommand(final ChannelUID channelUID, final Command command) {
212 // cloud handler has no channels
216 public Collection<ConfigStatusMessage> getConfigStatus() {
217 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
219 SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
220 String username = config.username;
221 String password = config.password;
223 if (username.isEmpty()) {
224 configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
225 .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
228 if (password.isEmpty()) {
229 configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
230 .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
233 return configStatusMessages;
237 * Get a list of all beds registered to the cloud service account.
239 * @return the list of beds (never <code>null</code>)
241 public List<Bed> getBeds() {
242 return cloud.getBeds();
246 * Get the {@link Bed} corresponding to the given identifier.
248 * @param bedId the bed identifier
249 * @return the identified {@link Bed} or <code>null</code> if no such bed exists
251 public Bed getBed(final String bedId) {
252 for (Bed bed : getBeds()) {
253 if (bedId.equals(bed.getBedId())) {
262 * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
265 * @param bed the source of data
266 * @param properties the properties to update (this may be <code>null</code>)
267 * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
269 public Map<String, String> updateProperties(final Bed bed, Map<String, String> properties) {
271 properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
272 properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
273 if (bed.isKidsBed() != null) {
274 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
276 properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
277 properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
278 if (bed.getPurchaseDate() != null) {
279 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
281 properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
282 properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());