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.List;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.openhab.binding.sleepiq.internal.SleepIQBindingConstants;
31 import org.openhab.binding.sleepiq.internal.SleepIQConfigStatusMessage;
32 import org.openhab.binding.sleepiq.internal.api.Configuration;
33 import org.openhab.binding.sleepiq.internal.api.LoginException;
34 import org.openhab.binding.sleepiq.internal.api.ResponseFormatException;
35 import org.openhab.binding.sleepiq.internal.api.SleepIQ;
36 import org.openhab.binding.sleepiq.internal.api.SleepIQException;
37 import org.openhab.binding.sleepiq.internal.api.UnauthorizedException;
38 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
39 import org.openhab.binding.sleepiq.internal.api.dto.BedStatus;
40 import org.openhab.binding.sleepiq.internal.api.dto.FamilyStatusResponse;
41 import org.openhab.binding.sleepiq.internal.api.dto.FoundationFeaturesResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.FoundationStatusResponse;
43 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
44 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
45 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuator;
46 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuatorSpeed;
47 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutlet;
48 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutletOperation;
49 import org.openhab.binding.sleepiq.internal.api.enums.FoundationPreset;
50 import org.openhab.binding.sleepiq.internal.api.enums.Side;
51 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
52 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
53 import org.openhab.core.config.core.status.ConfigStatusMessage;
54 import org.openhab.core.thing.Bridge;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.ThingTypeUID;
60 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
61 import org.openhab.core.types.Command;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
67 * sent to one of the channels.
69 * @author Gregory Moyer - Initial contribution
72 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
73 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_CLOUD);
75 private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
77 private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
79 private final HttpClient httpClient;
81 private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
83 private @Nullable ScheduledFuture<?> statusPollingJob;
84 private @Nullable ScheduledFuture<?> sleeperPollingJob;
86 private @Nullable SleepIQ cloud;
88 private @Nullable List<Sleeper> sleepers;
90 public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
92 this.httpClient = httpClient;
96 public void initialize() {
97 scheduler.execute(() -> {
99 createCloudConnection();
100 updateListenerManagement();
101 updateStatus(ThingStatus.ONLINE);
102 } catch (UnauthorizedException e) {
103 logger.debug("CloudHandler: SleepIQ cloud authentication failed", e);
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
105 } catch (LoginException e) {
106 logger.debug("CloudHandler: SleepIQ cloud login failed", e);
107 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
108 "SleepIQ cloud login failed: " + e.getMessage());
114 public synchronized void dispose() {
115 stopSleeperPollingJob();
116 stopStatusPollingJob();
123 public void handleCommand(final ChannelUID channelUID, final Command command) {
124 // cloud handler has no channels
128 * Validate the config from openHAB
130 * @return validity status of config parameters
133 public Collection<ConfigStatusMessage> getConfigStatus() {
134 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
135 SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
136 String username = config.username;
137 String password = config.password;
138 if (username.isBlank()) {
139 configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
140 .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
142 if (password.isBlank()) {
143 configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
144 .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
146 return configStatusMessages;
150 * Register the given listener to receive bed status updates.
152 * @param listener the listener to register
154 public void registerBedStatusListener(final BedStatusListener listener) {
155 bedStatusListeners.add(listener);
157 * Delay the initial sleeper and status update to give some time for the property update
158 * to determine if a foundation is installed.
160 scheduler.schedule(() -> {
163 updateListenerManagement();
164 }, 10L, TimeUnit.SECONDS);
168 * Unregister the given listener from further bed status updates.
170 * @param listener the listener to unregister
171 * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
174 public boolean unregisterBedStatusListener(final BedStatusListener listener) {
175 boolean result = bedStatusListeners.remove(listener);
177 updateListenerManagement();
183 * Get a list of all beds registered to the cloud service account.
185 * @return the list of beds or null if unable to get list
187 public @Nullable List<Bed> getBeds() {
189 return cloud.getBeds();
190 } catch (SleepIQException e) {
191 logger.debug("CloudHandler: Exception getting list of beds", e);
197 * Get the bed corresponding to the given bed id
199 * @param bedId the bed identifier
200 * @return the identified {@link Bed} or <code>null</code> if no such bed exists
202 public @Nullable Bed getBed(final String bedId) {
203 logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
204 List<Bed> beds = getBeds();
206 for (Bed bed : beds) {
207 if (bedId.equals(bed.getBedId())) {
216 * Get the sleeper associated with the bedId and side
218 * @param bedId the bed identifier
219 * @param side the side of the bed
220 * @return the sleeper or null if sleeper not found
222 public @Nullable Sleeper getSleeper(String bedId, Side side) {
223 logger.debug("CloudHandler: Get sleeper object for bedId={}, side={}", bedId, side);
224 List<Sleeper> localSleepers = sleepers;
225 if (localSleepers != null) {
226 for (Sleeper sleeper : localSleepers) {
227 if (bedId.equals(sleeper.getBedId()) && side.equals(sleeper.getSide())) {
236 * Set the sleep number of the specified chamber
238 * @param bedId the bed identifier
239 * @param sleepNumber the sleep number multiple of 5 between 5 and 100
240 * @param side the chamber to set
242 public void setSleepNumber(String bedId, Side side, int sleepNumber) {
244 cloud.setSleepNumber(bedId, side, sleepNumber);
245 } catch (SleepIQException e) {
246 logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
251 * Set the pause mode of the specified bed
253 * @param bedId the bed identifier
254 * @param mode turn pause mode on or off
256 public void setPauseMode(String bedId, boolean mode) {
258 cloud.setPauseMode(bedId, mode);
259 } catch (SleepIQException e) {
260 logger.debug("CloudHandler: Exception setting pause mode of bed={}", bedId, e);
265 * Get the foundation features of the specified bed
267 * @param bedId the bed identifier
269 public @Nullable FoundationFeaturesResponse getFoundationFeatures(String bedId) {
271 return cloud.getFoundationFeatures(bedId);
272 } catch (ResponseFormatException e) {
273 logger.debug("CloudHandler: Unable to parse foundation features response for bed={}", bedId);
274 } catch (SleepIQException e) {
275 logger.debug("CloudHandler: Exception getting foundation features, bed={}", bedId, e);
281 * Get the foundation status of the specified bed
283 * @param bedId the bed identifier
285 public @Nullable FoundationStatusResponse getFoundationStatus(String bedId) {
287 return cloud.getFoundationStatus(bedId);
288 } catch (ResponseFormatException e) {
289 logger.debug("CloudHandler: Unable to parse foundation status response for bed={}", bedId);
290 } catch (SleepIQException e) {
291 logger.debug("CloudHandler: Exception getting foundation status, bed={}: {}", bedId, e.getMessage());
297 * Set a foundation adjustment preset.
299 * @param bedId the bed identifier
300 * @param side the side of the bed
301 * @param preset the preset to be applied
302 * @param speed the speed with which to make the adjustment
304 public void setFoundationPreset(String bedId, Side side, FoundationPreset preset, FoundationActuatorSpeed speed) {
306 cloud.setFoundationPreset(bedId, side, preset, speed);
307 } catch (ResponseFormatException e) {
308 logger.debug("CloudHandler: ResponseFormatException setting foundation preset for bed={}: {}", bedId,
310 } catch (SleepIQException e) {
311 logger.debug("CloudHandler: Exception setting the foundation preset for bed={}", bedId, e);
317 * Set a foundation position on head or foot of bed side.
319 * @param bedId the bed identifier
320 * @param side the side of the bed
321 * @param actuator the head or foot of the bed
322 * @param position the new position of the actuator
323 * @param speed the speed with which to make the adjustment
325 public void setFoundationPosition(String bedId, Side side, FoundationActuator actuator, int position,
326 FoundationActuatorSpeed speed) {
328 cloud.setFoundationPosition(bedId, side, actuator, position, speed);
329 } catch (ResponseFormatException e) {
330 logger.debug("CloudHandler: ResponseFormatException setting foundation position for bed={}: {}", bedId,
332 } catch (SleepIQException e) {
333 logger.debug("CloudHandler: Exception setting the foundation position for bed={}", bedId, e);
339 * Operate an outlet on the foundation.
341 * @param bedId the bed identifier
342 * @param outlet the outlet to operate
343 * @param operation the operation (On or Off) performed on the outlet
345 public void setFoundationOutlet(String bedId, FoundationOutlet outlet, FoundationOutletOperation operation) {
347 cloud.setFoundationOutlet(bedId, outlet, operation);
348 } catch (ResponseFormatException e) {
349 logger.debug("CloudHandler: ResponseFormatException setting the foundation outlet for bed={}: {}", bedId,
351 } catch (SleepIQException e) {
352 logger.debug("CloudHandler: Exception setting the foundation outlet for bed={}", bedId, e);
358 * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
361 * @param bed the source of data
362 * @param properties the properties to update (this may be <code>null</code>)
363 * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
365 public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
367 logger.debug("CloudHandler: Updating bed properties for bed={}", bed.getBedId());
368 properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
369 properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
370 if (bed.isKidsBed() != null) {
371 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
373 properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
374 properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
375 if (bed.getPurchaseDate() != null) {
376 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
378 properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
379 properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
385 * Update the given foundation properties with features of the given bed foundation.
387 * @param bedId the source of data
388 * @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;