]> git.basschouten.com Git - openhab-addons.git/blob
96a7cac8e39cd58ca81aacc33d6ee0d7032b947d
[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 bedId the source of data
388      * @param features the foundation features to update (this may be <code>null</code>)
389      * @param properties
390      * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
391      */
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");
409         } else {
410             logger.debug("CloudHandler: Foundation not installed on bed={}", bedId);
411             properties.put(SleepIQBindingConstants.PROPERTY_FOUNDATION, "Not installed");
412         }
413         return properties;
414     }
415
416     /**
417      * Retrieve the latest status on all beds and update all registered listeners
418      * with bed status, foundation status, sleepers and sleep data.
419      */
420     private void refreshBedStatus() {
421         logger.debug("CloudHandler: Refreshing BED STATUS, updating chanels with status, sleepers, and sleep data");
422         try {
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));
430
431                     // Get foundation status only if bed has a foundation
432                     bedStatusListeners.stream().filter(l -> l.isFoundationInstalled()).forEach(l -> {
433                         try {
434                             l.onFoundationStateChanged(bedId, cloud.getFoundationStatus(bedStatus.getBedId()));
435                         } catch (SleepIQException e) {
436                             logger.debug("CloudHandler: Exception getting foundation status for bedId={}", bedId);
437                         }
438                     });
439                 }
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));
446                     }
447                 }
448                 return;
449             }
450         } catch (SleepIQException e) {
451             logger.debug("CloudHandler: Exception refreshing bed status", e);
452         }
453         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to SleepIQ cloud");
454     }
455
456     /**
457      * Refresh the list of sleepers
458      */
459     private void refreshSleepers() {
460         logger.debug("CloudHandler: Refreshing SLEEPERS");
461         try {
462             sleepers = cloud.getSleepers();
463         } catch (SleepIQException e) {
464             logger.debug("CloudHandler: Exception refreshing list of sleepers", e);
465         }
466     }
467
468     public @Nullable SleepDataResponse getDailySleepData(String sleeperId) {
469         return getSleepData(sleeperId, SleepDataInterval.DAY);
470     }
471
472     public @Nullable SleepDataResponse getMonthlySleepData(String sleeperId) {
473         return getSleepData(sleeperId, SleepDataInterval.MONTH);
474     }
475
476     private @Nullable SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval) {
477         try {
478             return cloud.getSleepData(sleeperId, interval);
479         } catch (SleepIQException e) {
480             logger.debug("CloudHandler: Exception getting sleep data for sleeperId={}", sleeperId, e);
481         }
482         return null;
483     }
484
485     /**
486      * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
487      *
488      * @throws LoginException if there is an error while authenticating to the service
489      */
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);
496         cloud.login();
497     }
498
499     /**
500      * Start or stop the background polling jobs
501      */
502     private synchronized void updateListenerManagement() {
503         startSleeperPollingJob();
504         startStatusPollingJob();
505     }
506
507     /**
508      * Start or stop the bed status polling job
509      */
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();
519         }
520     }
521
522     /**
523      * Stop the bed status polling job
524      */
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;
531         }
532     }
533
534     private int getStatusPollingIntervalSeconds() {
535         return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
536     }
537
538     /**
539      * Start or stop the sleeper polling job
540      */
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();
549         }
550     }
551
552     /**
553      * Stop the sleeper polling job
554      */
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;
561         }
562     }
563 }