]> git.basschouten.com Git - openhab-addons.git/blob
73b4e9fefd197936de00263a67848b5d6b572dcf
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.sleepiq.internal.handler;
14
15 import static org.openhab.binding.sleepiq.internal.SleepIQBindingConstants.THING_TYPE_CLOUD;
16 import static org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration.*;
17
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
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;
64
65 /**
66  * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
67  * sent to one of the channels.
68  *
69  * @author Gregory Moyer - Initial contribution
70  */
71 @NonNullByDefault
72 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
73     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_CLOUD);
74
75     private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
76
77     private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
78
79     private final HttpClient httpClient;
80
81     private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
82
83     private @Nullable ScheduledFuture<?> statusPollingJob;
84     private @Nullable ScheduledFuture<?> sleeperPollingJob;
85
86     private @Nullable SleepIQ cloud;
87
88     private @Nullable List<Sleeper> sleepers;
89
90     public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
91         super(bridge);
92         this.httpClient = httpClient;
93     }
94
95     @Override
96     public void initialize() {
97         scheduler.execute(() -> {
98             try {
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());
109             }
110         });
111     }
112
113     @Override
114     public synchronized void dispose() {
115         stopSleeperPollingJob();
116         stopStatusPollingJob();
117         if (cloud != null) {
118             cloud.shutdown();
119         }
120     }
121
122     @Override
123     public void handleCommand(final ChannelUID channelUID, final Command command) {
124         // cloud handler has no channels
125     }
126
127     /**
128      * Validate the config from openHAB
129      *
130      * @return validity status of config parameters
131      */
132     @Override
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());
141         }
142         if (password.isBlank()) {
143             configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
144                     .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
145         }
146         return configStatusMessages;
147     }
148
149     /**
150      * Register the given listener to receive bed status updates.
151      *
152      * @param listener the listener to register
153      */
154     public void registerBedStatusListener(final BedStatusListener listener) {
155         bedStatusListeners.add(listener);
156         /*
157          * Delay the initial sleeper and status update to give some time for the property update
158          * to determine if a foundation is installed.
159          */
160         scheduler.schedule(() -> {
161             refreshSleepers();
162             refreshBedStatus();
163             updateListenerManagement();
164         }, 10L, TimeUnit.SECONDS);
165     }
166
167     /**
168      * Unregister the given listener from further bed status updates.
169      *
170      * @param listener the listener to unregister
171      * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
172      *         otherwise
173      */
174     public boolean unregisterBedStatusListener(final BedStatusListener listener) {
175         boolean result = bedStatusListeners.remove(listener);
176         if (result) {
177             updateListenerManagement();
178         }
179         return result;
180     }
181
182     /**
183      * Get a list of all beds registered to the cloud service account.
184      *
185      * @return the list of beds or null if unable to get list
186      */
187     public @Nullable List<Bed> getBeds() {
188         try {
189             return cloud.getBeds();
190         } catch (SleepIQException e) {
191             logger.debug("CloudHandler: Exception getting list of beds", e);
192             return null;
193         }
194     }
195
196     /**
197      * Get the bed corresponding to the given bed id
198      *
199      * @param bedId the bed identifier
200      * @return the identified {@link Bed} or <code>null</code> if no such bed exists
201      */
202     public @Nullable Bed getBed(final String bedId) {
203         logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
204         List<Bed> beds = getBeds();
205         if (beds != null) {
206             for (Bed bed : beds) {
207                 if (bedId.equals(bed.getBedId())) {
208                     return bed;
209                 }
210             }
211         }
212         return null;
213     }
214
215     /**
216      * Get the sleeper associated with the bedId and side
217      *
218      * @param bedId the bed identifier
219      * @param side the side of the bed
220      * @return the sleeper or null if sleeper not found
221      */
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())) {
228                     return sleeper;
229                 }
230             }
231         }
232         return null;
233     }
234
235     /**
236      * Set the sleep number of the specified chamber
237      *
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
241      */
242     public void setSleepNumber(String bedId, Side side, int sleepNumber) {
243         try {
244             cloud.setSleepNumber(bedId, side, sleepNumber);
245         } catch (SleepIQException e) {
246             logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
247         }
248     }
249
250     /**
251      * Set the pause mode of the specified bed
252      *
253      * @param bedId the bed identifier
254      * @param mode turn pause mode on or off
255      */
256     public void setPauseMode(String bedId, boolean mode) {
257         try {
258             cloud.setPauseMode(bedId, mode);
259         } catch (SleepIQException e) {
260             logger.debug("CloudHandler: Exception setting pause mode of bed={}", bedId, e);
261         }
262     }
263
264     /**
265      * Get the foundation features of the specified bed
266      *
267      * @param bedId the bed identifier
268      */
269     public @Nullable FoundationFeaturesResponse getFoundationFeatures(String bedId) {
270         try {
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);
276         }
277         return null;
278     }
279
280     /**
281      * Get the foundation status of the specified bed
282      *
283      * @param bedId the bed identifier
284      */
285     public @Nullable FoundationStatusResponse getFoundationStatus(String bedId) {
286         try {
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());
292         }
293         return null;
294     }
295
296     /**
297      * Set a foundation adjustment preset.
298      *
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
303      */
304     public void setFoundationPreset(String bedId, Side side, FoundationPreset preset, FoundationActuatorSpeed speed) {
305         try {
306             cloud.setFoundationPreset(bedId, side, preset, speed);
307         } catch (ResponseFormatException e) {
308             logger.debug("CloudHandler: ResponseFormatException setting foundation preset for bed={}: {}", bedId,
309                     e.getMessage());
310         } catch (SleepIQException e) {
311             logger.debug("CloudHandler: Exception setting the foundation preset for bed={}", bedId, e);
312         }
313         return;
314     }
315
316     /**
317      * Set a foundation position on head or foot of bed side.
318      *
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
324      */
325     public void setFoundationPosition(String bedId, Side side, FoundationActuator actuator, int position,
326             FoundationActuatorSpeed speed) {
327         try {
328             cloud.setFoundationPosition(bedId, side, actuator, position, speed);
329         } catch (ResponseFormatException e) {
330             logger.debug("CloudHandler: ResponseFormatException setting foundation position for bed={}: {}", bedId,
331                     e.getMessage());
332         } catch (SleepIQException e) {
333             logger.debug("CloudHandler: Exception setting the foundation position for bed={}", bedId, e);
334         }
335         return;
336     }
337
338     /**
339      * Operate an outlet on the foundation.
340      *
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
344      */
345     public void setFoundationOutlet(String bedId, FoundationOutlet outlet, FoundationOutletOperation operation) {
346         try {
347             cloud.setFoundationOutlet(bedId, outlet, operation);
348         } catch (ResponseFormatException e) {
349             logger.debug("CloudHandler: ResponseFormatException setting the foundation outlet for bed={}: {}", bedId,
350                     e.getMessage());
351         } catch (SleepIQException e) {
352             logger.debug("CloudHandler: Exception setting the foundation outlet for bed={}", bedId, e);
353         }
354         return;
355     }
356
357     /**
358      * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
359      * created.
360      *
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
364      */
365     public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
366         if (bed != null) {
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());
372             }
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());
377             }
378             properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
379             properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
380         }
381         return properties;
382     }
383
384     /**
385      * Update the given foundation properties with features of the given bed foundation.
386      *
387      * @param bed the source of data
388      * @param features the foundation features to update (this may be <code>null</code>)
389      * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
390      */
391     public Map<String, String> updateFeatures(final String bedId, final @Nullable FoundationFeaturesResponse features,
392             Map<String, String> properties) {
393         if (features != null) {
394             logger.debug("CloudHandler: Updating foundation properties for bed={}", bedId);
395             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION, "Installed");
396             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HW_REV,
397                     String.valueOf(features.getBoardHWRev()));
398             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_IS_BOARD_AS_SINGLE,
399                     features.isBoardAsSingle() ? "yes" : "no");
400             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_MASSAGE_AND_LIGHT,
401                     features.hasMassageAndLight() ? "yes" : "no");
402             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_FOOT_CONTROL,
403                     features.hasFootControl() ? "yes" : "no");
404             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_FOOT_WARMER,
405                     features.hasFootWarming() ? "yes" : "no");
406             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION_HAS_UNDER_BED_LIGHT,
407                     features.hasUnderBedLight() ? "yes" : "no");
408         } else {
409             logger.debug("CloudHandler: Foundation not installed on bed={}", bedId);
410             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION, "Not installed");
411         }
412         return properties;
413     }
414
415     /**
416      * Retrieve the latest status on all beds and update all registered listeners
417      * with bed status, foundation status, sleepers and sleep data.
418      */
419     private void refreshBedStatus() {
420         logger.debug("CloudHandler: Refreshing BED STATUS, updating chanels with status, sleepers, and sleep data");
421         try {
422             FamilyStatusResponse familyStatus = cloud.getFamilyStatus();
423             if (familyStatus.getBeds() != null) {
424                 updateStatus(ThingStatus.ONLINE);
425                 for (BedStatus bedStatus : familyStatus.getBeds()) {
426                     String bedId = bedStatus.getBedId();
427                     logger.debug("CloudHandler: Informing listeners with bed status for bedId={}", bedId);
428                     bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(bedStatus));
429
430                     // Get foundation status only if bed has a foundation
431                     bedStatusListeners.stream().filter(l -> l.isFoundationInstalled()).forEach(l -> {
432                         try {
433                             l.onFoundationStateChanged(bedId, cloud.getFoundationStatus(bedStatus.getBedId()));
434                         } catch (SleepIQException e) {
435                             logger.debug("CloudHandler: Exception getting foundation status for bedId={}", bedId);
436                         }
437                     });
438                 }
439                 List<Sleeper> localSleepers = sleepers;
440                 if (localSleepers != null) {
441                     for (Sleeper sleeper : localSleepers) {
442                         logger.debug("CloudHandler: Informing listeners with sleepers for sleeperId={}",
443                                 sleeper.getSleeperId());
444                         bedStatusListeners.stream().forEach(l -> l.onSleeperChanged(sleeper));
445                     }
446                 }
447                 return;
448             }
449         } catch (SleepIQException e) {
450             logger.debug("CloudHandler: Exception refreshing bed status", e);
451         }
452         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to SleepIQ cloud");
453     }
454
455     /**
456      * Refresh the list of sleepers
457      */
458     private void refreshSleepers() {
459         logger.debug("CloudHandler: Refreshing SLEEPERS");
460         try {
461             sleepers = cloud.getSleepers();
462         } catch (SleepIQException e) {
463             logger.debug("CloudHandler: Exception refreshing list of sleepers", e);
464         }
465     }
466
467     public @Nullable SleepDataResponse getDailySleepData(String sleeperId) {
468         return getSleepData(sleeperId, SleepDataInterval.DAY);
469     }
470
471     public @Nullable SleepDataResponse getMonthlySleepData(String sleeperId) {
472         return getSleepData(sleeperId, SleepDataInterval.MONTH);
473     }
474
475     private @Nullable SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval) {
476         try {
477             return cloud.getSleepData(sleeperId, interval);
478         } catch (SleepIQException e) {
479             logger.debug("CloudHandler: Exception getting sleep data for sleeperId={}", sleeperId, e);
480         }
481         return null;
482     }
483
484     /**
485      * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
486      *
487      * @throws LoginException if there is an error while authenticating to the service
488      */
489     private void createCloudConnection() throws LoginException {
490         SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
491         Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
492                 .withPassword(bindingConfig.password);
493         logger.debug("CloudHandler: Authenticating at the SleepIQ cloud service");
494         cloud = SleepIQ.create(cloudConfig, httpClient);
495         cloud.login();
496     }
497
498     /**
499      * Start or stop the background polling jobs
500      */
501     private synchronized void updateListenerManagement() {
502         startSleeperPollingJob();
503         startStatusPollingJob();
504     }
505
506     /**
507      * Start or stop the bed status polling job
508      */
509     private void startStatusPollingJob() {
510         ScheduledFuture<?> localPollingJob = statusPollingJob;
511         if (!bedStatusListeners.isEmpty() && (localPollingJob == null || localPollingJob.isCancelled())) {
512             int pollingInterval = getStatusPollingIntervalSeconds();
513             logger.debug("CloudHandler: Scheduling bed status polling job every {} seconds", pollingInterval);
514             statusPollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval,
515                     pollingInterval, TimeUnit.SECONDS);
516         } else if (bedStatusListeners.isEmpty()) {
517             stopStatusPollingJob();
518         }
519     }
520
521     /**
522      * Stop the bed status polling job
523      */
524     private void stopStatusPollingJob() {
525         ScheduledFuture<?> localPollingJob = statusPollingJob;
526         if (localPollingJob != null) {
527             logger.debug("CloudHandler: Canceling bed status polling job");
528             localPollingJob.cancel(true);
529             statusPollingJob = null;
530         }
531     }
532
533     private int getStatusPollingIntervalSeconds() {
534         return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
535     }
536
537     /**
538      * Start or stop the sleeper polling job
539      */
540     private void startSleeperPollingJob() {
541         ScheduledFuture<?> localJob = sleeperPollingJob;
542         if (!bedStatusListeners.isEmpty() && (localJob == null || localJob.isCancelled())) {
543             logger.debug("CloudHandler: Scheduling sleeper polling job every {} hours", SLEEPER_POLLING_INTERVAL_HOURS);
544             sleeperPollingJob = scheduler.scheduleWithFixedDelay(this::refreshSleepers, SLEEPER_POLLING_INTERVAL_HOURS,
545                     SLEEPER_POLLING_INTERVAL_HOURS, TimeUnit.HOURS);
546         } else if (bedStatusListeners.isEmpty()) {
547             stopSleeperPollingJob();
548         }
549     }
550
551     /**
552      * Stop the sleeper polling job
553      */
554     private void stopSleeperPollingJob() {
555         ScheduledFuture<?> localJob = sleeperPollingJob;
556         if (localJob != null && !localJob.isCancelled()) {
557             logger.debug("CloudHandler: Canceling sleeper polling job");
558             localJob.cancel(true);
559             sleeperPollingJob = null;
560         }
561     }
562 }