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 org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.openhab.binding.sleepiq.internal.SleepIQBindingConstants;
32 import org.openhab.binding.sleepiq.internal.SleepIQConfigStatusMessage;
33 import org.openhab.binding.sleepiq.internal.api.Configuration;
34 import org.openhab.binding.sleepiq.internal.api.LoginException;
35 import org.openhab.binding.sleepiq.internal.api.ResponseFormatException;
36 import org.openhab.binding.sleepiq.internal.api.SleepIQ;
37 import org.openhab.binding.sleepiq.internal.api.SleepIQException;
38 import org.openhab.binding.sleepiq.internal.api.UnauthorizedException;
39 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
40 import org.openhab.binding.sleepiq.internal.api.dto.BedStatus;
41 import org.openhab.binding.sleepiq.internal.api.dto.FamilyStatusResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.FoundationFeaturesResponse;
43 import org.openhab.binding.sleepiq.internal.api.dto.FoundationStatusResponse;
44 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
45 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
46 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuator;
47 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuatorSpeed;
48 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutlet;
49 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutletOperation;
50 import org.openhab.binding.sleepiq.internal.api.enums.FoundationPreset;
51 import org.openhab.binding.sleepiq.internal.api.enums.Side;
52 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
53 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
54 import org.openhab.core.config.core.status.ConfigStatusMessage;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.ThingTypeUID;
61 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
62 import org.openhab.core.types.Command;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
67 * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
68 * sent to one of the channels.
70 * @author Gregory Moyer - Initial contribution
73 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
74 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
76 private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
78 private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
80 private final HttpClient httpClient;
82 private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
84 private @Nullable ScheduledFuture<?> statusPollingJob;
85 private @Nullable ScheduledFuture<?> sleeperPollingJob;
87 private @Nullable SleepIQ cloud;
89 private @Nullable List<Sleeper> sleepers;
91 public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
93 this.httpClient = httpClient;
97 public void initialize() {
98 scheduler.execute(() -> {
100 createCloudConnection();
101 updateListenerManagement();
102 updateStatus(ThingStatus.ONLINE);
103 } catch (UnauthorizedException e) {
104 logger.debug("CloudHandler: SleepIQ cloud authentication failed", e);
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
106 } catch (LoginException e) {
107 logger.debug("CloudHandler: SleepIQ cloud login failed", e);
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
109 "SleepIQ cloud login failed: " + e.getMessage());
115 public synchronized void dispose() {
116 stopSleeperPollingJob();
117 stopStatusPollingJob();
124 public void handleCommand(final ChannelUID channelUID, final Command command) {
125 // cloud handler has no channels
129 * Validate the config from openHAB
131 * @return validity status of config parameters
134 public Collection<ConfigStatusMessage> getConfigStatus() {
135 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
136 SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
137 String username = config.username;
138 String password = config.password;
139 if (username.isBlank()) {
140 configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
141 .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
143 if (password.isBlank()) {
144 configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
145 .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
147 return configStatusMessages;
151 * Register the given listener to receive bed status updates.
153 * @param listener the listener to register
155 public void registerBedStatusListener(final BedStatusListener listener) {
156 bedStatusListeners.add(listener);
158 * Delay the initial sleeper and status update to give some time for the property update
159 * to determine if a foundation is installed.
161 scheduler.schedule(() -> {
164 updateListenerManagement();
165 }, 10L, TimeUnit.SECONDS);
169 * Unregister the given listener from further bed status updates.
171 * @param listener the listener to unregister
172 * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
175 public boolean unregisterBedStatusListener(final BedStatusListener listener) {
176 boolean result = bedStatusListeners.remove(listener);
178 updateListenerManagement();
184 * Get a list of all beds registered to the cloud service account.
186 * @return the list of beds or null if unable to get list
188 public @Nullable List<Bed> getBeds() {
190 return cloud.getBeds();
191 } catch (SleepIQException e) {
192 logger.debug("CloudHandler: Exception getting list of beds", e);
198 * Get the bed corresponding to the given bed id
200 * @param bedId the bed identifier
201 * @return the identified {@link Bed} or <code>null</code> if no such bed exists
203 public @Nullable Bed getBed(final String bedId) {
204 logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
205 List<Bed> beds = getBeds();
207 for (Bed bed : beds) {
208 if (bedId.equals(bed.getBedId())) {
217 * Get the sleeper associated with the bedId and side
219 * @param bedId the bed identifier
220 * @param side the side of the bed
221 * @return the sleeper or null if sleeper not found
223 public @Nullable Sleeper getSleeper(String bedId, Side side) {
224 logger.debug("CloudHandler: Get sleeper object for bedId={}, side={}", bedId, side);
225 List<Sleeper> localSleepers = sleepers;
226 if (localSleepers != null) {
227 for (Sleeper sleeper : localSleepers) {
228 if (bedId.equals(sleeper.getBedId()) && side.equals(sleeper.getSide())) {
237 * Set the sleep number of the specified chamber
239 * @param bedId the bed identifier
240 * @param sleepNumber the sleep number multiple of 5 between 5 and 100
241 * @param side the chamber to set
243 public void setSleepNumber(String bedId, Side side, int sleepNumber) {
245 cloud.setSleepNumber(bedId, side, sleepNumber);
246 } catch (SleepIQException e) {
247 logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
252 * Set the pause mode of the specified bed
254 * @param bedId the bed identifier
255 * @param mode turn pause mode on or off
257 public void setPauseMode(String bedId, boolean mode) {
259 cloud.setPauseMode(bedId, mode);
260 } catch (SleepIQException e) {
261 logger.debug("CloudHandler: Exception setting pause mode of bed={}", bedId, e);
266 * Get the foundation features of the specified bed
268 * @param bedId the bed identifier
270 public @Nullable FoundationFeaturesResponse getFoundationFeatures(String bedId) {
272 return cloud.getFoundationFeatures(bedId);
273 } catch (ResponseFormatException e) {
274 logger.debug("CloudHandler: Unable to parse foundation features response for bed={}", bedId);
275 } catch (SleepIQException e) {
276 logger.debug("CloudHandler: Exception getting foundation features, bed={}", bedId, e);
282 * Get the foundation status of the specified bed
284 * @param bedId the bed identifier
286 public @Nullable FoundationStatusResponse getFoundationStatus(String bedId) {
288 return cloud.getFoundationStatus(bedId);
289 } catch (ResponseFormatException e) {
290 logger.debug("CloudHandler: Unable to parse foundation status response for bed={}", bedId);
291 } catch (SleepIQException e) {
292 logger.debug("CloudHandler: Exception getting foundation status, bed={}: {}", bedId, e.getMessage());
298 * Set a foundation adjustment preset.
300 * @param bedId the bed identifier
301 * @param side the side of the bed
302 * @param preset the preset to be applied
303 * @param speed the speed with which to make the adjustment
305 public void setFoundationPreset(String bedId, Side side, FoundationPreset preset, FoundationActuatorSpeed speed) {
307 cloud.setFoundationPreset(bedId, side, preset, speed);
308 } catch (ResponseFormatException e) {
309 logger.debug("CloudHandler: ResponseFormatException setting foundation preset for bed={}: {}", bedId,
311 } catch (SleepIQException e) {
312 logger.debug("CloudHandler: Exception setting the foundation preset for bed={}", bedId, e);
318 * Set a foundation position on head or foot of bed side.
320 * @param bedId the bed identifier
321 * @param side the side of the bed
322 * @param actuator the head or foot of the bed
323 * @param position the new position of the actuator
324 * @param speed the speed with which to make the adjustment
326 public void setFoundationPosition(String bedId, Side side, FoundationActuator actuator, int position,
327 FoundationActuatorSpeed speed) {
329 cloud.setFoundationPosition(bedId, side, actuator, position, speed);
330 } catch (ResponseFormatException e) {
331 logger.debug("CloudHandler: ResponseFormatException setting foundation position for bed={}: {}", bedId,
333 } catch (SleepIQException e) {
334 logger.debug("CloudHandler: Exception setting the foundation position for bed={}", bedId, e);
340 * Operate an outlet on the foundation.
342 * @param bedId the bed identifier
343 * @param outlet the outlet to operate
344 * @param operation the operation (On or Off) performed on the outlet
346 public void setFoundationOutlet(String bedId, FoundationOutlet outlet, FoundationOutletOperation operation) {
348 cloud.setFoundationOutlet(bedId, outlet, operation);
349 } catch (ResponseFormatException e) {
350 logger.debug("CloudHandler: ResponseFormatException setting the foundation outlet for bed={}: {}", bedId,
352 } catch (SleepIQException e) {
353 logger.debug("CloudHandler: Exception setting the foundation outlet for bed={}", bedId, e);
359 * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
362 * @param bed the source of data
363 * @param properties the properties to update (this may be <code>null</code>)
364 * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
366 public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
368 logger.debug("CloudHandler: Updating bed properties for bed={}", bed.getBedId());
369 properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
370 properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
371 if (bed.isKidsBed() != null) {
372 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
374 properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
375 properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
376 if (bed.getPurchaseDate() != null) {
377 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
379 properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
380 properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
386 * Update the given foundation properties with features of the given bed foundation.
388 * @param bed the source of data
389 * @param features the foundation features to update (this may be <code>null</code>)
390 * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
392 public Map<String, String> updateFeatures(final String bedId, final @Nullable FoundationFeaturesResponse features,
393 Map<String, String> properties) {
394 if (features != null) {
395 logger.debug("CloudHandler: Updating foundation properties for bed={}", bedId);
396 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION, "Installed");
397 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HW_REV,
398 String.valueOf(features.getBoardHWRev()));
399 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_IS_BOARD_AS_SINGLE,
400 features.isBoardAsSingle() ? "yes" : "no");
401 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_MASSAGE_AND_LIGHT,
402 features.hasMassageAndLight() ? "yes" : "no");
403 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_FOOT_CONTROL,
404 features.hasFootControl() ? "yes" : "no");
405 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_FOOT_WARMER,
406 features.hasFootWarming() ? "yes" : "no");
407 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_UNDER_BED_LIGHT,
408 features.hasUnderBedLight() ? "yes" : "no");
410 logger.debug("CloudHandler: Foundation not installed on bed={}", bedId);
411 properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION, "Not installed");
417 * Retrieve the latest status on all beds and update all registered listeners
418 * with bed status, foundation status, sleepers and sleep data.
420 private void refreshBedStatus() {
421 logger.debug("CloudHandler: Refreshing BED STATUS, updating chanels with status, sleepers, and sleep data");
423 FamilyStatusResponse familyStatus = cloud.getFamilyStatus();
424 if (familyStatus.getBeds() != null) {
425 updateStatus(ThingStatus.ONLINE);
426 for (BedStatus bedStatus : familyStatus.getBeds()) {
427 String bedId = bedStatus.getBedId();
428 logger.debug("CloudHandler: Informing listeners with bed status for bedId={}", bedId);
429 bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(bedStatus));
431 // Get foundation status only if bed has a foundation
432 bedStatusListeners.stream().filter(l -> l.isFoundationInstalled()).forEach(l -> {
434 l.onFoundationStateChanged(bedId, cloud.getFoundationStatus(bedStatus.getBedId()));
435 } catch (SleepIQException e) {
436 logger.debug("CloudHandler: Exception getting foundation status for bedId={}", bedId);
440 List<Sleeper> localSleepers = sleepers;
441 if (localSleepers != null) {
442 for (Sleeper sleeper : localSleepers) {
443 logger.debug("CloudHandler: Informing listeners with sleepers for sleeperId={}",
444 sleeper.getSleeperId());
445 bedStatusListeners.stream().forEach(l -> l.onSleeperChanged(sleeper));
450 } catch (SleepIQException e) {
451 logger.debug("CloudHandler: Exception refreshing bed status", e);
453 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to SleepIQ cloud");
457 * Refresh the list of sleepers
459 private void refreshSleepers() {
460 logger.debug("CloudHandler: Refreshing SLEEPERS");
462 sleepers = cloud.getSleepers();
463 } catch (SleepIQException e) {
464 logger.debug("CloudHandler: Exception refreshing list of sleepers", e);
468 public @Nullable SleepDataResponse getDailySleepData(String sleeperId) {
469 return getSleepData(sleeperId, SleepDataInterval.DAY);
472 public @Nullable SleepDataResponse getMonthlySleepData(String sleeperId) {
473 return getSleepData(sleeperId, SleepDataInterval.MONTH);
476 private @Nullable SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval) {
478 return cloud.getSleepData(sleeperId, interval);
479 } catch (SleepIQException e) {
480 logger.debug("CloudHandler: Exception getting sleep data for sleeperId={}", sleeperId, e);
486 * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
488 * @throws LoginException if there is an error while authenticating to the service
490 private void createCloudConnection() throws LoginException {
491 SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
492 Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
493 .withPassword(bindingConfig.password);
494 logger.debug("CloudHandler: Authenticating at the SleepIQ cloud service");
495 cloud = SleepIQ.create(cloudConfig, httpClient);
500 * Start or stop the background polling jobs
502 private synchronized void updateListenerManagement() {
503 startSleeperPollingJob();
504 startStatusPollingJob();
508 * Start or stop the bed status polling job
510 private void startStatusPollingJob() {
511 ScheduledFuture<?> localPollingJob = statusPollingJob;
512 if (!bedStatusListeners.isEmpty() && (localPollingJob == null || localPollingJob.isCancelled())) {
513 int pollingInterval = getStatusPollingIntervalSeconds();
514 logger.debug("CloudHandler: Scheduling bed status polling job every {} seconds", pollingInterval);
515 statusPollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval,
516 pollingInterval, TimeUnit.SECONDS);
517 } else if (bedStatusListeners.isEmpty()) {
518 stopStatusPollingJob();
523 * Stop the bed status polling job
525 private void stopStatusPollingJob() {
526 ScheduledFuture<?> localPollingJob = statusPollingJob;
527 if (localPollingJob != null) {
528 logger.debug("CloudHandler: Canceling bed status polling job");
529 localPollingJob.cancel(true);
530 statusPollingJob = null;
534 private int getStatusPollingIntervalSeconds() {
535 return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
539 * Start or stop the sleeper polling job
541 private void startSleeperPollingJob() {
542 ScheduledFuture<?> localJob = sleeperPollingJob;
543 if (!bedStatusListeners.isEmpty() && (localJob == null || localJob.isCancelled())) {
544 logger.debug("CloudHandler: Scheduling sleeper polling job every {} hours", SLEEPER_POLLING_INTERVAL_HOURS);
545 sleeperPollingJob = scheduler.scheduleWithFixedDelay(this::refreshSleepers, SLEEPER_POLLING_INTERVAL_HOURS,
546 SLEEPER_POLLING_INTERVAL_HOURS, TimeUnit.HOURS);
547 } else if (bedStatusListeners.isEmpty()) {
548 stopSleeperPollingJob();
553 * Stop the sleeper polling job
555 private void stopSleeperPollingJob() {
556 ScheduledFuture<?> localJob = sleeperPollingJob;
557 if (localJob != null && !localJob.isCancelled()) {
558 logger.debug("CloudHandler: Canceling sleeper polling job");
559 localJob.cancel(true);
560 sleeperPollingJob = null;