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.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.SleepDataResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
43 import org.openhab.binding.sleepiq.internal.api.enums.Side;
44 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
45 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
46 import org.openhab.core.config.core.status.ConfigStatusMessage;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.ThingTypeUID;
53 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
54 import org.openhab.core.types.Command;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Gregory Moyer - Initial contribution
65 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
66 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
68 private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
70 private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
72 private final HttpClient httpClient;
74 private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
76 private @Nullable ScheduledFuture<?> statusPollingJob;
77 private @Nullable ScheduledFuture<?> sleeperPollingJob;
79 private @Nullable SleepIQ cloud;
81 private @Nullable List<Sleeper> sleepers;
83 public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
85 this.httpClient = httpClient;
89 public void initialize() {
90 scheduler.execute(() -> {
92 createCloudConnection();
93 updateListenerManagement();
94 updateStatus(ThingStatus.ONLINE);
95 } catch (UnauthorizedException e) {
96 logger.debug("CloudHandler: SleepIQ cloud authentication failed", e);
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
98 } catch (LoginException e) {
99 logger.debug("CloudHandler: SleepIQ cloud login failed", e);
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
101 "SleepIQ cloud login failed: " + e.getMessage());
107 public synchronized void dispose() {
108 stopSleeperPollingJob();
109 stopStatusPollingJob();
116 public void handleCommand(final ChannelUID channelUID, final Command command) {
117 // cloud handler has no channels
121 * Validate the config from openHAB
123 * @return validity status of config parameters
126 public Collection<ConfigStatusMessage> getConfigStatus() {
127 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
128 SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
129 String username = config.username;
130 String password = config.password;
131 if (username.isBlank()) {
132 configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
133 .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
135 if (password.isBlank()) {
136 configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
137 .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
139 return configStatusMessages;
143 * Register the given listener to receive bed status updates.
145 * @param listener the listener to register
147 public void registerBedStatusListener(final BedStatusListener listener) {
148 bedStatusListeners.add(listener);
149 scheduler.execute(() -> {
152 updateListenerManagement();
157 * Unregister the given listener from further bed status updates.
159 * @param listener the listener to unregister
160 * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
163 public boolean unregisterBedStatusListener(final BedStatusListener listener) {
164 boolean result = bedStatusListeners.remove(listener);
166 updateListenerManagement();
172 * Get a list of all beds registered to the cloud service account.
174 * @return the list of beds or null if unable to get list
176 public @Nullable List<Bed> getBeds() {
178 return cloud.getBeds();
179 } catch (SleepIQException e) {
180 logger.debug("CloudHandler: Exception getting list of beds", e);
186 * Get the bed corresponding to the given bed id
188 * @param bedId the bed identifier
189 * @return the identified {@link Bed} or <code>null</code> if no such bed exists
191 public @Nullable Bed getBed(final @Nullable String bedId) {
192 logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
196 List<Bed> beds = getBeds();
198 for (Bed bed : beds) {
199 if (bedId.equals(bed.getBedId())) {
208 * Get the sleeper associated with the bedId and side
210 * @param bedId the bed identifier
211 * @param side the side of the bed
212 * @return the sleeper or null if sleeper not found
214 public @Nullable Sleeper getSleeper(@Nullable String bedId, Side side) {
215 logger.debug("CloudHandler: Get sleeper object for bedId={}, side={}", bedId, side);
219 List<Sleeper> localSleepers = sleepers;
220 if (localSleepers != null) {
221 for (Sleeper sleeper : localSleepers) {
222 if (bedId.equals(sleeper.getBedId()) && side.equals(sleeper.getSide())) {
231 * Set the sleep number of the specified chamber
233 * @param bedId the bed identifier
234 * @param sleepNumber the sleep number multiple of 5 between 5 and 100
235 * @param side the chamber to set
237 public void setSleepNumber(@Nullable String bedId, Side side, int sleepNumber) {
242 cloud.setSleepNumber(bedId, side, sleepNumber);
243 } catch (SleepIQException e) {
244 logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
249 * Set the pause mode of the specified bed
251 * @param bedId the bed identifier
252 * @param mode turn pause mode on or off
254 public void setPauseMode(@Nullable 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 * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
269 * @param bed the source of data
270 * @param properties the properties to update (this may be <code>null</code>)
271 * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
273 public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
275 logger.debug("CloudHandler: Updating bed properties for bed={}", bed.getBedId());
276 properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
277 properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
278 if (bed.isKidsBed() != null) {
279 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
281 properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
282 properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
283 if (bed.getPurchaseDate() != null) {
284 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
286 properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
287 properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
293 * Retrieve the latest status on all beds and update all registered listeners
294 * with bed status, sleepers and sleep data.
296 private void refreshBedStatus() {
297 logger.debug("CloudHandler: Refreshing BED STATUS, updating chanels with status, sleepers, and sleep data");
299 FamilyStatusResponse familyStatus = cloud.getFamilyStatus();
300 if (familyStatus != null && familyStatus.getBeds() != null) {
301 updateStatus(ThingStatus.ONLINE);
302 for (BedStatus bedStatus : familyStatus.getBeds()) {
303 logger.debug("CloudHandler: Informing listeners with bed status for bedId={}",
304 bedStatus.getBedId());
305 bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(bedStatus));
308 List<Sleeper> localSleepers = sleepers;
309 if (localSleepers != null) {
310 for (Sleeper sleeper : localSleepers) {
311 logger.debug("CloudHandler: Informing listeners with sleepers for sleeperId={}",
312 sleeper.getSleeperId());
313 bedStatusListeners.stream().forEach(l -> l.onSleeperChanged(sleeper));
318 } catch (SleepIQException e) {
319 logger.debug("CloudHandler: Exception refreshing bed status", e);
321 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to SleepIQ cloud");
325 * Refresh the list of sleepers
327 private void refreshSleepers() {
328 logger.debug("CloudHandler: Refreshing SLEEPERS");
330 sleepers = cloud.getSleepers();
331 } catch (SleepIQException e) {
332 logger.debug("CloudHandler: Exception refreshing list of sleepers", e);
336 public @Nullable SleepDataResponse getDailySleepData(String sleeperId) {
337 return getSleepData(sleeperId, SleepDataInterval.DAY);
340 public @Nullable SleepDataResponse getMonthlySleepData(String sleeperId) {
341 return getSleepData(sleeperId, SleepDataInterval.MONTH);
344 private @Nullable SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval) {
346 return cloud.getSleepData(sleeperId, interval);
347 } catch (SleepIQException e) {
348 logger.debug("CloudHandler: Exception getting sleep data for sleeperId={}", sleeperId, e);
354 * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
356 * @throws LoginException if there is an error while authenticating to the service
358 private void createCloudConnection() throws LoginException {
359 SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
360 Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
361 .withPassword(bindingConfig.password).withLogging(logger.isTraceEnabled());
362 logger.debug("CloudHandler: Authenticating at the SleepIQ cloud service");
363 cloud = SleepIQ.create(cloudConfig, httpClient);
368 * Start or stop the background polling jobs
370 private synchronized void updateListenerManagement() {
371 startSleeperPollingJob();
372 startStatusPollingJob();
376 * Start or stop the bed status polling job
378 private void startStatusPollingJob() {
379 ScheduledFuture<?> localPollingJob = statusPollingJob;
380 if (!bedStatusListeners.isEmpty() && (localPollingJob == null || localPollingJob.isCancelled())) {
381 int pollingInterval = getStatusPollingIntervalSeconds();
382 logger.debug("CloudHandler: Scheduling bed status polling job every {} seconds", pollingInterval);
383 statusPollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval,
384 pollingInterval, TimeUnit.SECONDS);
385 } else if (bedStatusListeners.isEmpty()) {
386 stopStatusPollingJob();
391 * Stop the bed status polling job
393 private void stopStatusPollingJob() {
394 ScheduledFuture<?> localPollingJob = statusPollingJob;
395 if (localPollingJob != null) {
396 logger.debug("CloudHandler: Canceling bed status polling job");
397 localPollingJob.cancel(true);
398 statusPollingJob = null;
402 private int getStatusPollingIntervalSeconds() {
403 return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
407 * Start or stop the sleeper polling job
409 private void startSleeperPollingJob() {
410 ScheduledFuture<?> localJob = sleeperPollingJob;
411 if (!bedStatusListeners.isEmpty() && (localJob == null || localJob.isCancelled())) {
412 logger.debug("CloudHandler: Scheduling sleeper polling job every {} hours", SLEEPER_POLLING_INTERVAL_HOURS);
413 sleeperPollingJob = scheduler.scheduleWithFixedDelay(this::refreshSleepers, SLEEPER_POLLING_INTERVAL_HOURS,
414 SLEEPER_POLLING_INTERVAL_HOURS, TimeUnit.HOURS);
415 } else if (bedStatusListeners.isEmpty()) {
416 stopSleeperPollingJob();
421 * Stop the sleeper polling job
423 private void stopSleeperPollingJob() {
424 ScheduledFuture<?> localJob = sleeperPollingJob;
425 if (localJob != null && !localJob.isCancelled()) {
426 logger.debug("CloudHandler: Canceling sleeper polling job");
427 localJob.cancel(true);
428 sleeperPollingJob = null;