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