]> git.basschouten.com Git - openhab-addons.git/blob
880a70811386e6b87613ae3af31eb1d731b11be7
[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.Collections;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Set;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
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.ResponseFormatException;
36 import org.openhab.binding.sleepiq.internal.api.SleepIQ;
37 import org.openhab.binding.sleepiq.internal.api.SleepIQException;
38 import org.openhab.binding.sleepiq.internal.api.UnauthorizedException;
39 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
40 import org.openhab.binding.sleepiq.internal.api.dto.BedStatus;
41 import org.openhab.binding.sleepiq.internal.api.dto.FamilyStatusResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.FoundationFeaturesResponse;
43 import org.openhab.binding.sleepiq.internal.api.dto.FoundationStatusResponse;
44 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
45 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
46 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuator;
47 import org.openhab.binding.sleepiq.internal.api.enums.FoundationActuatorSpeed;
48 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutlet;
49 import org.openhab.binding.sleepiq.internal.api.enums.FoundationOutletOperation;
50 import org.openhab.binding.sleepiq.internal.api.enums.FoundationPreset;
51 import org.openhab.binding.sleepiq.internal.api.enums.Side;
52 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
53 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
54 import org.openhab.core.config.core.status.ConfigStatusMessage;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.ThingTypeUID;
61 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
62 import org.openhab.core.types.Command;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 /**
67  * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
68  * sent to one of the channels.
69  *
70  * @author Gregory Moyer - Initial contribution
71  */
72 @NonNullByDefault
73 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
74     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
75
76     private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
77
78     private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
79
80     private final HttpClient httpClient;
81
82     private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
83
84     private @Nullable ScheduledFuture<?> statusPollingJob;
85     private @Nullable ScheduledFuture<?> sleeperPollingJob;
86
87     private @Nullable SleepIQ cloud;
88
89     private @Nullable List<Sleeper> sleepers;
90
91     public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
92         super(bridge);
93         this.httpClient = httpClient;
94     }
95
96     @Override
97     public void initialize() {
98         scheduler.execute(() -> {
99             try {
100                 createCloudConnection();
101                 updateListenerManagement();
102                 updateStatus(ThingStatus.ONLINE);
103             } catch (UnauthorizedException e) {
104                 logger.debug("CloudHandler: SleepIQ cloud authentication failed", e);
105                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
106             } catch (LoginException e) {
107                 logger.debug("CloudHandler: SleepIQ cloud login failed", e);
108                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
109                         "SleepIQ cloud login failed: " + e.getMessage());
110             }
111         });
112     }
113
114     @Override
115     public synchronized void dispose() {
116         stopSleeperPollingJob();
117         stopStatusPollingJob();
118         if (cloud != null) {
119             cloud.shutdown();
120         }
121     }
122
123     @Override
124     public void handleCommand(final ChannelUID channelUID, final Command command) {
125         // cloud handler has no channels
126     }
127
128     /**
129      * Validate the config from openHAB
130      *
131      * @return validity status of config parameters
132      */
133     @Override
134     public Collection<ConfigStatusMessage> getConfigStatus() {
135         Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
136         SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
137         String username = config.username;
138         String password = config.password;
139         if (username.isBlank()) {
140             configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
141                     .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
142         }
143         if (password.isBlank()) {
144             configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
145                     .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
146         }
147         return configStatusMessages;
148     }
149
150     /**
151      * Register the given listener to receive bed status updates.
152      *
153      * @param listener the listener to register
154      */
155     public void registerBedStatusListener(final BedStatusListener listener) {
156         bedStatusListeners.add(listener);
157         /*
158          * Delay the initial sleeper and status update to give some time for the property update
159          * to determine if a foundation is installed.
160          */
161         scheduler.schedule(() -> {
162             refreshSleepers();
163             refreshBedStatus();
164             updateListenerManagement();
165         }, 10L, TimeUnit.SECONDS);
166     }
167
168     /**
169      * Unregister the given listener from further bed status updates.
170      *
171      * @param listener the listener to unregister
172      * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
173      *         otherwise
174      */
175     public boolean unregisterBedStatusListener(final BedStatusListener listener) {
176         boolean result = bedStatusListeners.remove(listener);
177         if (result) {
178             updateListenerManagement();
179         }
180         return result;
181     }
182
183     /**
184      * Get a list of all beds registered to the cloud service account.
185      *
186      * @return the list of beds or null if unable to get list
187      */
188     public @Nullable List<Bed> getBeds() {
189         try {
190             return cloud.getBeds();
191         } catch (SleepIQException e) {
192             logger.debug("CloudHandler: Exception getting list of beds", e);
193             return null;
194         }
195     }
196
197     /**
198      * Get the bed corresponding to the given bed id
199      *
200      * @param bedId the bed identifier
201      * @return the identified {@link Bed} or <code>null</code> if no such bed exists
202      */
203     public @Nullable Bed getBed(final String bedId) {
204         logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
205         List<Bed> beds = getBeds();
206         if (beds != null) {
207             for (Bed bed : beds) {
208                 if (bedId.equals(bed.getBedId())) {
209                     return bed;
210                 }
211             }
212         }
213         return null;
214     }
215
216     /**
217      * Get the sleeper associated with the bedId and side
218      *
219      * @param bedId the bed identifier
220      * @param side the side of the bed
221      * @return the sleeper or null if sleeper not found
222      */
223     public @Nullable Sleeper getSleeper(String bedId, Side side) {
224         logger.debug("CloudHandler: Get sleeper object for bedId={}, side={}", bedId, side);
225         List<Sleeper> localSleepers = sleepers;
226         if (localSleepers != null) {
227             for (Sleeper sleeper : localSleepers) {
228                 if (bedId.equals(sleeper.getBedId()) && side.equals(sleeper.getSide())) {
229                     return sleeper;
230                 }
231             }
232         }
233         return null;
234     }
235
236     /**
237      * Set the sleep number of the specified chamber
238      *
239      * @param bedId the bed identifier
240      * @param sleepNumber the sleep number multiple of 5 between 5 and 100
241      * @param side the chamber to set
242      */
243     public void setSleepNumber(String bedId, Side side, int sleepNumber) {
244         try {
245             cloud.setSleepNumber(bedId, side, sleepNumber);
246         } catch (SleepIQException e) {
247             logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
248         }
249     }
250
251     /**
252      * Set the pause mode of the specified bed
253      *
254      * @param bedId the bed identifier
255      * @param mode turn pause mode on or off
256      */
257     public void setPauseMode(String bedId, boolean mode) {
258         try {
259             cloud.setPauseMode(bedId, mode);
260         } catch (SleepIQException e) {
261             logger.debug("CloudHandler: Exception setting pause mode of bed={}", bedId, e);
262         }
263     }
264
265     /**
266      * Get the foundation features of the specified bed
267      *
268      * @param bedId the bed identifier
269      */
270     public @Nullable FoundationFeaturesResponse getFoundationFeatures(String bedId) {
271         try {
272             return cloud.getFoundationFeatures(bedId);
273         } catch (ResponseFormatException e) {
274             logger.debug("CloudHandler: Unable to parse foundation features response for bed={}", bedId);
275         } catch (SleepIQException e) {
276             logger.debug("CloudHandler: Exception getting foundation features, bed={}", bedId, e);
277         }
278         return null;
279     }
280
281     /**
282      * Get the foundation status of the specified bed
283      *
284      * @param bedId the bed identifier
285      */
286     public @Nullable FoundationStatusResponse getFoundationStatus(String bedId) {
287         try {
288             return cloud.getFoundationStatus(bedId);
289         } catch (ResponseFormatException e) {
290             logger.debug("CloudHandler: Unable to parse foundation status response for bed={}", bedId);
291         } catch (SleepIQException e) {
292             logger.debug("CloudHandler: Exception getting foundation status, bed={}: {}", bedId, e.getMessage());
293         }
294         return null;
295     }
296
297     /**
298      * Set a foundation adjustment preset.
299      *
300      * @param bedId the bed identifier
301      * @param side the side of the bed
302      * @param preset the preset to be applied
303      * @param speed the speed with which to make the adjustment
304      */
305     public void setFoundationPreset(String bedId, Side side, FoundationPreset preset, FoundationActuatorSpeed speed) {
306         try {
307             cloud.setFoundationPreset(bedId, side, preset, speed);
308         } catch (ResponseFormatException e) {
309             logger.debug("CloudHandler: ResponseFormatException setting foundation preset for bed={}: {}", bedId,
310                     e.getMessage());
311         } catch (SleepIQException e) {
312             logger.debug("CloudHandler: Exception setting the foundation preset for bed={}", bedId, e);
313         }
314         return;
315     }
316
317     /**
318      * Set a foundation position on head or foot of bed side.
319      *
320      * @param bedId the bed identifier
321      * @param side the side of the bed
322      * @param actuator the head or foot of the bed
323      * @param position the new position of the actuator
324      * @param speed the speed with which to make the adjustment
325      */
326     public void setFoundationPosition(String bedId, Side side, FoundationActuator actuator, int position,
327             FoundationActuatorSpeed speed) {
328         try {
329             cloud.setFoundationPosition(bedId, side, actuator, position, speed);
330         } catch (ResponseFormatException e) {
331             logger.debug("CloudHandler: ResponseFormatException setting foundation position for bed={}: {}", bedId,
332                     e.getMessage());
333         } catch (SleepIQException e) {
334             logger.debug("CloudHandler: Exception setting the foundation position for bed={}", bedId, e);
335         }
336         return;
337     }
338
339     /**
340      * Operate an outlet on the foundation.
341      *
342      * @param bedId the bed identifier
343      * @param outlet the outlet to operate
344      * @param operation the operation (On or Off) performed on the outlet
345      */
346     public void setFoundationOutlet(String bedId, FoundationOutlet outlet, FoundationOutletOperation operation) {
347         try {
348             cloud.setFoundationOutlet(bedId, outlet, operation);
349         } catch (ResponseFormatException e) {
350             logger.debug("CloudHandler: ResponseFormatException setting the foundation outlet for bed={}: {}", bedId,
351                     e.getMessage());
352         } catch (SleepIQException e) {
353             logger.debug("CloudHandler: Exception setting the foundation outlet for bed={}", bedId, e);
354         }
355         return;
356     }
357
358     /**
359      * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
360      * created.
361      *
362      * @param bed the source of data
363      * @param properties the properties to update (this may be <code>null</code>)
364      * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
365      */
366     public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
367         if (bed != null) {
368             logger.debug("CloudHandler: Updating bed properties for bed={}", bed.getBedId());
369             properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
370             properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
371             if (bed.isKidsBed() != null) {
372                 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
373             }
374             properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
375             properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
376             if (bed.getPurchaseDate() != null) {
377                 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
378             }
379             properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
380             properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
381         }
382         return properties;
383     }
384
385     /**
386      * Update the given foundation properties with features of the given bed foundation.
387      *
388      * @param bed the source of data
389      * @param features the foundation features to update (this may be <code>null</code>)
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 }