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.*;
18 import java.util.concurrent.TimeUnit;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
23 import org.openhab.binding.sleepiq.internal.api.dto.BedSideStatus;
24 import org.openhab.binding.sleepiq.internal.api.dto.BedStatus;
25 import org.openhab.binding.sleepiq.internal.api.dto.FoundationFeaturesResponse;
26 import org.openhab.binding.sleepiq.internal.api.dto.FoundationStatusResponse;
27 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
28 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
29 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuator;
30 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuatorSpeed;
31 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutlet;
32 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutletOperation;
33 import org.openhab.binding.sleepiq.internal.api.enums.FoundationPreset;
34 import org.openhab.binding.sleepiq.internal.api.enums.Side;
35 import org.openhab.binding.sleepiq.internal.config.SleepIQBedConfiguration;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingStatusInfo;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link SleepIQDualBedHandler} is responsible for handling channel state updates from the cloud service.
59 * @author Gregory Moyer - Initial contribution
62 public class SleepIQDualBedHandler extends BaseThingHandler implements BedStatusListener {
63 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_DUAL_BED);
65 private static final long GET_SLEEP_DATA_DELAY_MINUTES = 5;
67 private final Logger logger = LoggerFactory.getLogger(SleepIQDualBedHandler.class);
69 private volatile String bedId = "";
71 private @Nullable Sleeper sleeperLeft;
72 private @Nullable Sleeper sleeperRight;
74 private @Nullable BedStatus previousStatus;
76 private @Nullable FoundationFeaturesResponse foundationFeatures;
78 public SleepIQDualBedHandler(final Thing thing) {
83 public void initialize() {
84 Bridge bridge = getBridge();
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
87 "No cloud service bridge has been configured");
90 ThingHandler handler = bridge.getHandler();
91 if (!(handler instanceof SleepIQCloudHandler)) {
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect bridge thing found");
95 String localBedId = getConfigAs(SleepIQBedConfiguration.class).bedId;
96 if (localBedId == null) {
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
98 "Bed id not found in configuration");
102 // Assume the bed has a foundation until we determine otherwise
103 setFoundationFeatures(new FoundationFeaturesResponse());
105 logger.debug("BedHandler: Registering SleepIQ bed status listener for bedId={}", bedId);
106 SleepIQCloudHandler cloudHandler = (SleepIQCloudHandler) handler;
107 cloudHandler.registerBedStatusListener(this);
109 if (ThingStatus.ONLINE != bridge.getStatus()) {
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
112 updateStatus(ThingStatus.ONLINE);
113 scheduler.execute(() -> {
120 public void dispose() {
121 SleepIQCloudHandler cloudHandler = getCloudHandler();
122 if (cloudHandler != null) {
123 cloudHandler.unregisterBedStatusListener(this);
129 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
130 super.bridgeStatusChanged(bridgeStatusInfo);
131 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
137 public void handleCommand(final ChannelUID channelUID, final Command command) {
138 if (command == RefreshType.REFRESH) {
139 // Channels will be refreshed automatically by cloud handler
142 String channelId = channelUID.getId();
143 String groupId = channelUID.getGroupId();
146 case CHANNEL_LEFT_SLEEP_NUMBER:
147 case CHANNEL_RIGHT_SLEEP_NUMBER:
148 if (command instanceof DecimalType decimalCommand) {
149 Side side = Side.convertFromGroup(groupId);
150 logger.debug("BedHandler: Set sleepnumber to {} for bedId={}, side={}", command, bedId, side);
151 SleepIQCloudHandler cloudHandler = getCloudHandler();
152 if (cloudHandler != null) {
153 cloudHandler.setSleepNumber(bedId, side, decimalCommand.intValue());
157 case CHANNEL_LEFT_PRIVACY_MODE:
158 case CHANNEL_RIGHT_PRIVACY_MODE:
159 if (command instanceof OnOffType) {
160 Side side = Side.convertFromGroup(groupId);
161 logger.debug("BedHandler: Set sleepnumber to {} for bedId={}, side={}", command, bedId, side);
162 SleepIQCloudHandler cloudHandler = getCloudHandler();
163 if (cloudHandler != null) {
164 cloudHandler.setPauseMode(bedId, command == OnOffType.ON ? true : false);
168 case CHANNEL_LEFT_FOUNDATION_PRESET:
169 case CHANNEL_RIGHT_FOUNDATION_PRESET:
170 logger.debug("Received command {} on channel {} to set preset", command, channelUID);
171 if (isFoundationInstalled() && command instanceof DecimalType decimalCommand) {
173 Side side = Side.convertFromGroup(groupId);
174 FoundationPreset preset = FoundationPreset.forValue(decimalCommand.intValue());
175 logger.debug("BedHandler: Set foundation preset to {} for bedId={}, side={}", command, bedId,
177 SleepIQCloudHandler cloudHandler = getCloudHandler();
178 if (cloudHandler != null) {
179 cloudHandler.setFoundationPreset(bedId, side, preset, FoundationActuatorSpeed.SLOW);
181 } catch (IllegalArgumentException e) {
182 logger.info("BedHandler: Foundation preset invalid: {} must be 1-6", command);
186 case CHANNEL_LEFT_NIGHT_STAND_OUTLET:
187 case CHANNEL_RIGHT_NIGHT_STAND_OUTLET:
188 case CHANNEL_LEFT_UNDER_BED_LIGHT:
189 case CHANNEL_RIGHT_UNDER_BED_LIGHT:
190 logger.debug("Received command {} on channel {} to control outlet", command, channelUID);
191 if (isFoundationInstalled() && command instanceof OnOffType) {
193 logger.debug("BedHandler: Set foundation outlet channel {} to {} for bedId={}", channelId,
195 FoundationOutlet outlet = FoundationOutlet.convertFromChannelId(channelId);
196 SleepIQCloudHandler cloudHandler = getCloudHandler();
197 if (cloudHandler != null) {
198 FoundationOutletOperation operation = command == OnOffType.ON ? FoundationOutletOperation.ON
199 : FoundationOutletOperation.OFF;
200 cloudHandler.setFoundationOutlet(bedId, outlet, operation);
202 } catch (IllegalArgumentException e) {
203 logger.info("BedHandler: Can't convert channel {} to foundation outlet", channelId);
207 case CHANNEL_LEFT_POSITION_HEAD:
208 case CHANNEL_RIGHT_POSITION_HEAD:
209 logger.debug("Received command {} on channel {} to set position", command, channelUID);
210 if (groupId != null && isFoundationInstalled() && command instanceof DecimalType) {
211 setFoundationPosition(groupId, channelId, command);
214 case CHANNEL_LEFT_POSITION_FOOT:
215 case CHANNEL_RIGHT_POSITION_FOOT:
216 logger.debug("Received command {} on channel {} to set position", command, channelUID);
217 if (groupId != null && isFoundationInstalled() && isFoundationFootAdjustable()
218 && command instanceof DecimalType) {
219 setFoundationPosition(groupId, channelId, command);
226 public void onSleeperChanged(final @Nullable Sleeper sleeper) {
227 if (sleeper == null || !sleeper.getBedId().equals(bedId)) {
230 logger.debug("BedHandler: Updating sleeper information channels for bed={}, side={}", bedId, sleeper.getSide());
232 if (sleeper.getSide().equals(Side.LEFT)) {
233 sleeperLeft = sleeper;
234 updateState(CHANNEL_LEFT_FIRST_NAME, new StringType(sleeper.getFirstName()));
235 updateState(CHANNEL_LEFT_SLEEP_GOAL_MINUTES, new QuantityType<>(sleeper.getSleepGoal(), Units.MINUTE));
237 sleeperRight = sleeper;
238 updateState(CHANNEL_RIGHT_FIRST_NAME, new StringType(sleeper.getFirstName()));
239 updateState(CHANNEL_RIGHT_SLEEP_GOAL_MINUTES, new QuantityType<>(sleeper.getSleepGoal(), Units.MINUTE));
244 public void onBedStateChanged(final @Nullable BedStatus status) {
245 if (status == null || !status.getBedId().equals(bedId)) {
248 logger.debug("BedHandler: Updating bed status channels for bed {}", bedId);
249 BedStatus localPreviousStatus = previousStatus;
251 BedSideStatus left = status.getLeftSide();
252 updateState(CHANNEL_LEFT_IN_BED, left.isInBed() ? OnOffType.ON : OnOffType.OFF);
253 updateState(CHANNEL_LEFT_SLEEP_NUMBER, new DecimalType(left.getSleepNumber()));
254 updateState(CHANNEL_LEFT_PRESSURE, new DecimalType(left.getPressure()));
255 updateState(CHANNEL_LEFT_LAST_LINK, new StringType(left.getLastLink().toString()));
256 updateState(CHANNEL_LEFT_ALERT_ID, new DecimalType(left.getAlertId()));
257 updateState(CHANNEL_LEFT_ALERT_DETAILED_MESSAGE, new StringType(left.getAlertDetailedMessage()));
258 if (localPreviousStatus != null) {
259 updateSleepDataChannels(localPreviousStatus.getLeftSide(), left, sleeperLeft);
262 BedSideStatus right = status.getRightSide();
263 updateState(CHANNEL_RIGHT_IN_BED, right.isInBed() ? OnOffType.ON : OnOffType.OFF);
264 updateState(CHANNEL_RIGHT_SLEEP_NUMBER, new DecimalType(right.getSleepNumber()));
265 updateState(CHANNEL_RIGHT_PRESSURE, new DecimalType(right.getPressure()));
266 updateState(CHANNEL_RIGHT_LAST_LINK, new StringType(right.getLastLink().toString()));
267 updateState(CHANNEL_RIGHT_ALERT_ID, new DecimalType(right.getAlertId()));
268 updateState(CHANNEL_RIGHT_ALERT_DETAILED_MESSAGE, new StringType(right.getAlertDetailedMessage()));
269 if (localPreviousStatus != null) {
270 updateSleepDataChannels(localPreviousStatus.getRightSide(), right, sleeperRight);
273 previousStatus = status;
277 public void onFoundationStateChanged(String bedId, final @Nullable FoundationStatusResponse status) {
278 if (status == null || !bedId.equals(this.bedId)) {
281 logger.debug("BedHandler: Updating foundation status channels for bed {}", bedId);
282 updateState(CHANNEL_LEFT_POSITION_HEAD, new DecimalType(status.getLeftHeadPosition()));
283 updateState(CHANNEL_LEFT_POSITION_FOOT, new DecimalType(status.getLeftFootPosition()));
284 updateState(CHANNEL_RIGHT_POSITION_HEAD, new DecimalType(status.getRightHeadPosition()));
285 updateState(CHANNEL_RIGHT_POSITION_FOOT, new DecimalType(status.getRightFootPosition()));
286 updateState(CHANNEL_LEFT_FOUNDATION_PRESET, new DecimalType(status.getCurrentPositionPresetLeft().value()));
287 updateState(CHANNEL_RIGHT_FOUNDATION_PRESET, new DecimalType(status.getCurrentPositionPresetRight().value()));
291 public boolean isFoundationInstalled() {
292 return foundationFeatures != null;
295 private void setFoundationFeatures(@Nullable FoundationFeaturesResponse features) {
296 foundationFeatures = features;
299 private boolean isFoundationFootAdjustable() {
300 FoundationFeaturesResponse localFoundationFeatures = foundationFeatures;
301 return localFoundationFeatures != null ? localFoundationFeatures.hasFootControl() : false;
304 private void setFoundationPosition(String groupId, String channelId, Command command) {
306 logger.debug("BedHandler: Set foundation position channel {} to {} for bedId={}", channelId, command,
308 Side side = Side.convertFromGroup(groupId);
309 FoundationActuator actuator = FoundationActuator.convertFromChannelId(channelId);
310 int position = ((DecimalType) command).intValue();
311 SleepIQCloudHandler cloudHandler = getCloudHandler();
312 if (cloudHandler != null) {
313 cloudHandler.setFoundationPosition(bedId, side, actuator, position, FoundationActuatorSpeed.SLOW);
315 } catch (IllegalArgumentException e) {
316 logger.info("BedHandler: Can't convert channel {} to foundation position", channelId);
320 private void updateSleepDataChannels(BedSideStatus previousSideStatus, BedSideStatus currentSideStatus,
321 @Nullable Sleeper sleeper) {
322 if (sleeper == null) {
323 logger.debug("BedHandler: Can't update sleep data channels because sleeper is null");
326 if (previousSideStatus.isInBed() && !currentSideStatus.isInBed()) {
327 logger.debug("BedHandler: Bed status changed from IN BED to OUT OF BED for {}, side {}", bedId,
329 scheduler.schedule(() -> {
330 updateDailySleepDataChannels(sleeper);
331 updateMonthlySleepDataChannels(sleeper);
332 }, GET_SLEEP_DATA_DELAY_MINUTES, TimeUnit.MINUTES);
336 public void updateDailySleepDataChannels(final @Nullable Sleeper sleeper) {
337 SleepIQCloudHandler cloudHandler = getCloudHandler();
338 if (cloudHandler == null || sleeper == null) {
341 SleepDataResponse sleepData = cloudHandler.getDailySleepData(sleeper.getSleeperId());
342 if (sleepData == null) {
343 logger.debug("BedHandler: Received no daily sleep data for bedId={}, sleeperId={}", sleeper.getBedId(),
344 sleeper.getSleeperId());
348 logger.debug("BedHandler: UPDATING DAILY SLEEP DATA CHANNELS for bedId={}, sleeperId={}", sleeper.getBedId(),
349 sleeper.getSleeperId());
350 if (sleepData.getSleepDataDays() == null || sleepData.getSleepDataDays().size() != 1) {
351 if (sleeper.getSide().equals(Side.LEFT)) {
352 updateState(CHANNEL_LEFT_TODAY_SLEEP_IQ, UnDefType.UNDEF);
353 updateState(CHANNEL_LEFT_TODAY_AVG_HEART_RATE, UnDefType.UNDEF);
354 updateState(CHANNEL_LEFT_TODAY_AVG_RESPIRATION_RATE, UnDefType.UNDEF);
355 updateState(CHANNEL_LEFT_TODAY_MESSAGE, UnDefType.UNDEF);
356 updateState(CHANNEL_LEFT_TODAY_SLEEP_DURATION_SECONDS, UnDefType.UNDEF);
357 updateState(CHANNEL_LEFT_TODAY_SLEEP_IN_BED_SECONDS, UnDefType.UNDEF);
358 updateState(CHANNEL_LEFT_TODAY_SLEEP_OUT_OF_BED_SECONDS, UnDefType.UNDEF);
359 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTFUL_SECONDS, UnDefType.UNDEF);
360 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTLESS_SECONDS, UnDefType.UNDEF);
362 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IQ, UnDefType.UNDEF);
363 updateState(CHANNEL_RIGHT_TODAY_AVG_HEART_RATE, UnDefType.UNDEF);
364 updateState(CHANNEL_RIGHT_TODAY_AVG_RESPIRATION_RATE, UnDefType.UNDEF);
365 updateState(CHANNEL_RIGHT_TODAY_MESSAGE, UnDefType.UNDEF);
366 updateState(CHANNEL_RIGHT_TODAY_SLEEP_DURATION_SECONDS, UnDefType.UNDEF);
367 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IN_BED_SECONDS, UnDefType.UNDEF);
368 updateState(CHANNEL_RIGHT_TODAY_SLEEP_OUT_OF_BED_SECONDS, UnDefType.UNDEF);
369 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTFUL_SECONDS, UnDefType.UNDEF);
370 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTLESS_SECONDS, UnDefType.UNDEF);
373 } else if (sleeper.getSide().equals(Side.LEFT)) {
374 updateState(CHANNEL_LEFT_TODAY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
375 updateState(CHANNEL_LEFT_TODAY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
376 updateState(CHANNEL_LEFT_TODAY_AVG_RESPIRATION_RATE,
377 new DecimalType(sleepData.getAverageRespirationRate()));
378 updateState(CHANNEL_LEFT_TODAY_MESSAGE, new StringType(sleepData.getSleepDataDays().get(0).getMessage()));
379 updateState(CHANNEL_LEFT_TODAY_SLEEP_DURATION_SECONDS,
380 new QuantityType<>(sleepData.getTotalSleepSessionTime(), Units.SECOND));
381 updateState(CHANNEL_LEFT_TODAY_SLEEP_IN_BED_SECONDS,
382 new QuantityType<>(sleepData.getTotalInBedSeconds(), Units.SECOND));
383 updateState(CHANNEL_LEFT_TODAY_SLEEP_OUT_OF_BED_SECONDS,
384 new QuantityType<>(sleepData.getTotalOutOfBedSeconds(), Units.SECOND));
385 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTFUL_SECONDS,
386 new QuantityType<>(sleepData.getTotalRestfulSeconds(), Units.SECOND));
387 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTLESS_SECONDS,
388 new QuantityType<>(sleepData.getTotalRestlessSeconds(), Units.SECOND));
389 } else if (sleeper.getSide().equals(Side.RIGHT)) {
390 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
391 updateState(CHANNEL_RIGHT_TODAY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
392 updateState(CHANNEL_RIGHT_TODAY_AVG_RESPIRATION_RATE,
393 new DecimalType(sleepData.getAverageRespirationRate()));
394 updateState(CHANNEL_RIGHT_TODAY_MESSAGE, new StringType(sleepData.getSleepDataDays().get(0).getMessage()));
395 updateState(CHANNEL_RIGHT_TODAY_SLEEP_DURATION_SECONDS,
396 new QuantityType<>(sleepData.getTotalSleepSessionTime(), Units.SECOND));
397 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IN_BED_SECONDS,
398 new QuantityType<>(sleepData.getTotalInBedSeconds(), Units.SECOND));
399 updateState(CHANNEL_RIGHT_TODAY_SLEEP_OUT_OF_BED_SECONDS,
400 new QuantityType<>(sleepData.getTotalOutOfBedSeconds(), Units.SECOND));
401 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTFUL_SECONDS,
402 new QuantityType<>(sleepData.getTotalRestfulSeconds(), Units.SECOND));
403 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTLESS_SECONDS,
404 new QuantityType<>(sleepData.getTotalRestlessSeconds(), Units.SECOND));
408 public void updateMonthlySleepDataChannels(final @Nullable Sleeper sleeper) {
409 SleepIQCloudHandler cloudHandler = getCloudHandler();
410 if (cloudHandler == null || sleeper == null) {
413 SleepDataResponse sleepData = cloudHandler.getMonthlySleepData(sleeper.getSleeperId());
414 if (sleepData == null) {
415 logger.debug("BedHandler: Received no monthly sleep data for bedId={}, sleeperId={}", sleeper.getBedId(),
416 sleeper.getSleeperId());
420 logger.debug("BedHandler: UPDATING MONTHLY SLEEP DATA CHANNELS for bedId={}, sleeperId={}", sleeper.getBedId(),
421 sleeper.getSleeperId());
422 if (sleeper.getSide().equals(Side.LEFT)) {
423 updateState(CHANNEL_LEFT_MONTHLY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
424 updateState(CHANNEL_LEFT_MONTHLY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
425 updateState(CHANNEL_LEFT_MONTHLY_AVG_RESPIRATION_RATE,
426 new DecimalType(sleepData.getAverageRespirationRate()));
428 updateState(CHANNEL_RIGHT_MONTHLY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
429 updateState(CHANNEL_RIGHT_MONTHLY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
430 updateState(CHANNEL_RIGHT_MONTHLY_AVG_RESPIRATION_RATE,
431 new DecimalType(sleepData.getAverageRespirationRate()));
435 private void updateProperties() {
436 logger.debug("BedHandler: Updating bed properties for bedId={}", bedId);
437 SleepIQCloudHandler cloudHandler = getCloudHandler();
438 if (cloudHandler != null) {
439 Bed bed = cloudHandler.getBed(bedId);
441 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
442 "No bed found with ID " + bedId);
445 updateProperties(cloudHandler.updateProperties(bed, editProperties()));
447 logger.debug("BedHandler: Checking if foundation is installed for bedId={}", bedId);
448 if (isFoundationInstalled()) {
449 FoundationFeaturesResponse foundationFeaturesResponse = cloudHandler.getFoundationFeatures(bedId);
450 updateProperties(cloudHandler.updateFeatures(bedId, foundationFeaturesResponse, editProperties()));
451 setFoundationFeatures(foundationFeaturesResponse);
456 private @Nullable SleepIQCloudHandler getCloudHandler() {
457 Bridge bridge = getBridge();
458 if (bridge != null) {
459 return (SleepIQCloudHandler) bridge.getHandler();