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