]> git.basschouten.com Git - openhab-addons.git/blob
1b8a4bf558098ae0488f3b107a614a7134c1a663
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.openhab.binding.sleepiq.api.Configuration;
29 import org.openhab.binding.sleepiq.api.LoginException;
30 import org.openhab.binding.sleepiq.api.SleepIQ;
31 import org.openhab.binding.sleepiq.api.UnauthorizedException;
32 import org.openhab.binding.sleepiq.api.model.Bed;
33 import org.openhab.binding.sleepiq.api.model.BedStatus;
34 import org.openhab.binding.sleepiq.api.model.FamilyStatus;
35 import org.openhab.binding.sleepiq.internal.SleepIQBindingConstants;
36 import org.openhab.binding.sleepiq.internal.SleepIQConfigStatusMessage;
37 import org.openhab.binding.sleepiq.internal.config.SleepIQCloudConfiguration;
38 import org.openhab.core.cache.ExpiringCache;
39 import org.openhab.core.config.core.status.ConfigStatusMessage;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.ThingTypeUID;
46 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
47 import org.openhab.core.types.Command;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link SleepIQCloudHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Gregory Moyer - Initial contribution
56  */
57 public class SleepIQCloudHandler extends ConfigStatusBridgeHandler {
58     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Collections.singleton(THING_TYPE_CLOUD);
59
60     private final Logger logger = LoggerFactory.getLogger(SleepIQCloudHandler.class);
61
62     private final List<BedStatusListener> bedStatusListeners = new CopyOnWriteArrayList<>();
63
64     private ExpiringCache<FamilyStatus> statusCache;
65
66     private ScheduledFuture<?> pollingJob;
67
68     private SleepIQ cloud;
69
70     public SleepIQCloudHandler(final Bridge bridge) {
71         super(bridge);
72     }
73
74     @Override
75     public void initialize() {
76         try {
77             logger.debug("Configuring bed status cache");
78             statusCache = new ExpiringCache<>(TimeUnit.SECONDS.toMillis(getPollingInterval() / 2),
79                     () -> cloud.getFamilyStatus());
80
81             createCloudConnection();
82
83             logger.debug("Setting SleepIQ cloud online");
84             updateListenerManagement();
85             updateStatus(ThingStatus.ONLINE);
86         } catch (UnauthorizedException e) {
87             logger.debug("SleepIQ cloud authentication failed", e);
88             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid SleepIQ credentials");
89         } catch (LoginException e) {
90             logger.debug("SleepIQ cloud login failed", e);
91             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
92                     "SleepIQ cloud login failed: " + e.getMessage());
93         } catch (Exception e) {
94             logger.debug("Unexpected error while communicating with SleepIQ cloud", e);
95             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
96                     "Unable to connect to SleepIQ cloud: " + e.getMessage());
97         }
98     }
99
100     /**
101      * Create a new SleepIQ cloud service connection. If a connection already exists, it will be lost.
102      *
103      * @throws LoginException if there is an error while authenticating to the service
104      */
105     private void createCloudConnection() throws LoginException {
106         logger.debug("Reading SleepIQ cloud binding configuration");
107         SleepIQCloudConfiguration bindingConfig = getConfigAs(SleepIQCloudConfiguration.class);
108
109         logger.debug("Creating SleepIQ client");
110         Configuration cloudConfig = new Configuration().withUsername(bindingConfig.username)
111                 .withPassword(bindingConfig.password).withLogging(logger.isDebugEnabled());
112         cloud = SleepIQ.create(cloudConfig);
113
114         logger.debug("Authenticating at the SleepIQ cloud service");
115         cloud.login();
116
117         logger.info("Successfully authenticated at the SleepIQ cloud service");
118     }
119
120     @Override
121     public synchronized void dispose() {
122         logger.debug("Disposing SleepIQ cloud handler");
123
124         if (pollingJob != null && !pollingJob.isCancelled()) {
125             pollingJob.cancel(true);
126             pollingJob = null;
127         }
128     }
129
130     /**
131      * Start or stop a background polling job to look for bed status updates based on whether or not there are any
132      * listeners to notify.
133      */
134     private synchronized void updateListenerManagement() {
135         if (!bedStatusListeners.isEmpty() && (pollingJob == null || pollingJob.isCancelled())) {
136             int pollingInterval = getPollingInterval();
137             pollingJob = scheduler.scheduleWithFixedDelay(this::refreshBedStatus, pollingInterval, pollingInterval,
138                     TimeUnit.SECONDS);
139         } else if (bedStatusListeners.isEmpty() && pollingJob != null && !pollingJob.isCancelled()) {
140             pollingJob.cancel(true);
141             pollingJob = null;
142         }
143     }
144
145     /**
146      * Retrieve the polling interval for updating bed status.
147      *
148      * @return the polling interval in seconds
149      */
150     private int getPollingInterval() {
151         return getConfigAs(SleepIQCloudConfiguration.class).pollingInterval;
152     }
153
154     /**
155      * Retrieve the latest status on all beds and update all registered listeners.
156      */
157     public void refreshBedStatus() {
158         try {
159             FamilyStatus status = statusCache.getValue();
160             updateStatus(ThingStatus.ONLINE);
161
162             for (BedStatus bedStatus : status.getBeds()) {
163                 bedStatusListeners.stream().forEach(l -> l.onBedStateChanged(cloud, bedStatus));
164             }
165         } catch (Exception e) {
166             logger.debug("Unexpected error while communicating with SleepIQ cloud", e);
167             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
168                     "Unable to connect to SleepIQ cloud: " + e.getMessage());
169         }
170     }
171
172     /**
173      * Register the given listener to receive bed status updates.
174      *
175      * @param listener the listener to register
176      */
177     public void registerBedStatusListener(final BedStatusListener listener) {
178         if (listener == null) {
179             return;
180         }
181
182         bedStatusListeners.add(listener);
183         refreshBedStatus();
184         updateListenerManagement();
185     }
186
187     /**
188      * Unregister the given listener from further bed status updates.
189      *
190      * @param listener the listener to unregister
191      * @return <code>true</code> if listener was previously registered and is now unregistered; <code>false</code>
192      *         otherwise
193      */
194     public boolean unregisterBedStatusListener(final BedStatusListener listener) {
195         boolean result = bedStatusListeners.remove(listener);
196         if (result) {
197             updateListenerManagement();
198         }
199
200         return result;
201     }
202
203     @Override
204     public void handleCommand(final ChannelUID channelUID, final Command command) {
205         // cloud handler has no channels
206     }
207
208     @Override
209     public Collection<ConfigStatusMessage> getConfigStatus() {
210         Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
211
212         SleepIQCloudConfiguration config = getConfigAs(SleepIQCloudConfiguration.class);
213         String username = config.username;
214         String password = config.password;
215
216         if (username.isEmpty()) {
217             configStatusMessages.add(ConfigStatusMessage.Builder.error(USERNAME)
218                     .withMessageKeySuffix(SleepIQConfigStatusMessage.USERNAME_MISSING).withArguments(USERNAME).build());
219         }
220
221         if (password.isEmpty()) {
222             configStatusMessages.add(ConfigStatusMessage.Builder.error(PASSWORD)
223                     .withMessageKeySuffix(SleepIQConfigStatusMessage.PASSWORD_MISSING).withArguments(PASSWORD).build());
224         }
225
226         return configStatusMessages;
227     }
228
229     /**
230      * Get a list of all beds registered to the cloud service account.
231      *
232      * @return the list of beds (never <code>null</code>)
233      */
234     public List<Bed> getBeds() {
235         return cloud.getBeds();
236     }
237
238     /**
239      * Get the {@link Bed} corresponding to the given identifier.
240      *
241      * @param bedId the bed identifier
242      * @return the identified {@link Bed} or <code>null</code> if no such bed exists
243      */
244     public Bed getBed(final String bedId) {
245         for (Bed bed : getBeds()) {
246             if (bedId.equals(bed.getBedId())) {
247                 return bed;
248             }
249         }
250
251         return null;
252     }
253
254     /**
255      * Update the given properties with attributes of the given bed. If no properties are given, a new map will be
256      * created.
257      *
258      * @param bed the source of data
259      * @param properties the properties to update (this may be <code>null</code>)
260      * @return the given map (or a new map if no map was given) with updated/set properties from the supplied bed
261      */
262     public Map<String, String> updateProperties(final Bed bed, Map<String, String> properties) {
263         if (bed != null) {
264             properties.put(Thing.PROPERTY_MODEL_ID, bed.getModel());
265             properties.put(SleepIQBindingConstants.PROPERTY_BASE, bed.getBase());
266             if (bed.isKidsBed() != null) {
267                 properties.put(SleepIQBindingConstants.PROPERTY_KIDS_BED, bed.isKidsBed().toString());
268             }
269             properties.put(SleepIQBindingConstants.PROPERTY_MAC_ADDRESS, bed.getMacAddress());
270             properties.put(SleepIQBindingConstants.PROPERTY_NAME, bed.getName());
271             if (bed.getPurchaseDate() != null) {
272                 properties.put(SleepIQBindingConstants.PROPERTY_PURCHASE_DATE, bed.getPurchaseDate().toString());
273             }
274             properties.put(SleepIQBindingConstants.PROPERTY_SIZE, bed.getSize());
275             properties.put(SleepIQBindingConstants.PROPERTY_SKU, bed.getSku());
276         }
277
278         return properties;
279     }
280 }