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.mystrom.internal;
15 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_BRIGHTNESS;
16 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR;
17 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR_TEMPERATURE;
18 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_MODE;
19 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_POWER;
20 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_RAMP;
21 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_SWITCH;
22 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.HSV;
23 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.MONO;
24 import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.RGB;
25 import static org.openhab.core.library.unit.Units.SECOND;
26 import static org.openhab.core.library.unit.Units.WATT;
28 import java.lang.reflect.Type;
29 import java.time.Duration;
30 import java.util.HashMap;
32 import java.util.Objects;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.eclipse.jdt.annotation.Nullable;
36 import org.eclipse.jetty.client.HttpClient;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.util.Fields;
39 import org.openhab.core.cache.ExpiringCache;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.HSBType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.library.unit.MetricPrefix;
47 import org.openhab.core.thing.ChannelUID;
48 import org.openhab.core.thing.Thing;
49 import org.openhab.core.thing.ThingStatus;
50 import org.openhab.core.thing.ThingStatusDetail;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
56 import com.google.gson.reflect.TypeToken;
59 * The {@link MyStromBulbHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Frederic Chastagnol - Initial contribution
65 public class MyStromBulbHandler extends AbstractMyStromHandler {
67 private static final Type DEVICE_INFO_MAP_TYPE = new TypeToken<HashMap<String, MyStromDeviceSpecificInfo>>() {
70 private final Logger logger = LoggerFactory.getLogger(MyStromBulbHandler.class);
72 private final ExpiringCache<Map<String, MyStromDeviceSpecificInfo>> cache = new ExpiringCache<>(
73 Duration.ofSeconds(3), this::getReport);
75 private PercentType lastBrightness = PercentType.HUNDRED;
76 private PercentType lastColorTemperature = new PercentType(50);
77 private String lastMode = MONO;
78 private HSBType lastColor = HSBType.WHITE;
80 public MyStromBulbHandler(Thing thing, HttpClient httpClient) {
81 super(thing, httpClient);
85 public void handleCommand(ChannelUID channelUID, Command command) {
87 if (command instanceof RefreshType) {
91 switch (channelUID.getId()) {
93 if (command instanceof OnOffType) {
94 sResp = sendToBulb(command == OnOffType.ON ? "on" : "off", null, null, null);
98 if (command instanceof HSBType) {
99 if (Objects.equals(((HSBType) command).as(OnOffType.class), OnOffType.OFF)) {
100 sResp = sendToBulb("off", null, null, null);
102 String hsv = command.toString().replaceAll(",", ";");
103 sResp = sendToBulb("on", hsv, null, HSV);
107 case CHANNEL_BRIGHTNESS:
108 if (command instanceof PercentType) {
109 if (Objects.equals(((PercentType) command).as(OnOffType.class), OnOffType.OFF)) {
110 sResp = sendToBulb("off", null, null, null);
112 if (lastMode.equals(MONO)) {
113 String mono = convertPercentageToMyStromCT(lastColorTemperature) + ";"
114 + command.toString();
115 sResp = sendToBulb("on", mono, null, MONO);
117 String hsv = lastColor.getHue().intValue() + ";" + lastColor.getSaturation() + ";"
118 + command.toString();
119 sResp = sendToBulb("on", hsv, null, HSV);
124 case CHANNEL_COLOR_TEMPERATURE:
125 if (command instanceof PercentType) {
126 String mono = convertPercentageToMyStromCT((PercentType) command) + ";"
127 + lastBrightness.toString();
128 sResp = sendToBulb("on", mono, null, MONO);
132 if (command instanceof DecimalType) {
133 sResp = sendToBulb(null, null, command.toString(), null);
137 if (command instanceof StringType) {
138 sResp = sendToBulb(null, null, null, command.toString());
145 Map<String, MyStromDeviceSpecificInfo> report = gson.fromJson(sResp, DEVICE_INFO_MAP_TYPE);
146 if (report != null) {
147 report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst()
148 .ifPresent(info -> updateDevice(info.getValue()));
152 } catch (MyStromException e) {
153 logger.warn("Error while handling command {}", e.getMessage());
158 protected void checkRequiredInfo() throws MyStromException {
160 throw new MyStromException("Cannot retrieve MAC info from myStrom device " + getThing().getUID());
164 private @Nullable Map<String, MyStromDeviceSpecificInfo> getReport() {
166 String returnContent = sendHttpRequest(HttpMethod.GET, "/api/v1/device", null);
167 Map<String, MyStromDeviceSpecificInfo> report = gson.fromJson(returnContent, DEVICE_INFO_MAP_TYPE);
168 updateStatus(ThingStatus.ONLINE);
170 } catch (MyStromException e) {
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
177 protected void pollDevice() {
178 Map<String, MyStromDeviceSpecificInfo> report = cache.getValue();
179 if (report != null) {
180 report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst()
181 .ifPresent(info -> updateDevice(info.getValue()));
185 private void updateDevice(@Nullable MyStromBulbResponse deviceInfo) {
186 if (deviceInfo != null) {
187 updateState(CHANNEL_SWITCH, deviceInfo.on ? OnOffType.ON : OnOffType.OFF);
188 updateState(CHANNEL_RAMP, QuantityType.valueOf(deviceInfo.ramp, MetricPrefix.MILLI(SECOND)));
189 if (deviceInfo instanceof MyStromDeviceSpecificInfo) {
190 updateState(CHANNEL_POWER, QuantityType.valueOf(((MyStromDeviceSpecificInfo) deviceInfo).power, WATT));
194 lastMode = deviceInfo.mode;
195 long numSemicolon = deviceInfo.color.chars().filter(c -> c == ';').count();
196 if (numSemicolon == 1 && deviceInfo.mode.equals(MONO)) {
197 String[] xy = deviceInfo.color.split(";");
198 lastColorTemperature = new PercentType(convertMyStromCTToPercentage(xy[0]));
199 lastBrightness = PercentType.valueOf(xy[1]);
200 lastColor = new HSBType(lastColor.getHue() + ",0," + lastBrightness);
201 updateState(CHANNEL_COLOR_TEMPERATURE, lastColorTemperature);
202 } else if (numSemicolon == 2 && deviceInfo.mode.equals(HSV)) {
203 lastColor = HSBType.valueOf(deviceInfo.color.replaceAll(";", ","));
204 lastBrightness = lastColor.getBrightness();
205 } else if (!deviceInfo.color.equals("") && deviceInfo.mode.equals(RGB)) {
206 int r = Integer.parseInt(deviceInfo.color.substring(2, 4), 16);
207 int g = Integer.parseInt(deviceInfo.color.substring(4, 6), 16);
208 int b = Integer.parseInt(deviceInfo.color.substring(6, 8), 16);
209 lastColor = HSBType.fromRGB(r, g, b);
210 lastBrightness = lastColor.getBrightness();
212 updateState(CHANNEL_COLOR, lastColor);
213 updateState(CHANNEL_BRIGHTNESS, lastBrightness);
214 updateState(CHANNEL_MODE, StringType.valueOf(lastMode));
215 } catch (IllegalArgumentException e) {
216 logger.warn("Error while updating {}", e.getMessage());
223 * Given a URL and a set parameters, send a HTTP POST request to the URL location
224 * created by the URL and parameters.
226 * @param action The action we want to take (on,off or toggle)
227 * @param color The color we set the bulb to (When using RGBW mode the first two hex numbers are used for the
228 * white channel! hsv is of form <UINT 0..360>;<UINT 0..100>;<UINT 0..100>)
229 * @param ramp Transition time from the light’s current state to the new state. [ms]
230 * @param mode The color mode we want the Bulb to set to (rgb or hsv or mono)
231 * @return String contents of the response for the GET request.
232 * @throws MyStromException Throws on communication error
234 private String sendToBulb(@Nullable String action, @Nullable String color, @Nullable String ramp,
235 @Nullable String mode) throws MyStromException {
236 Fields fields = new Fields();
237 if (action != null) {
238 fields.put("action", action);
241 fields.put("color", color);
244 fields.put("ramp", ramp);
247 fields.put("mode", mode);
249 StringBuilder builder = new StringBuilder(fields.getSize() * 32);
250 for (Fields.Field field : fields) {
251 for (String value : field.getValues()) {
252 if (builder.length() > 0) {
255 builder.append(field.getName()).append("=").append(value);
258 return sendHttpRequest(HttpMethod.POST, "/api/v1/device/" + mac, builder.toString());
262 * Convert the color temperature from myStrom (1-18) to openHAB (percentage)
264 * @param ctValue Color temperature in myStrom: "1" = warm to "18" = cold.
265 * @return Color temperature (0-100%). 0% is the coldest setting.
266 * @throws NumberFormatException if the argument is not an integer
268 private int convertMyStromCTToPercentage(String ctValue) throws NumberFormatException {
269 int ct = Integer.parseInt(ctValue);
270 return Math.round((18 - limitColorTemperature(ct)) / 17F * 100F);
274 * Convert the color temperature from openHAB (percentage) to myStrom (1-18)
276 * @param colorTemperature Color temperature from openHab. 0 = coldest, 100 = warmest
277 * @return Color temperature from myStrom. 1 = warmest, 18 = coldest
279 private String convertPercentageToMyStromCT(PercentType colorTemperature) {
280 int ct = 18 - Math.round(colorTemperature.floatValue() * 17F / 100F);
281 return Integer.toString(limitColorTemperature(ct));
284 private int limitColorTemperature(int colorTemperature) {
285 return Math.max(1, Math.min(colorTemperature, 18));
288 private static class MyStromBulbResponse {
290 public String color = "";
291 public String mode = "";
295 public String toString() {
296 return "MyStromBulbResponse{" + "on=" + on + ", color='" + color + '\'' + ", mode='" + mode + '\''
297 + ", ramp=" + ramp + '}';
301 private static class MyStromDeviceSpecificInfo extends MyStromBulbResponse {