]> git.basschouten.com Git - openhab-addons.git/blob
30f316084476a52842a56ab7fe4ac1b09df28cf8
[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.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.SleepDataResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
43 import org.openhab.binding.sleepiq.internal.api.enums.Side;
44 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
45 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
46 import org.openhab.core.config.core.status.ConfigStatusMessage;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.ThingTypeUID;
53 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
54 import org.openhab.core.types.Command;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 /**
59  * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Gregory Moyer - Initial contribution
63  */
64 @NonNullByDefault
65 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
66     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
67
68     private static final int SLEEPER_POLLING_INTERVAL_HOURS = 12;
69
70     private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
71
72     private final HttpClient httpClient;
73
74     private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
75
76     private @Nullable ScheduledFuture<?> statusPollingJob;
77     private @Nullable ScheduledFuture<?> sleeperPollingJob;
78
79     private @Nullable SleepIQ cloud;
80
81     private @Nullable List<Sleeper> sleepers;
82
83     public SleepIQCloudHandler(final Bridge bridge, HttpClient httpClient) {
84         super(bridge);
85         this.httpClient = httpClient;
86     }
87
88     @Override
89     public void initialize() {
90         scheduler.execute(() -> {
91             try {
92                 createCloudConnection();
93                 updateListenerManagement();
94                 updateStatus(ThingStatus.ONLINE);
95             } catch (UnauthorizedException e) {
96                 logger.debug("CloudHandler: SleepIQ cloud authentication failed", e);
97                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
98             } catch (LoginException e) {
99                 logger.debug("CloudHandler: SleepIQ cloud login failed", e);
100                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
101                         "SleepIQ cloud login failed: " + e.getMessage());
102             }
103         });
104     }
105
106     @Override
107     public synchronized void dispose() {
108         stopSleeperPollingJob();
109         stopStatusPollingJob();
110         if (cloud != null) {
111             cloud.shutdown();
112         }
113     }
114
115     @Override
116     public void handleCommand(final ChannelUID channelUID, final Command command) {
117         // cloud handler has no channels
118     }
119
120     /**
121      * Validate the config from openHAB
122      *
123      * @return validity status of config parameters
124      */
125     @Override
126     public Collection<ConfigStatusMessage> getConfigStatus() {
127         Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
128         SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
129         String username = config.username;
130         String password = config.password;
131         if (username.isBlank()) {
132             configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
133                     .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
134         }
135         if (password.isBlank()) {
136             configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
137                     .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
138         }
139         return configStatusMessages;
140     }
141
142     /**
143      * Register the given listener to receive bed status updates.
144      *
145      * @param listener the listener to register
146      */
147     public void registerBedStatusListener(final BedStatusListener listener) {
148         bedStatusListeners.add(listener);
149         scheduler.execute(() -> {
150             refreshSleepers();
151             refreshBedStatus();
152             updateListenerManagement();
153         });
154     }
155
156     /**
157      * Unregister the given listener from further bed status updates.
158      *
159      * @param listener the listener to unregister
160      * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
161      *         otherwise
162      */
163     public boolean unregisterBedStatusListener(final BedStatusListener listener) {
164         boolean result = bedStatusListeners.remove(listener);
165         if (result) {
166             updateListenerManagement();
167         }
168         return result;
169     }
170
171     /**
172      * Get a list of all beds registered to the cloud service account.
173      *
174      * @return the list of beds or null if unable to get list
175      */
176     public @Nullable List<Bed> getBeds() {
177         try {
178             return cloud.getBeds();
179         } catch (SleepIQException e) {
180             logger.debug("CloudHandler: Exception getting list of beds", e);
181             return null;
182         }
183     }
184
185     /**
186      * Get the bed corresponding to the given bed id
187      *
188      * @param bedId the bed identifier
189      * @return the identified {@link Bed} or <code>null</code> if no such bed exists
190      */
191     public @Nullable Bed getBed(final @Nullable String bedId) {
192         logger.debug("CloudHandler: Get bed object for bedId={}", bedId);
193         if (bedId == null) {
194             return null;
195         }
196         List<Bed> beds = getBeds();
197         if (beds != null) {
198             for (Bed bed : beds) {
199                 if (bedId.equals(bed.getBedId())) {
200                     return bed;
201                 }
202             }
203         }
204         return null;
205     }
206
207     /**
208      * Get the sleeper associated with the bedId and side
209      *
210      * @param bedId the bed identifier
211      * @param side the side of the bed
212      * @return the sleeper or null if sleeper not found
213      */
214     public @Nullable Sleeper getSleeper(@Nullable String bedId, Side side) {
215         logger.debug("CloudHandler: Get sleeper object for bedId={}, side={}", bedId, side);
216         if (bedId == null) {
217             return null;
218         }
219         List<Sleeper> localSleepers = sleepers;
220         if (localSleepers != null) {
221             for (Sleeper sleeper : localSleepers) {
222                 if (bedId.equals(sleeper.getBedId()) && side.equals(sleeper.getSide())) {
223                     return sleeper;
224                 }
225             }
226         }
227         return null;
228     }
229
230     /**
231      * Set the sleep number of the specified chamber
232      *
233      * @param bedId the bed identifier
234      * @param sleepNumber the sleep number multiple of 5 between 5 and 100
235      * @param side the chamber to set
236      */
237     public void setSleepNumber(@Nullable String bedId, Side side, int sleepNumber) {
238         if (bedId == null) {
239             return;
240         }
241         try {
242             cloud.setSleepNumber(bedId, side, sleepNumber);
243         } catch (SleepIQException e) {
244             logger.debug("CloudHandler: Exception setting sleep number of bed={}", bedId, e);
245         }
246     }
247
248     /**
249      * Set the pause mode of the specified bed
250      *
251      * @param bedId the bed identifier
252      * @param mode turn pause mode on or off
253      */
254     public void setPauseMode(@Nullable String bedId, boolean mode) {
255         if (bedId == null) {
256             return;
257         }
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      * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
267      * created.
268      *
269      * @param bed the source of data
270      * @param properties the properties to update (this may be <code>null</code>)
271      * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
272      */
273     public Map<String, String> updateProperties(final @Nullable Bed bed, Map<String, String> properties) {
274         if (bed != null) {
275             logger.debug("CloudHandler: Updating bed properties for bed={}", bed.getBedId());
276             properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
277             properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
278             if (bed.isKidsBed() != null) {
279                 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
280             }
281             properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
282             properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
283             if (bed.getPurchaseDate() != null) {
284                 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
285             }
286             properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
287             properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
288         }
289         return properties;
290     }
291
292     /**
293      * Retrieve the latest status on all beds and update all registered listeners
294      * with bed status, sleepers and sleep data.
295      */
296     private void refreshBedStatus() {
297         logger.debug("CloudHandler: Refreshing BED STATUS, updating chanels with status, sleepers, and sleep data");
298         try {
299             FamilyStatusResponse familyStatus = cloud.getFamilyStatus();
300             if (familyStatus != null && familyStatus.getBeds() != null) {
301                 updateStatus(ThingStatus.ONLINE);
302                 for (BedStatus bedStatus : familyStatus.getBeds()) {
303                     logger.debug("CloudHandler: Informing listeners with bed status for bedId={}",
304                             bedStatus.getBedId());
305                     bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(bedStatus));
306                 }
307
308                 List<Sleeper> localSleepers = sleepers;
309                 if (localSleepers != null) {
310                     for (Sleeper sleeper : localSleepers) {
311                         logger.debug("CloudHandler: Informing listeners with sleepers for sleeperId={}",
312                                 sleeper.getSleeperId());
313                         bedStatusListeners.stream().forEach(l -> l.onSleeperChanged(sleeper));
314                     }
315                 }
316                 return;
317             }
318         } catch (SleepIQException e) {
319             logger.debug("CloudHandler: Exception refreshing bed status", e);
320         }
321         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Unable to connect to SleepIQ cloud");
322     }
323
324     /**
325      * Refresh the list of sleepers
326      */
327     private void refreshSleepers() {
328         logger.debug("CloudHandler: Refreshing SLEEPERS");
329         try {
330             sleepers = cloud.getSleepers();
331         } catch (SleepIQException e) {
332             logger.debug("CloudHandler: Exception refreshing list of sleepers", e);
333         }
334     }
335
336     public @Nullable SleepDataResponse getDailySleepData(String sleeperId) {
337         return getSleepData(sleeperId, SleepDataInterval.DAY);
338     }
339
340     public @Nullable SleepDataResponse getMonthlySleepData(String sleeperId) {
341         return getSleepData(sleeperId, SleepDataInterval.MONTH);
342     }
343
344     private @Nullable SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval) {
345         try {
346             return cloud.getSleepData(sleeperId, interval);
347         } catch (SleepIQException e) {
348             logger.debug("CloudHandler: Exception getting sleep data for sleeperId={}", sleeperId, e);
349         }
350         return null;
351     }
352
353     /**
354      * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
355      *
356      * @throws LoginException if there is an error while authenticating to the service
357      */
358     private void createCloudConnection() throws LoginException {
359         SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
360         Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
361                 .withPassword(bindingConfig.password).withLogging(logger.isTraceEnabled());
362         logger.debug("CloudHandler: Authenticating at the SleepIQ cloud service");
363         cloud = SleepIQ.create(cloudConfig, httpClient);
364         cloud.login();
365     }
366
367     /**
368      * Start or stop the background polling jobs
369      */
370     private synchronized void updateListenerManagement() {
371         startSleeperPollingJob();
372         startStatusPollingJob();
373     }
374
375     /**
376      * Start or stop the bed status polling job
377      */
378     private void startStatusPollingJob() {
379         ScheduledFuture<?> localPollingJob = statusPollingJob;
380         if (!bedStatusListeners.isEmpty() && (localPollingJob == null || localPollingJob.isCancelled())) {
381             int pollingInterval = getStatusPollingIntervalSeconds();
382             logger.debug("CloudHandler: Scheduling bed status polling job every {} seconds", pollingInterval);
383             statusPollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval,
384                     pollingInterval, TimeUnit.SECONDS);
385         } else if (bedStatusListeners.isEmpty()) {
386             stopStatusPollingJob();
387         }
388     }
389
390     /**
391      * Stop the bed status polling job
392      */
393     private void stopStatusPollingJob() {
394         ScheduledFuture<?> localPollingJob = statusPollingJob;
395         if (localPollingJob != null) {
396             logger.debug("CloudHandler: Canceling bed status polling job");
397             localPollingJob.cancel(true);
398             statusPollingJob = null;
399         }
400     }
401
402     private int getStatusPollingIntervalSeconds() {
403         return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
404     }
405
406     /**
407      * Start or stop the sleeper polling job
408      */
409     private void startSleeperPollingJob() {
410         ScheduledFuture<?> localJob = sleeperPollingJob;
411         if (!bedStatusListeners.isEmpty() && (localJob == null || localJob.isCancelled())) {
412             logger.debug("CloudHandler: Scheduling sleeper polling job every {} hours", SLEEPER_POLLING_INTERVAL_HOURS);
413             sleeperPollingJob = scheduler.scheduleWithFixedDelay(this::refreshSleepers, SLEEPER_POLLING_INTERVAL_HOURS,
414                     SLEEPER_POLLING_INTERVAL_HOURS, TimeUnit.HOURS);
415         } else if (bedStatusListeners.isEmpty()) {
416             stopSleeperPollingJob();
417         }
418     }
419
420     /**
421      * Stop the sleeper polling job
422      */
423     private void stopSleeperPollingJob() {
424         ScheduledFuture<?> localJob = sleeperPollingJob;
425         if (localJob != null && !localJob.isCancelled()) {
426             logger.debug("CloudHandler: Canceling sleeper polling job");
427             localJob.cancel(true);
428             sleeperPollingJob = null;
429         }
430     }
431 }