]> git.basschouten.com Git - openhab-addons.git/blob
033eeda6d9d6467a393684cba64dc56f54201607
[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.io.openhabcloud.internal;
14
15 import java.io.File;
16 import java.io.FileInputStream;
17 import java.io.FileNotFoundException;
18 import java.io.FileOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Set;
28
29 import org.apache.commons.io.IOUtils;
30 import org.apache.commons.lang.RandomStringUtils;
31 import org.apache.commons.lang.StringUtils;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.openhab.core.OpenHAB;
34 import org.openhab.core.config.core.ConfigConstants;
35 import org.openhab.core.config.core.ConfigurableService;
36 import org.openhab.core.events.Event;
37 import org.openhab.core.events.EventFilter;
38 import org.openhab.core.events.EventPublisher;
39 import org.openhab.core.events.EventSubscriber;
40 import org.openhab.core.id.InstanceUUID;
41 import org.openhab.core.io.net.http.HttpClientFactory;
42 import org.openhab.core.items.Item;
43 import org.openhab.core.items.ItemNotFoundException;
44 import org.openhab.core.items.ItemRegistry;
45 import org.openhab.core.items.events.ItemEventFactory;
46 import org.openhab.core.items.events.ItemStateEvent;
47 import org.openhab.core.library.items.RollershutterItem;
48 import org.openhab.core.library.items.SwitchItem;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.library.types.UpDownType;
51 import org.openhab.core.model.script.engine.action.ActionService;
52 import org.openhab.core.net.HttpServiceUtil;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.TypeParser;
55 import org.openhab.io.openhabcloud.NotificationAction;
56 import org.osgi.framework.BundleContext;
57 import org.osgi.framework.Constants;
58 import org.osgi.service.component.annotations.Activate;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Deactivate;
61 import org.osgi.service.component.annotations.Modified;
62 import org.osgi.service.component.annotations.Reference;
63 import org.osgi.service.component.annotations.ReferenceCardinality;
64 import org.osgi.service.component.annotations.ReferencePolicy;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * This class starts the cloud connection service and implements interface to communicate with the cloud.
70  *
71  * @author Victor Belov - Initial contribution
72  * @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
73  */
74 @Component(immediate = true, service = { EventSubscriber.class,
75         ActionService.class }, configurationPid = "org.openhab.openhabcloud", property = {
76                 Constants.SERVICE_PID + "=org.openhab.openhabcloud",
77                 ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=io:openhabcloud",
78                 ConfigurableService.SERVICE_PROPERTY_LABEL + "=openHAB Cloud",
79                 ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=io" })
80 public class CloudService implements ActionService, CloudClientListener, EventSubscriber {
81
82     private static final String CFG_EXPOSE = "expose";
83     private static final String CFG_BASE_URL = "baseURL";
84     private static final String CFG_MODE = "mode";
85     private static final String SECRET_FILE_NAME = "openhabcloud" + File.separator + "secret";
86     private static final String DEFAULT_URL = "https://myopenhab.org/";
87     private static final int DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS = 200;
88     private static final int DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT = 30000;
89     private static final String HTTPCLIENT_NAME = "openhabcloud";
90
91     private Logger logger = LoggerFactory.getLogger(CloudService.class);
92
93     public static String clientVersion = null;
94     private CloudClient cloudClient;
95     private String cloudBaseUrl = null;
96     private HttpClient httpClient;
97     protected ItemRegistry itemRegistry = null;
98     protected EventPublisher eventPublisher = null;
99
100     private boolean remoteAccessEnabled = true;
101     private Set<String> exposedItems = null;
102     private int localPort;
103
104     public CloudService() {
105     }
106
107     /**
108      * This method sends notification message to mobile app through the openHAB Cloud service
109      *
110      * @param userId the {@link String} containing the openHAB Cloud user id to send message to
111      * @param message the {@link String} containing a message to send to specified user id
112      * @param icon the {@link String} containing a name of the icon to be used with this notification
113      * @param severity the {@link String} containing severity (good, info, warning, error) of notification
114      */
115     public void sendNotification(String userId, String message, String icon, String severity) {
116         logger.debug("Sending message '{}' to user id {}", message, userId);
117         cloudClient.sendNotification(userId, message, icon, severity);
118     }
119
120     /**
121      * Sends an advanced notification to log. Log notifications are not pushed to user
122      * devices but are shown to all account users in notifications log
123      *
124      * @param message the {@link String} containing a message to send to specified user id
125      * @param icon the {@link String} containing a name of the icon to be used with this notification
126      * @param severity the {@link String} containing severity (good, info, warning, error) of notification
127      */
128     public void sendLogNotification(String message, String icon, String severity) {
129         logger.debug("Sending log message '{}'", message);
130         cloudClient.sendLogNotification(message, icon, severity);
131     }
132
133     /**
134      * Sends a broadcast notification. Broadcast notifications are pushed to all
135      * mobile devices of all users of the account
136      *
137      * @param message the {@link String} containing a message to send to specified user id
138      * @param icon the {@link String} containing a name of the icon to be used with this notification
139      * @param severity the {@link String} containing severity (good, info, warning, error) of notification
140      */
141     public void sendBroadcastNotification(String message, String icon, String severity) {
142         logger.debug("Sending broadcast message '{}' to all users", message);
143         cloudClient.sendBroadcastNotification(message, icon, severity);
144     }
145
146     @Activate
147     protected void activate(BundleContext context, Map<String, ?> config) {
148         clientVersion = StringUtils.substringBefore(context.getBundle().getVersion().toString(), ".qualifier");
149         localPort = HttpServiceUtil.getHttpServicePort(context);
150         if (localPort == -1) {
151             logger.warn("openHAB Cloud connector not started, since no local HTTP port could be determined");
152         } else {
153             logger.debug("openHAB Cloud connector activated");
154             checkJavaVersion();
155             modified(config);
156         }
157     }
158
159     private void checkJavaVersion() {
160         String version = System.getProperty("java.version");
161         if (version.charAt(2) == '8') {
162             // we are on Java 8, let's check the update
163             String update = version.substring(version.indexOf('_') + 1);
164             try {
165                 Integer uVersion = Integer.valueOf(update);
166                 if (uVersion < 101) {
167                     logger.warn(
168                             "You are running Java {} - the openhab Cloud connection requires at least Java 1.8.0_101, if your cloud server uses Let's Encrypt certificates!",
169                             version);
170                 }
171             } catch (NumberFormatException e) {
172                 logger.debug("Could not determine update version of java {}", version);
173             }
174         }
175     }
176
177     @Deactivate
178     protected void deactivate() {
179         logger.debug("openHAB Cloud connector deactivated");
180         cloudClient.shutdown();
181         try {
182             httpClient.stop();
183         } catch (Exception e) {
184             logger.debug("Could not stop Jetty http client", e);
185         }
186     }
187
188     @Modified
189     protected void modified(Map<String, ?> config) {
190         if (config != null && config.get(CFG_MODE) != null) {
191             remoteAccessEnabled = "remote".equals(config.get(CFG_MODE));
192         } else {
193             logger.debug("remoteAccessEnabled is not set, keeping value '{}'", remoteAccessEnabled);
194         }
195
196         if (config.get(CFG_BASE_URL) != null) {
197             cloudBaseUrl = (String) config.get(CFG_BASE_URL);
198         } else {
199             cloudBaseUrl = DEFAULT_URL;
200         }
201
202         exposedItems = new HashSet<>();
203         Object expCfg = config.get(CFG_EXPOSE);
204         if (expCfg instanceof String) {
205             String value = (String) expCfg;
206             while (value.startsWith("[")) {
207                 value = value.substring(1);
208             }
209             while (value.endsWith("]")) {
210                 value = value.substring(0, value.length() - 1);
211             }
212             for (String itemName : Arrays.asList((value).split(","))) {
213                 exposedItems.add(itemName.trim());
214             }
215         } else if (expCfg instanceof Iterable) {
216             for (Object entry : ((Iterable<?>) expCfg)) {
217                 exposedItems.add(entry.toString());
218             }
219         }
220
221         logger.debug("UUID = {}, secret = {}", InstanceUUID.get(), getSecret());
222
223         if (cloudClient != null) {
224             cloudClient.shutdown();
225         }
226
227         httpClient.setMaxConnectionsPerDestination(DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS);
228         httpClient.setConnectTimeout(DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT);
229         httpClient.setFollowRedirects(false);
230         if (!httpClient.isRunning()) {
231             try {
232                 httpClient.start();
233             } catch (Exception e) {
234                 logger.error("Could not start Jetty http client", e);
235             }
236         }
237
238         String localBaseUrl = "http://localhost:" + localPort;
239         cloudClient = new CloudClient(httpClient, InstanceUUID.get(), getSecret(), cloudBaseUrl, localBaseUrl,
240                 remoteAccessEnabled, exposedItems);
241         cloudClient.setOpenHABVersion(OpenHAB.getVersion());
242         cloudClient.connect();
243         cloudClient.setListener(this);
244         NotificationAction.cloudService = this;
245     }
246
247     @Override
248     public String getActionClassName() {
249         return NotificationAction.class.getCanonicalName();
250     }
251
252     @Override
253     public Class<?> getActionClass() {
254         return NotificationAction.class;
255     }
256
257     /**
258      * Reads the first line from specified file
259      */
260
261     private String readFirstLine(File file) {
262         List<String> lines = null;
263         try (InputStream fis = new FileInputStream(file)) {
264             lines = IOUtils.readLines(fis);
265         } catch (IOException ioe) {
266             // no exception handling - we just return the empty String
267         }
268         return lines != null && !lines.isEmpty() ? lines.get(0) : "";
269     }
270
271     /**
272      * Writes a String to a specified file
273      */
274
275     private void writeFile(File file, String content) {
276         // create intermediary directories
277         file.getParentFile().mkdirs();
278         try (OutputStream fos = new FileOutputStream(file)) {
279             IOUtils.write(content, fos);
280             logger.debug("Created file '{}' with content '{}'", file.getAbsolutePath(), content);
281         } catch (FileNotFoundException e) {
282             logger.error("Couldn't create file '{}'.", file.getPath(), e);
283         } catch (IOException e) {
284             logger.error("Couldn't write to file '{}'.", file.getPath(), e);
285         }
286     }
287
288     /**
289      * Creates a random secret and writes it to the <code>userdata/openhabcloud</code>
290      * directory. An existing <code>secret</code> file won't be overwritten.
291      * Returns either existing secret from the file or newly created secret.
292      */
293     private String getSecret() {
294         File file = new File(ConfigConstants.getUserDataFolder() + File.separator + SECRET_FILE_NAME);
295         String newSecretString = "";
296
297         if (!file.exists()) {
298             newSecretString = RandomStringUtils.randomAlphanumeric(20);
299             logger.debug("New secret = {}", newSecretString);
300             writeFile(file, newSecretString);
301         } else {
302             newSecretString = readFirstLine(file);
303             logger.debug("Using secret at '{}' with content '{}'", file.getAbsolutePath(), newSecretString);
304         }
305
306         return newSecretString;
307     }
308
309     @Override
310     public void sendCommand(String itemName, String commandString) {
311         try {
312             if (itemRegistry != null) {
313                 Item item = itemRegistry.getItem(itemName);
314                 Command command = null;
315                 if (item != null) {
316                     if (this.eventPublisher != null) {
317                         if ("toggle".equalsIgnoreCase(commandString)
318                                 && (item instanceof SwitchItem || item instanceof RollershutterItem)) {
319                             if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) {
320                                 command = OnOffType.OFF;
321                             }
322                             if (OnOffType.OFF.equals(item.getStateAs(OnOffType.class))) {
323                                 command = OnOffType.ON;
324                             }
325                             if (UpDownType.UP.equals(item.getStateAs(UpDownType.class))) {
326                                 command = UpDownType.DOWN;
327                             }
328                             if (UpDownType.DOWN.equals(item.getStateAs(UpDownType.class))) {
329                                 command = UpDownType.UP;
330                             }
331                         } else {
332                             command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
333                         }
334                         if (command != null) {
335                             logger.debug("Received command '{}' for item '{}'", commandString, itemName);
336                             this.eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command));
337                         } else {
338                             logger.warn("Received invalid command '{}' for item '{}'", commandString, itemName);
339                         }
340                     }
341                 } else {
342                     logger.warn("Received command '{}' for non-existent item '{}'", commandString, itemName);
343                 }
344             } else {
345                 return;
346             }
347         } catch (ItemNotFoundException e) {
348             logger.warn("Received command for a non-existent item '{}'", itemName);
349         }
350     }
351
352     @Reference
353     protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
354         this.httpClient = httpClientFactory.createHttpClient(HTTPCLIENT_NAME);
355         this.httpClient.setStopTimeout(0);
356     }
357
358     protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) {
359         this.httpClient = null;
360     }
361
362     @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC)
363     public void setItemRegistry(ItemRegistry itemRegistry) {
364         this.itemRegistry = itemRegistry;
365     }
366
367     public void unsetItemRegistry(ItemRegistry itemRegistry) {
368         this.itemRegistry = null;
369     }
370
371     @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
372     public void setEventPublisher(EventPublisher eventPublisher) {
373         this.eventPublisher = eventPublisher;
374     }
375
376     public void unsetEventPublisher(EventPublisher eventPublisher) {
377         this.eventPublisher = null;
378     }
379
380     @Override
381     public Set<String> getSubscribedEventTypes() {
382         return Collections.singleton(ItemStateEvent.TYPE);
383     }
384
385     @Override
386     public EventFilter getEventFilter() {
387         return null;
388     }
389
390     @Override
391     public void receive(Event event) {
392         ItemStateEvent ise = (ItemStateEvent) event;
393         if (exposedItems != null && exposedItems.contains(ise.getItemName())) {
394             cloudClient.sendItemUpdate(ise.getItemName(), ise.getItemState().toString());
395         }
396     }
397 }