2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.openhabcloud.internal;
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;
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.ConfigurableService;
35 import org.openhab.core.events.Event;
36 import org.openhab.core.events.EventFilter;
37 import org.openhab.core.events.EventPublisher;
38 import org.openhab.core.events.EventSubscriber;
39 import org.openhab.core.id.InstanceUUID;
40 import org.openhab.core.io.net.http.HttpClientFactory;
41 import org.openhab.core.items.Item;
42 import org.openhab.core.items.ItemNotFoundException;
43 import org.openhab.core.items.ItemRegistry;
44 import org.openhab.core.items.events.ItemEventFactory;
45 import org.openhab.core.items.events.ItemStateEvent;
46 import org.openhab.core.library.items.RollershutterItem;
47 import org.openhab.core.library.items.SwitchItem;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.UpDownType;
50 import org.openhab.core.model.script.engine.action.ActionService;
51 import org.openhab.core.net.HttpServiceUtil;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.TypeParser;
54 import org.openhab.io.openhabcloud.NotificationAction;
55 import org.osgi.framework.BundleContext;
56 import org.osgi.framework.Constants;
57 import org.osgi.service.component.annotations.Activate;
58 import org.osgi.service.component.annotations.Component;
59 import org.osgi.service.component.annotations.Deactivate;
60 import org.osgi.service.component.annotations.Modified;
61 import org.osgi.service.component.annotations.Reference;
62 import org.osgi.service.component.annotations.ReferenceCardinality;
63 import org.osgi.service.component.annotations.ReferencePolicy;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
68 * This class starts the cloud connection service and implements interface to communicate with the cloud.
70 * @author Victor Belov - Initial contribution
71 * @author Kai Kreuzer - migrated code to new Jetty client and ESH APIs
73 @Component(immediate = true, service = { EventSubscriber.class,
74 ActionService.class }, configurationPid = "org.openhab.openhabcloud", property = {
75 Constants.SERVICE_PID + "=org.openhab.openhabcloud",
76 ConfigurableService.SERVICE_PROPERTY_DESCRIPTION_URI + "=io:openhabcloud",
77 ConfigurableService.SERVICE_PROPERTY_LABEL + "=openHAB Cloud",
78 ConfigurableService.SERVICE_PROPERTY_CATEGORY + "=io" })
79 public class CloudService implements ActionService, CloudClientListener, EventSubscriber {
81 private static final String CFG_EXPOSE = "expose";
82 private static final String CFG_BASE_URL = "baseURL";
83 private static final String CFG_MODE = "mode";
84 private static final String SECRET_FILE_NAME = "openhabcloud" + File.separator + "secret";
85 private static final String DEFAULT_URL = "https://myopenhab.org/";
86 private static final int DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS = 200;
87 private static final int DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT = 30000;
88 private static final String HTTPCLIENT_NAME = "openhabcloud";
90 private Logger logger = LoggerFactory.getLogger(CloudService.class);
92 public static String clientVersion = null;
93 private CloudClient cloudClient;
94 private String cloudBaseUrl = null;
95 private HttpClient httpClient;
96 protected ItemRegistry itemRegistry = null;
97 protected EventPublisher eventPublisher = null;
99 private boolean remoteAccessEnabled = true;
100 private Set<String> exposedItems = null;
101 private int localPort;
103 public CloudService() {
107 * This method sends notification message to mobile app through the openHAB Cloud service
109 * @param userId the {@link String} containing the openHAB Cloud user id to send message to
110 * @param message the {@link String} containing a message to send to specified user id
111 * @param icon the {@link String} containing a name of the icon to be used with this notification
112 * @param severity the {@link String} containing severity (good, info, warning, error) of notification
114 public void sendNotification(String userId, String message, String icon, String severity) {
115 logger.debug("Sending message '{}' to user id {}", message, userId);
116 cloudClient.sendNotification(userId, message, icon, severity);
120 * Sends an advanced notification to log. Log notifications are not pushed to user
121 * devices but are shown to all account users in notifications log
123 * @param message the {@link String} containing a message to send to specified user id
124 * @param icon the {@link String} containing a name of the icon to be used with this notification
125 * @param severity the {@link String} containing severity (good, info, warning, error) of notification
127 public void sendLogNotification(String message, String icon, String severity) {
128 logger.debug("Sending log message '{}'", message);
129 cloudClient.sendLogNotification(message, icon, severity);
133 * Sends a broadcast notification. Broadcast notifications are pushed to all
134 * mobile devices of all users of the account
136 * @param message the {@link String} containing a message to send to specified user id
137 * @param icon the {@link String} containing a name of the icon to be used with this notification
138 * @param severity the {@link String} containing severity (good, info, warning, error) of notification
140 public void sendBroadcastNotification(String message, String icon, String severity) {
141 logger.debug("Sending broadcast message '{}' to all users", message);
142 cloudClient.sendBroadcastNotification(message, icon, severity);
146 protected void activate(BundleContext context, Map<String, ?> config) {
147 clientVersion = StringUtils.substringBefore(context.getBundle().getVersion().toString(), ".qualifier");
148 localPort = HttpServiceUtil.getHttpServicePort(context);
149 if (localPort == -1) {
150 logger.warn("openHAB Cloud connector not started, since no local HTTP port could be determined");
152 logger.debug("openHAB Cloud connector activated");
158 private void checkJavaVersion() {
159 String version = System.getProperty("java.version");
160 if (version.charAt(2) == '8') {
161 // we are on Java 8, let's check the update
162 String update = version.substring(version.indexOf('_') + 1);
164 Integer uVersion = Integer.valueOf(update);
165 if (uVersion < 101) {
167 "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!",
170 } catch (NumberFormatException e) {
171 logger.debug("Could not determine update version of java {}", version);
177 protected void deactivate() {
178 logger.debug("openHAB Cloud connector deactivated");
179 cloudClient.shutdown();
182 } catch (Exception e) {
183 logger.debug("Could not stop Jetty http client", e);
188 protected void modified(Map<String, ?> config) {
189 if (config != null && config.get(CFG_MODE) != null) {
190 remoteAccessEnabled = "remote".equals(config.get(CFG_MODE));
192 logger.debug("remoteAccessEnabled is not set, keeping value '{}'", remoteAccessEnabled);
195 if (config.get(CFG_BASE_URL) != null) {
196 cloudBaseUrl = (String) config.get(CFG_BASE_URL);
198 cloudBaseUrl = DEFAULT_URL;
201 exposedItems = new HashSet<>();
202 Object expCfg = config.get(CFG_EXPOSE);
203 if (expCfg instanceof String) {
204 String value = (String) expCfg;
205 while (value.startsWith("[")) {
206 value = value.substring(1);
208 while (value.endsWith("]")) {
209 value = value.substring(0, value.length() - 1);
211 for (String itemName : Arrays.asList((value).split(","))) {
212 exposedItems.add(itemName.trim());
214 } else if (expCfg instanceof Iterable) {
215 for (Object entry : ((Iterable<?>) expCfg)) {
216 exposedItems.add(entry.toString());
220 logger.debug("UUID = {}, secret = {}", InstanceUUID.get(), getSecret());
222 if (cloudClient != null) {
223 cloudClient.shutdown();
226 httpClient.setMaxConnectionsPerDestination(DEFAULT_LOCAL_OPENHAB_MAX_CONCURRENT_REQUESTS);
227 httpClient.setConnectTimeout(DEFAULT_LOCAL_OPENHAB_REQUEST_TIMEOUT);
228 httpClient.setFollowRedirects(false);
229 if (!httpClient.isRunning()) {
232 } catch (Exception e) {
233 logger.error("Could not start Jetty http client", e);
237 String localBaseUrl = "http://localhost:" + localPort;
238 cloudClient = new CloudClient(httpClient, InstanceUUID.get(), getSecret(), cloudBaseUrl, localBaseUrl,
239 remoteAccessEnabled, exposedItems);
240 cloudClient.setOpenHABVersion(OpenHAB.getVersion());
241 cloudClient.connect();
242 cloudClient.setListener(this);
243 NotificationAction.cloudService = this;
247 public String getActionClassName() {
248 return NotificationAction.class.getCanonicalName();
252 public Class<?> getActionClass() {
253 return NotificationAction.class;
257 * Reads the first line from specified file
260 private String readFirstLine(File file) {
261 List<String> lines = null;
262 try (InputStream fis = new FileInputStream(file)) {
263 lines = IOUtils.readLines(fis);
264 } catch (IOException ioe) {
265 // no exception handling - we just return the empty String
267 return lines != null && !lines.isEmpty() ? lines.get(0) : "";
271 * Writes a String to a specified file
274 private void writeFile(File file, String content) {
275 // create intermediary directories
276 file.getParentFile().mkdirs();
277 try (OutputStream fos = new FileOutputStream(file)) {
278 IOUtils.write(content, fos);
279 logger.debug("Created file '{}' with content '{}'", file.getAbsolutePath(), content);
280 } catch (FileNotFoundException e) {
281 logger.error("Couldn't create file '{}'.", file.getPath(), e);
282 } catch (IOException e) {
283 logger.error("Couldn't write to file '{}'.", file.getPath(), e);
288 * Creates a random secret and writes it to the <code>userdata/openhabcloud</code>
289 * directory. An existing <code>secret</code> file won't be overwritten.
290 * Returns either existing secret from the file or newly created secret.
292 private String getSecret() {
293 File file = new File(OpenHAB.getUserDataFolder() + File.separator + SECRET_FILE_NAME);
294 String newSecretString = "";
296 if (!file.exists()) {
297 newSecretString = RandomStringUtils.randomAlphanumeric(20);
298 logger.debug("New secret = {}", newSecretString);
299 writeFile(file, newSecretString);
301 newSecretString = readFirstLine(file);
302 logger.debug("Using secret at '{}' with content '{}'", file.getAbsolutePath(), newSecretString);
305 return newSecretString;
309 public void sendCommand(String itemName, String commandString) {
311 if (itemRegistry != null) {
312 Item item = itemRegistry.getItem(itemName);
313 Command command = null;
315 if (this.eventPublisher != null) {
316 if ("toggle".equalsIgnoreCase(commandString)
317 && (item instanceof SwitchItem || item instanceof RollershutterItem)) {
318 if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) {
319 command = OnOffType.OFF;
321 if (OnOffType.OFF.equals(item.getStateAs(OnOffType.class))) {
322 command = OnOffType.ON;
324 if (UpDownType.UP.equals(item.getStateAs(UpDownType.class))) {
325 command = UpDownType.DOWN;
327 if (UpDownType.DOWN.equals(item.getStateAs(UpDownType.class))) {
328 command = UpDownType.UP;
331 command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString);
333 if (command != null) {
334 logger.debug("Received command '{}' for item '{}'", commandString, itemName);
335 this.eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command));
337 logger.warn("Received invalid command '{}' for item '{}'", commandString, itemName);
341 logger.warn("Received command '{}' for non-existent item '{}'", commandString, itemName);
346 } catch (ItemNotFoundException e) {
347 logger.warn("Received command for a non-existent item '{}'", itemName);
352 protected void setHttpClientFactory(HttpClientFactory httpClientFactory) {
353 this.httpClient = httpClientFactory.createHttpClient(HTTPCLIENT_NAME);
354 this.httpClient.setStopTimeout(0);
357 protected void unsetHttpClientFactory(HttpClientFactory httpClientFactory) {
358 this.httpClient = null;
361 @Reference(cardinality = ReferenceCardinality.MANDATORY, policy = ReferencePolicy.DYNAMIC)
362 public void setItemRegistry(ItemRegistry itemRegistry) {
363 this.itemRegistry = itemRegistry;
366 public void unsetItemRegistry(ItemRegistry itemRegistry) {
367 this.itemRegistry = null;
370 @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
371 public void setEventPublisher(EventPublisher eventPublisher) {
372 this.eventPublisher = eventPublisher;
375 public void unsetEventPublisher(EventPublisher eventPublisher) {
376 this.eventPublisher = null;
380 public Set<String> getSubscribedEventTypes() {
381 return Collections.singleton(ItemStateEvent.TYPE);
385 public EventFilter getEventFilter() {
390 public void receive(Event event) {
391 ItemStateEvent ise = (ItemStateEvent) event;
392 if (exposedItems != null && exposedItems.contains(ise.getItemName())) {
393 cloudClient.sendItemUpdate(ise.getItemName(), ise.getItemState().toString());