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.*;
17 import java.util.Collections;
19 import java.util.concurrent.TimeUnit;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
24 import org.openhab.binding.sleepiq.internal.api.dto.BedSideStatus;
25 import org.openhab.binding.sleepiq.internal.api.dto.BedStatus;
26 import org.openhab.binding.sleepiq.internal.api.dto.FoundationFeaturesResponse;
27 import org.openhab.binding.sleepiq.internal.api.dto.FoundationStatusResponse;
28 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
29 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
30 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuator;
31 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuatorSpeed;
32 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutlet;
33 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutletOperation;
34 import org.openhab.binding.sleepiq.internal.api.enums.FoundationPreset;
35 import org.openhab.binding.sleepiq.internal.api.enums.Side;
36 import org.openhab.binding.sleepiq.internal.config.SleepIQBedConfiguration;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.library.unit.Units;
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.ThingStatusInfo;
48 import org.openhab.core.thing.ThingTypeUID;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.ThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link SleepIQDualBedHandler} is responsible for handling channel state updates from the cloud service.
60 * @author Gregory Moyer - Initial contribution
63 public class SleepIQDualBedHandler extends BaseThingHandler implements BedStatusListener {
64 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_DUAL_BED);
66 private static final long GET_SLEEP_DATA_DELAY_MINUTES = 5;
68 private final Logger logger = LoggerFactory.getLogger(SleepIQDualBedHandler.class);
70 private volatile String bedId = "";
72 private @Nullable Sleeper sleeperLeft;
73 private @Nullable Sleeper sleeperRight;
75 private @Nullable BedStatus previousStatus;
77 private @Nullable FoundationFeaturesResponse foundationFeatures;
79 public SleepIQDualBedHandler(final Thing thing) {
84 public void initialize() {
85 Bridge bridge = getBridge();
87 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
88 "No cloud service bridge has been configured");
91 ThingHandler handler = bridge.getHandler();
92 if (!(handler instanceof SleepIQCloudHandler)) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect bridge thing found");
96 String localBedId = getConfigAs(SleepIQBedConfiguration.class).bedId;
97 if (localBedId == null) {
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
99 "Bed id not found in configuration");
103 // Assume the bed has a foundation until we determine otherwise
104 setFoundationFeatures(new FoundationFeaturesResponse());
106 logger.debug("BedHandler: Registering SleepIQ bed status listener for bedId={}", bedId);
107 SleepIQCloudHandler cloudHandler = (SleepIQCloudHandler) handler;
108 cloudHandler.registerBedStatusListener(this);
110 if (ThingStatus.ONLINE != bridge.getStatus()) {
111 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
113 updateStatus(ThingStatus.ONLINE);
114 scheduler.execute(() -> {
121 public void dispose() {
122 SleepIQCloudHandler cloudHandler = getCloudHandler();
123 if (cloudHandler != null) {
124 cloudHandler.unregisterBedStatusListener(this);
130 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
131 super.bridgeStatusChanged(bridgeStatusInfo);
132 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
138 public void handleCommand(final ChannelUID channelUID, final Command command) {
139 if (command == RefreshType.REFRESH) {
140 // Channels will be refreshed automatically by cloud handler
143 String channelId = channelUID.getId();
144 String groupId = channelUID.getGroupId();
147 case CHANNEL_LEFT_SLEEP_NUMBER:
148 case CHANNEL_RIGHT_SLEEP_NUMBER:
149 if (command instanceof DecimalType) {
150 Side side = Side.convertFromGroup(groupId);
151 logger.debug("BedHandler: Set sleepnumber to {} for bedId={}, side={}", command, bedId, side);
152 SleepIQCloudHandler cloudHandler = getCloudHandler();
153 if (cloudHandler != null) {
154 cloudHandler.setSleepNumber(bedId, side, ((DecimalType) command).intValue());
158 case CHANNEL_LEFT_PRIVACY_MODE:
159 case CHANNEL_RIGHT_PRIVACY_MODE:
160 if (command instanceof OnOffType) {
161 Side side = Side.convertFromGroup(groupId);
162 logger.debug("BedHandler: Set sleepnumber to {} for bedId={}, side={}", command, bedId, side);
163 SleepIQCloudHandler cloudHandler = getCloudHandler();
164 if (cloudHandler != null) {
165 cloudHandler.setPauseMode(bedId, command == OnOffType.ON ? true : false);
169 case CHANNEL_LEFT_FOUNDATION_PRESET:
170 case CHANNEL_RIGHT_FOUNDATION_PRESET:
171 logger.debug("Received command {} on channel {} to set preset", command, channelUID);
172 if (isFoundationInstalled() && command instanceof DecimalType) {
174 Side side = Side.convertFromGroup(groupId);
175 FoundationPreset preset = FoundationPreset.forValue(((DecimalType) command).intValue());
176 logger.debug("BedHandler: Set foundation preset to {} for bedId={}, side={}", command, bedId,
178 SleepIQCloudHandler cloudHandler = getCloudHandler();
179 if (cloudHandler != null) {
180 cloudHandler.setFoundationPreset(bedId, side, preset, FoundationActuatorSpeed.SLOW);
182 } catch (IllegalArgumentException e) {
183 logger.info("BedHandler: Foundation preset invalid: {} must be 1-6", command);
187 case CHANNEL_LEFT_NIGHT_STAND_OUTLET:
188 case CHANNEL_RIGHT_NIGHT_STAND_OUTLET:
189 case CHANNEL_LEFT_UNDER_BED_LIGHT:
190 case CHANNEL_RIGHT_UNDER_BED_LIGHT:
191 logger.debug("Received command {} on channel {} to control outlet", command, channelUID);
192 if (isFoundationInstalled() && command instanceof OnOffType) {
194 logger.debug("BedHandler: Set foundation outlet channel {} to {} for bedId={}", channelId,
196 FoundationOutlet outlet = FoundationOutlet.convertFromChannelId(channelId);
197 SleepIQCloudHandler cloudHandler = getCloudHandler();
198 if (cloudHandler != null) {
199 FoundationOutletOperation operation = command == OnOffType.ON ? FoundationOutletOperation.ON
200 : FoundationOutletOperation.OFF;
201 cloudHandler.setFoundationOutlet(bedId, outlet, operation);
203 } catch (IllegalArgumentException e) {
204 logger.info("BedHandler: Can't convert channel {} to foundation outlet", channelId);
208 case CHANNEL_LEFT_POSITION_HEAD:
209 case CHANNEL_RIGHT_POSITION_HEAD:
210 logger.debug("Received command {} on channel {} to set position", command, channelUID);
211 if (groupId != null && isFoundationInstalled() && command instanceof DecimalType) {
212 setFoundationPosition(groupId, channelId, command);
215 case CHANNEL_LEFT_POSITION_FOOT:
216 case CHANNEL_RIGHT_POSITION_FOOT:
217 logger.debug("Received command {} on channel {} to set position", command, channelUID);
218 if (groupId != null && isFoundationInstalled() && isFoundationFootAdjustable()
219 && command instanceof DecimalType) {
220 setFoundationPosition(groupId, channelId, command);
227 public void onSleeperChanged(final @Nullable Sleeper sleeper) {
228 if (sleeper == null || !sleeper.getBedId().equals(bedId)) {
231 logger.debug("BedHandler: Updating sleeper information channels for bed={}, side={}", bedId, sleeper.getSide());
233 if (sleeper.getSide().equals(Side.LEFT)) {
234 sleeperLeft = sleeper;
235 updateState(CHANNEL_LEFT_FIRST_NAME, new StringType(sleeper.getFirstName()));
236 updateState(CHANNEL_LEFT_SLEEP_GOAL_MINUTES, new QuantityType<>(sleeper.getSleepGoal(), Units.MINUTE));
238 sleeperRight = sleeper;
239 updateState(CHANNEL_RIGHT_FIRST_NAME, new StringType(sleeper.getFirstName()));
240 updateState(CHANNEL_RIGHT_SLEEP_GOAL_MINUTES, new QuantityType<>(sleeper.getSleepGoal(), Units.MINUTE));
245 public void onBedStateChanged(final @Nullable BedStatus status) {
246 if (status == null || !status.getBedId().equals(bedId)) {
249 logger.debug("BedHandler: Updating bed status channels for bed {}", bedId);
250 BedStatus localPreviousStatus = previousStatus;
252 BedSideStatus left = status.getLeftSide();
253 updateState(CHANNEL_LEFT_IN_BED, left.isInBed() ? OnOffType.ON : OnOffType.OFF);
254 updateState(CHANNEL_LEFT_SLEEP_NUMBER, new DecimalType(left.getSleepNumber()));
255 updateState(CHANNEL_LEFT_PRESSURE, new DecimalType(left.getPressure()));
256 updateState(CHANNEL_LEFT_LAST_LINK, new StringType(left.getLastLink().toString()));
257 updateState(CHANNEL_LEFT_ALERT_ID, new DecimalType(left.getAlertId()));
258 updateState(CHANNEL_LEFT_ALERT_DETAILED_MESSAGE, new StringType(left.getAlertDetailedMessage()));
259 if (localPreviousStatus != null) {
260 updateSleepDataChannels(localPreviousStatus.getLeftSide(), left, sleeperLeft);
263 BedSideStatus right = status.getRightSide();
264 updateState(CHANNEL_RIGHT_IN_BED, right.isInBed() ? OnOffType.ON : OnOffType.OFF);
265 updateState(CHANNEL_RIGHT_SLEEP_NUMBER, new DecimalType(right.getSleepNumber()));
266 updateState(CHANNEL_RIGHT_PRESSURE, new DecimalType(right.getPressure()));
267 updateState(CHANNEL_RIGHT_LAST_LINK, new StringType(right.getLastLink().toString()));
268 updateState(CHANNEL_RIGHT_ALERT_ID, new DecimalType(right.getAlertId()));
269 updateState(CHANNEL_RIGHT_ALERT_DETAILED_MESSAGE, new StringType(right.getAlertDetailedMessage()));
270 if (localPreviousStatus != null) {
271 updateSleepDataChannels(localPreviousStatus.getRightSide(), right, sleeperRight);
274 previousStatus = status;
278 public void onFoundationStateChanged(String bedId, final @Nullable FoundationStatusResponse status) {
279 if (status == null || !bedId.equals(this.bedId)) {
282 logger.debug("BedHandler: Updating foundation status channels for bed {}", bedId);
283 updateState(CHANNEL_LEFT_POSITION_HEAD, new DecimalType(status.getLeftHeadPosition()));
284 updateState(CHANNEL_LEFT_POSITION_FOOT, new DecimalType(status.getLeftFootPosition()));
285 updateState(CHANNEL_RIGHT_POSITION_HEAD, new DecimalType(status.getRightHeadPosition()));
286 updateState(CHANNEL_RIGHT_POSITION_FOOT, new DecimalType(status.getRightFootPosition()));
287 updateState(CHANNEL_LEFT_FOUNDATION_PRESET, new DecimalType(status.getCurrentPositionPresetLeft().value()));
288 updateState(CHANNEL_RIGHT_FOUNDATION_PRESET, new DecimalType(status.getCurrentPositionPresetRight().value()));
292 public boolean isFoundationInstalled() {
293 return foundationFeatures != null;
296 private void setFoundationFeatures(@Nullable FoundationFeaturesResponse features) {
297 foundationFeatures = features;
300 private boolean isFoundationFootAdjustable() {
301 FoundationFeaturesResponse localFoundationFeatures = foundationFeatures;
302 return localFoundationFeatures != null ? localFoundationFeatures.hasFootControl() : false;
305 private void setFoundationPosition(String groupId, String channelId, Command command) {
307 logger.debug("BedHandler: Set foundation position channel {} to {} for bedId={}", channelId, command,
309 Side side = Side.convertFromGroup(groupId);
310 FoundationActuator actuator = FoundationActuator.convertFromChannelId(channelId);
311 int position = ((DecimalType) command).intValue();
312 SleepIQCloudHandler cloudHandler = getCloudHandler();
313 if (cloudHandler != null) {
314 cloudHandler.setFoundationPosition(bedId, side, actuator, position, FoundationActuatorSpeed.SLOW);
316 } catch (IllegalArgumentException e) {
317 logger.info("BedHandler: Can't convert channel {} to foundation position", channelId);
321 private void updateSleepDataChannels(BedSideStatus previousSideStatus, BedSideStatus currentSideStatus,
322 @Nullable Sleeper sleeper) {
323 if (sleeper == null) {
324 logger.debug("BedHandler: Can't update sleep data channels because sleeper is null");
327 if (previousSideStatus.isInBed() && !currentSideStatus.isInBed()) {
328 logger.debug("BedHandler: Bed status changed from IN BED to OUT OF BED for {}, side {}", bedId,
330 scheduler.schedule(() -> {
331 updateDailySleepDataChannels(sleeper);
332 updateMonthlySleepDataChannels(sleeper);
333 }, GET_SLEEP_DATA_DELAY_MINUTES, TimeUnit.MINUTES);
337 public void updateDailySleepDataChannels(final @Nullable Sleeper sleeper) {
338 SleepIQCloudHandler cloudHandler = getCloudHandler();
339 if (cloudHandler == null || sleeper == null) {
342 SleepDataResponse sleepData = cloudHandler.getDailySleepData(sleeper.getSleeperId());
343 if (sleepData == null) {
344 logger.debug("BedHandler: Received no daily sleep data for bedId={}, sleeperId={}", sleeper.getBedId(),
345 sleeper.getSleeperId());
349 logger.debug("BedHandler: UPDATING DAILY SLEEP DATA CHANNELS for bedId={}, sleeperId={}", sleeper.getBedId(),
350 sleeper.getSleeperId());
351 if (sleepData.getSleepDataDays() == null || sleepData.getSleepDataDays().size() != 1) {
352 if (sleeper.getSide().equals(Side.LEFT)) {
353 updateState(CHANNEL_LEFT_TODAY_SLEEP_IQ, UnDefType.UNDEF);
354 updateState(CHANNEL_LEFT_TODAY_AVG_HEART_RATE, UnDefType.UNDEF);
355 updateState(CHANNEL_LEFT_TODAY_AVG_RESPIRATION_RATE, UnDefType.UNDEF);
356 updateState(CHANNEL_LEFT_TODAY_MESSAGE, UnDefType.UNDEF);
357 updateState(CHANNEL_LEFT_TODAY_SLEEP_DURATION_SECONDS, UnDefType.UNDEF);
358 updateState(CHANNEL_LEFT_TODAY_SLEEP_IN_BED_SECONDS, UnDefType.UNDEF);
359 updateState(CHANNEL_LEFT_TODAY_SLEEP_OUT_OF_BED_SECONDS, UnDefType.UNDEF);
360 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTFUL_SECONDS, UnDefType.UNDEF);
361 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTLESS_SECONDS, UnDefType.UNDEF);
363 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IQ, UnDefType.UNDEF);
364 updateState(CHANNEL_RIGHT_TODAY_AVG_HEART_RATE, UnDefType.UNDEF);
365 updateState(CHANNEL_RIGHT_TODAY_AVG_RESPIRATION_RATE, UnDefType.UNDEF);
366 updateState(CHANNEL_RIGHT_TODAY_MESSAGE, UnDefType.UNDEF);
367 updateState(CHANNEL_RIGHT_TODAY_SLEEP_DURATION_SECONDS, UnDefType.UNDEF);
368 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IN_BED_SECONDS, UnDefType.UNDEF);
369 updateState(CHANNEL_RIGHT_TODAY_SLEEP_OUT_OF_BED_SECONDS, UnDefType.UNDEF);
370 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTFUL_SECONDS, UnDefType.UNDEF);
371 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTLESS_SECONDS, UnDefType.UNDEF);
374 } else if (sleeper.getSide().equals(Side.LEFT)) {
375 updateState(CHANNEL_LEFT_TODAY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
376 updateState(CHANNEL_LEFT_TODAY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
377 updateState(CHANNEL_LEFT_TODAY_AVG_RESPIRATION_RATE,
378 new DecimalType(sleepData.getAverageRespirationRate()));
379 updateState(CHANNEL_LEFT_TODAY_MESSAGE, new StringType(sleepData.getSleepDataDays().get(0).getMessage()));
380 updateState(CHANNEL_LEFT_TODAY_SLEEP_DURATION_SECONDS,
381 new QuantityType<>(sleepData.getTotalSleepSessionTime(), Units.SECOND));
382 updateState(CHANNEL_LEFT_TODAY_SLEEP_IN_BED_SECONDS,
383 new QuantityType<>(sleepData.getTotalInBedSeconds(), Units.SECOND));
384 updateState(CHANNEL_LEFT_TODAY_SLEEP_OUT_OF_BED_SECONDS,
385 new QuantityType<>(sleepData.getTotalOutOfBedSeconds(), Units.SECOND));
386 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTFUL_SECONDS,
387 new QuantityType<>(sleepData.getTotalRestfulSeconds(), Units.SECOND));
388 updateState(CHANNEL_LEFT_TODAY_SLEEP_RESTLESS_SECONDS,
389 new QuantityType<>(sleepData.getTotalRestlessSeconds(), Units.SECOND));
390 } else if (sleeper.getSide().equals(Side.RIGHT)) {
391 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
392 updateState(CHANNEL_RIGHT_TODAY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
393 updateState(CHANNEL_RIGHT_TODAY_AVG_RESPIRATION_RATE,
394 new DecimalType(sleepData.getAverageRespirationRate()));
395 updateState(CHANNEL_RIGHT_TODAY_MESSAGE, new StringType(sleepData.getSleepDataDays().get(0).getMessage()));
396 updateState(CHANNEL_RIGHT_TODAY_SLEEP_DURATION_SECONDS,
397 new QuantityType<>(sleepData.getTotalSleepSessionTime(), Units.SECOND));
398 updateState(CHANNEL_RIGHT_TODAY_SLEEP_IN_BED_SECONDS,
399 new QuantityType<>(sleepData.getTotalInBedSeconds(), Units.SECOND));
400 updateState(CHANNEL_RIGHT_TODAY_SLEEP_OUT_OF_BED_SECONDS,
401 new QuantityType<>(sleepData.getTotalOutOfBedSeconds(), Units.SECOND));
402 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTFUL_SECONDS,
403 new QuantityType<>(sleepData.getTotalRestfulSeconds(), Units.SECOND));
404 updateState(CHANNEL_RIGHT_TODAY_SLEEP_RESTLESS_SECONDS,
405 new QuantityType<>(sleepData.getTotalRestlessSeconds(), Units.SECOND));
409 public void updateMonthlySleepDataChannels(final @Nullable Sleeper sleeper) {
410 SleepIQCloudHandler cloudHandler = getCloudHandler();
411 if (cloudHandler == null || sleeper == null) {
414 SleepDataResponse sleepData = cloudHandler.getMonthlySleepData(sleeper.getSleeperId());
415 if (sleepData == null) {
416 logger.debug("BedHandler: Received no monthly sleep data for bedId={}, sleeperId={}", sleeper.getBedId(),
417 sleeper.getSleeperId());
421 logger.debug("BedHandler: UPDATING MONTHLY SLEEP DATA CHANNELS for bedId={}, sleeperId={}", sleeper.getBedId(),
422 sleeper.getSleeperId());
423 if (sleeper.getSide().equals(Side.LEFT)) {
424 updateState(CHANNEL_LEFT_MONTHLY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
425 updateState(CHANNEL_LEFT_MONTHLY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
426 updateState(CHANNEL_LEFT_MONTHLY_AVG_RESPIRATION_RATE,
427 new DecimalType(sleepData.getAverageRespirationRate()));
429 updateState(CHANNEL_RIGHT_MONTHLY_SLEEP_IQ, new DecimalType(sleepData.getAverageSleepIQ()));
430 updateState(CHANNEL_RIGHT_MONTHLY_AVG_HEART_RATE, new DecimalType(sleepData.getAverageHeartRate()));
431 updateState(CHANNEL_RIGHT_MONTHLY_AVG_RESPIRATION_RATE,
432 new DecimalType(sleepData.getAverageRespirationRate()));
436 private void updateProperties() {
437 logger.debug("BedHandler: Updating bed properties for bedId={}", bedId);
438 SleepIQCloudHandler cloudHandler = getCloudHandler();
439 if (cloudHandler != null) {
440 Bed bed = cloudHandler.getBed(bedId);
442 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
443 "No bed found with ID " + bedId);
446 updateProperties(cloudHandler.updateProperties(bed, editProperties()));
448 logger.debug("BedHandler: Checking if foundation is installed for bedId={}", bedId);
449 if (isFoundationInstalled()) {
450 FoundationFeaturesResponse foundationFeaturesResponse = cloudHandler.getFoundationFeatures(bedId);
451 updateProperties(cloudHandler.updateFeatures(bedId, foundationFeaturesResponse, editProperties()));
452 setFoundationFeatures(foundationFeaturesResponse);
457 private @Nullable SleepIQCloudHandler getCloudHandler() {
458 Bridge bridge = getBridge();
459 if (bridge != null) {
460 return (SleepIQCloudHandler) bridge.getHandler();