2 * Copyright (c) 2010-2023 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.binding.shelly.internal.util;
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
17 import java.io.UnsupportedEncodingException;
18 import java.math.BigDecimal;
19 import java.math.RoundingMode;
20 import java.net.URLEncoder;
21 import java.nio.charset.StandardCharsets;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.time.DateTimeException;
25 import java.time.Instant;
26 import java.time.LocalDateTime;
27 import java.time.ZoneId;
28 import java.time.ZonedDateTime;
29 import java.time.format.DateTimeFormatter;
31 import javax.measure.Unit;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.shelly.internal.api.ShellyApiException;
36 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.OpenClosedType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.types.Command;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
48 import com.google.gson.Gson;
49 import com.google.gson.JsonSyntaxException;
52 * {@link ShellyUtils} provides general utility functions
54 * @author Markus Michels - Initial contribution
57 public class ShellyUtils {
58 private static final String PRE = "Unable to create object of type ";
59 public static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern(DateTimeType.DATE_PATTERN);
61 public static <T> T fromJson(Gson gson, @Nullable String json, Class<T> classOfT) throws ShellyApiException {
63 T o = fromJson(gson, json, classOfT, true);
65 throw new ShellyApiException("Unable to create JSON object");
70 public static @Nullable <T> T fromJson(Gson gson, @Nullable String json, Class<T> classOfT, boolean exceptionOnNull)
71 throws ShellyApiException {
72 String className = substringAfter(classOfT.getName(), "$");
75 if (exceptionOnNull) {
76 throw new IllegalArgumentException(PRE + className + ": json is null!");
82 if (classOfT.isInstance(json)) {
83 return wrap(classOfT).cast(json);
84 } else if (json.isEmpty()) { // update GSON might return null
85 throw new ShellyApiException(PRE + className + " from empty JSON");
89 T obj = gson.fromJson(json, classOfT);
90 if ((obj == null) && exceptionOnNull) { // new in OH3: fromJson may return null
91 throw new ShellyApiException(PRE + className + " from JSON: " + json);
94 } catch (JsonSyntaxException e) {
95 throw new ShellyApiException(
96 PRE + className + " from JSON (syntax/format error: " + e.getMessage() + "): " + json, e);
97 } catch (RuntimeException e) {
98 throw new ShellyApiException(
99 PRE + className + " from JSON (" + getString(e.getMessage() + "), JSON=" + json), e);
104 @SuppressWarnings("unchecked")
105 private static <T> Class<T> wrap(Class<T> type) {
106 if (type == int.class) {
107 return (Class<T>) Integer.class;
109 if (type == float.class) {
110 return (Class<T>) Float.class;
112 if (type == byte.class) {
113 return (Class<T>) Byte.class;
115 if (type == double.class) {
116 return (Class<T>) Double.class;
118 if (type == long.class) {
119 return (Class<T>) Long.class;
121 if (type == char.class) {
122 return (Class<T>) Character.class;
124 if (type == boolean.class) {
125 return (Class<T>) Boolean.class;
127 if (type == short.class) {
128 return (Class<T>) Short.class;
130 if (type == void.class) {
131 return (Class<T>) Void.class;
136 public static String mkChannelId(String group, String channel) {
137 return group + "#" + channel;
140 public static String getString(@Nullable String value) {
141 return value != null ? value : "";
144 public static String substringBefore(@Nullable String string, String pattern) {
145 if (string != null) {
146 int pos = string.indexOf(pattern);
148 return string.substring(0, pos);
154 public static String substringBeforeLast(@Nullable String string, String pattern) {
155 if (string != null) {
156 int pos = string.lastIndexOf(pattern);
158 return string.substring(0, pos);
164 public static String substringAfter(@Nullable String string, String pattern) {
165 if (string != null) {
166 int pos = string.indexOf(pattern);
168 return string.substring(pos + pattern.length());
174 public static String substringAfterLast(@Nullable String string, String pattern) {
175 if (string == null) {
178 int pos = string.lastIndexOf(pattern);
180 return string.substring(pos + pattern.length());
185 public static String substringBetween(@Nullable String string, String begin, String end) {
186 if (string != null) {
187 int s = string.indexOf(begin);
189 // The end tag might be included before the start tag, e.g.
190 // when using "http://" and ":" to get the IP from http://192.168.1.1:8081/xxx
191 // therefore make it 2 steps
192 String result = string.substring(s + begin.length());
193 return substringBefore(result, end);
199 public static String getMessage(Exception e) {
200 String message = e.getMessage();
201 return message != null ? message : "";
204 public static Integer getInteger(@Nullable Integer value) {
205 return (value != null ? (Integer) value : 0);
208 public static Long getLong(@Nullable Long value) {
209 return (value != null ? (Long) value : 0);
212 public static Double getDouble(@Nullable Double value) {
213 return (value != null ? (Double) value : 0);
216 public static Boolean getBool(@Nullable Boolean value) {
217 return (value != null ? (Boolean) value : false);
222 public static StringType getStringType(@Nullable String value) {
223 return new StringType(value != null ? value : "");
226 public static DecimalType getDecimal(@Nullable Double value) {
227 return new DecimalType((value != null ? value : 0));
230 public static DecimalType getDecimal(@Nullable Integer value) {
231 return new DecimalType((value != null ? value : 0));
234 public static DecimalType getDecimal(@Nullable Long value) {
235 return new DecimalType((value != null ? value : 0));
238 public static Double getNumber(Command command) throws IllegalArgumentException {
239 if (command instanceof DecimalType decimalCommand) {
240 return decimalCommand.doubleValue();
242 if (command instanceof QuantityType quantityCommand) {
243 return quantityCommand.doubleValue();
245 throw new IllegalArgumentException("Unable to convert number");
248 public static OnOffType getOnOff(@Nullable Boolean value) {
249 return OnOffType.from(value != null && value);
252 public static OpenClosedType getOpenClosed(@Nullable Boolean value) {
253 return (value != null && value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
256 public static OnOffType getOnOff(int value) {
257 return OnOffType.from(value != 0);
260 public static State toQuantityType(@Nullable Double value, int digits, Unit<?> unit) {
262 return UnDefType.NULL;
264 BigDecimal bd = new BigDecimal(value.doubleValue());
265 return toQuantityType(bd.setScale(digits, RoundingMode.HALF_UP), unit);
268 public static State toQuantityType(@Nullable Number value, Unit<?> unit) {
269 return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
272 public static State toQuantityType(@Nullable PercentType value, Unit<?> unit) {
273 return value == null ? UnDefType.NULL : toQuantityType(value.toBigDecimal(), unit);
276 public static void validateRange(String name, Integer value, int min, int max) {
277 if ((value < min) || (value > max)) {
278 throw new IllegalArgumentException("Value " + name + " is out of range (" + min + "-" + max + ")");
282 public static String urlEncode(String input) {
284 return URLEncoder.encode(input, StandardCharsets.UTF_8.toString());
285 } catch (UnsupportedEncodingException e) {
290 public static Long now() {
291 return System.currentTimeMillis() / 1000L;
294 public static DateTimeType getTimestamp() {
295 return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(now()), ZoneId.systemDefault()));
298 public static DateTimeType getTimestamp(String zone, long timestamp) {
300 ZoneId zoneId = !zone.isEmpty() ? ZoneId.of(zone) : ZoneId.systemDefault();
301 ZonedDateTime zdt = LocalDateTime.now().atZone(zoneId);
302 int delta = zdt.getOffset().getTotalSeconds();
303 return new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(timestamp - delta), zoneId));
304 } catch (DateTimeException e) {
305 // Unable to convert device's timezone, use system one
306 return getTimestamp();
310 public static String getTimestamp(DateTimeType dt) {
311 return dt.getZonedDateTime().toString().replace('T', ' ').replace('-', '/');
314 public static String convertTimestamp(long ts) {
318 String time = DATE_TIME.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault()));
319 return time.replace('T', ' ').replace('-', '/');
322 public static Integer getLightIdFromGroup(String groupName) {
323 if (groupName.startsWith(CHANNEL_GROUP_LIGHT_CHANNEL)) {
324 return Integer.parseInt(substringAfter(groupName, CHANNEL_GROUP_LIGHT_CHANNEL)) - 1;
326 return 0; // only 1 light, e.g. bulb or rgbw2 in color mode
329 public static String buildControlGroupName(ShellyDeviceProfile profile, Integer channelId) {
330 return !profile.isRGBW2 || profile.inColor ? CHANNEL_GROUP_LIGHT_CONTROL
331 : CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
334 public static String buildWhiteGroupName(ShellyDeviceProfile profile, Integer channelId) {
335 return profile.isBulb || profile.isDuo ? CHANNEL_GROUP_WHITE_CONTROL
336 : CHANNEL_GROUP_LIGHT_CHANNEL + channelId.toString();
339 public static DecimalType mapSignalStrength(int dbm) {
343 } else if (dbm > -70) {
345 } else if (dbm > -80) {
347 } else if (dbm > -90) {
352 return new DecimalType(strength);
355 public static boolean isDigit(char c) {
356 return c >= '0' && c <= '9';
359 public static char lastChar(String s) {
360 return s.length() > 1 ? s.charAt(s.length() - 1) : '*';
363 public static String sha256(String string) throws ShellyApiException {
365 MessageDigest digest = MessageDigest.getInstance("SHA-256");
366 final byte[] hashbytes = digest.digest(string.getBytes(StandardCharsets.UTF_8));
367 return bytesToHex(hashbytes);
368 } catch (NoSuchAlgorithmException e) {
369 throw new ShellyApiException("SHA256 can't be initialzed", e);
373 public static String bytesToHex(byte[] bytes) {
374 StringBuilder hexString = new StringBuilder(2 * bytes.length);
375 for (int i = 0; i < bytes.length; i++) {
376 String hex = Integer.toHexString(0xff & bytes[i]);
377 if (hex.length() == 1) {
378 hexString.append('0');
380 hexString.append(hex);
382 return hexString.toString();