]> git.basschouten.com Git - openhab-addons.git/blob
9187ca06f2fec684ad01f799c775c399797b7150
[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.mystrom.internal;
14
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;
27
28 import java.lang.reflect.Type;
29 import java.time.Duration;
30 import java.util.HashMap;
31 import java.util.Map;
32 import java.util.Objects;
33
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;
55
56 import com.google.gson.reflect.TypeToken;
57
58 /**
59  * The {@link MyStromBulbHandler} is responsible for handling commands, which are
60  * sent to one of the channels.
61  *
62  * @author Frederic Chastagnol - Initial contribution
63  */
64 @NonNullByDefault
65 public class MyStromBulbHandler extends AbstractMyStromHandler {
66
67     private static final Type DEVICE_INFO_MAP_TYPE = new TypeToken<HashMap<String, MyStromDeviceSpecificInfo>>() {
68     }.getType();
69
70     private final Logger logger = LoggerFactory.getLogger(MyStromBulbHandler.class);
71
72     private final ExpiringCache<Map<String, MyStromDeviceSpecificInfo>> cache = new ExpiringCache<>(
73             Duration.ofSeconds(3), this::getReport);
74
75     private PercentType lastBrightness = PercentType.HUNDRED;
76     private PercentType lastColorTemperature = new PercentType(50);
77     private String lastMode = MONO;
78     private HSBType lastColor = HSBType.WHITE;
79
80     public MyStromBulbHandler(Thing thing, HttpClient httpClient) {
81         super(thing, httpClient);
82     }
83
84     @Override
85     public void handleCommand(ChannelUID channelUID, Command command) {
86         try {
87             if (command instanceof RefreshType) {
88                 pollDevice();
89             } else {
90                 String sResp = null;
91                 switch (channelUID.getId()) {
92                     case CHANNEL_SWITCH:
93                         if (command instanceof OnOffType) {
94                             sResp = sendToBulb(command == OnOffType.ON ? "on" : "off", null, null, null);
95                         }
96                         break;
97                     case CHANNEL_COLOR:
98                         if (command instanceof HSBType hsb) {
99                             if (Objects.equals(hsb.as(OnOffType.class), OnOffType.OFF)) {
100                                 sResp = sendToBulb("off", null, null, null);
101                             } else {
102                                 String hsv = command.toString().replace(",", ";");
103                                 sResp = sendToBulb("on", hsv, null, HSV);
104                             }
105                         }
106                         break;
107                     case CHANNEL_BRIGHTNESS:
108                         if (command instanceof PercentType brightness) {
109                             if (Objects.equals(brightness.as(OnOffType.class), OnOffType.OFF)) {
110                                 sResp = sendToBulb("off", null, null, null);
111                             } else {
112                                 if (lastMode.equals(MONO)) {
113                                     String mono = convertPercentageToMyStromCT(lastColorTemperature) + ";"
114                                             + command.toString();
115                                     sResp = sendToBulb("on", mono, null, MONO);
116                                 } else {
117                                     String hsv = lastColor.getHue().intValue() + ";" + lastColor.getSaturation() + ";"
118                                             + command.toString();
119                                     sResp = sendToBulb("on", hsv, null, HSV);
120                                 }
121                             }
122                         }
123                         break;
124                     case CHANNEL_COLOR_TEMPERATURE:
125                         if (command instanceof PercentType temperature) {
126                             String mono = convertPercentageToMyStromCT(temperature) + ";" + lastBrightness.toString();
127                             sResp = sendToBulb("on", mono, null, MONO);
128                         }
129                         break;
130                     case CHANNEL_RAMP:
131                         if (command instanceof DecimalType) {
132                             sResp = sendToBulb(null, null, command.toString(), null);
133                         }
134                         break;
135                     case CHANNEL_MODE:
136                         if (command instanceof StringType) {
137                             sResp = sendToBulb(null, null, null, command.toString());
138                         }
139                         break;
140                     default:
141                 }
142
143                 if (sResp != null) {
144                     Map<String, MyStromDeviceSpecificInfo> report = gson.fromJson(sResp, DEVICE_INFO_MAP_TYPE);
145                     if (report != null) {
146                         report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst()
147                                 .ifPresent(info -> updateDevice(info.getValue()));
148                     }
149                 }
150             }
151         } catch (MyStromException e) {
152             logger.warn("Error while handling command {}", e.getMessage());
153         }
154     }
155
156     @Override
157     protected void checkRequiredInfo() throws MyStromException {
158         if (mac.isBlank()) {
159             throw new MyStromException("Cannot retrieve MAC info from myStrom device " + getThing().getUID());
160         }
161     }
162
163     private @Nullable Map<String, MyStromDeviceSpecificInfo> getReport() {
164         try {
165             String returnContent = sendHttpRequest(HttpMethod.GET, "/api/v1/device", null);
166             Map<String, MyStromDeviceSpecificInfo> report = gson.fromJson(returnContent, DEVICE_INFO_MAP_TYPE);
167             updateStatus(ThingStatus.ONLINE);
168             return report;
169         } catch (MyStromException e) {
170             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
171             return null;
172         }
173     }
174
175     @Override
176     protected void pollDevice() {
177         Map<String, MyStromDeviceSpecificInfo> report = cache.getValue();
178         if (report != null) {
179             report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst()
180                     .ifPresent(info -> updateDevice(info.getValue()));
181         }
182     }
183
184     private void updateDevice(@Nullable MyStromBulbResponse deviceInfo) {
185         if (deviceInfo != null) {
186             updateState(CHANNEL_SWITCH, deviceInfo.on ? OnOffType.ON : OnOffType.OFF);
187             updateState(CHANNEL_RAMP, QuantityType.valueOf(deviceInfo.ramp, MetricPrefix.MILLI(SECOND)));
188             if (deviceInfo instanceof MyStromDeviceSpecificInfo info) {
189                 updateState(CHANNEL_POWER, QuantityType.valueOf(info.power, WATT));
190             }
191             if (deviceInfo.on) {
192                 try {
193                     lastMode = deviceInfo.mode;
194                     long numSemicolon = deviceInfo.color.chars().filter(c -> c == ';').count();
195                     if (numSemicolon == 1 && deviceInfo.mode.equals(MONO)) {
196                         String[] xy = deviceInfo.color.split(";");
197                         lastColorTemperature = new PercentType(convertMyStromCTToPercentage(xy[0]));
198                         lastBrightness = PercentType.valueOf(xy[1]);
199                         lastColor = new HSBType(lastColor.getHue() + ",0," + lastBrightness);
200                         updateState(CHANNEL_COLOR_TEMPERATURE, lastColorTemperature);
201                     } else if (numSemicolon == 2 && deviceInfo.mode.equals(HSV)) {
202                         lastColor = HSBType.valueOf(deviceInfo.color.replace(";", ","));
203                         lastBrightness = lastColor.getBrightness();
204                     } else if (!"".equals(deviceInfo.color) && deviceInfo.mode.equals(RGB)) {
205                         int r = Integer.parseInt(deviceInfo.color.substring(2, 4), 16);
206                         int g = Integer.parseInt(deviceInfo.color.substring(4, 6), 16);
207                         int b = Integer.parseInt(deviceInfo.color.substring(6, 8), 16);
208                         lastColor = HSBType.fromRGB(r, g, b);
209                         lastBrightness = lastColor.getBrightness();
210                     }
211                     updateState(CHANNEL_COLOR, lastColor);
212                     updateState(CHANNEL_BRIGHTNESS, lastBrightness);
213                     updateState(CHANNEL_MODE, StringType.valueOf(lastMode));
214                 } catch (IllegalArgumentException e) {
215                     logger.warn("Error while updating {}", e.getMessage());
216                 }
217             }
218         }
219     }
220
221     /**
222      * Given a URL and a set parameters, send a HTTP POST request to the URL location
223      * created by the URL and parameters.
224      *
225      * @param action The action we want to take (on,off or toggle)
226      * @param color The color we set the bulb to (When using RGBW mode the first two hex numbers are used for the
227      *            white channel! hsv is of form <UINT 0..360>;<UINT 0..100>;<UINT 0..100>)
228      * @param ramp Transition time from the light’s current state to the new state. [ms]
229      * @param mode The color mode we want the Bulb to set to (rgb or hsv or mono)
230      * @return String contents of the response for the GET request.
231      * @throws MyStromException Throws on communication error
232      */
233     private String sendToBulb(@Nullable String action, @Nullable String color, @Nullable String ramp,
234             @Nullable String mode) throws MyStromException {
235         Fields fields = new Fields();
236         if (action != null) {
237             fields.put("action", action);
238         }
239         if (color != null) {
240             fields.put("color", color);
241         }
242         if (ramp != null) {
243             fields.put("ramp", ramp);
244         }
245         if (mode != null) {
246             fields.put("mode", mode);
247         }
248         StringBuilder builder = new StringBuilder(fields.getSize() * 32);
249         for (Fields.Field field : fields) {
250             for (String value : field.getValues()) {
251                 if (builder.length() > 0) {
252                     builder.append("&");
253                 }
254                 builder.append(field.getName()).append("=").append(value);
255             }
256         }
257         return sendHttpRequest(HttpMethod.POST, "/api/v1/device/" + mac, builder.toString());
258     }
259
260     /**
261      * Convert the color temperature from myStrom (1-18) to openHAB (percentage)
262      *
263      * @param ctValue Color temperature in myStrom: "1" = warm to "18" = cold.
264      * @return Color temperature (0-100%). 0% is the coldest setting.
265      * @throws NumberFormatException if the argument is not an integer
266      */
267     private int convertMyStromCTToPercentage(String ctValue) throws NumberFormatException {
268         int ct = Integer.parseInt(ctValue);
269         return Math.round((18 - limitColorTemperature(ct)) / 17F * 100F);
270     }
271
272     /**
273      * Convert the color temperature from openHAB (percentage) to myStrom (1-18)
274      *
275      * @param colorTemperature Color temperature from openHab. 0 = coldest, 100 = warmest
276      * @return Color temperature from myStrom. 1 = warmest, 18 = coldest
277      */
278     private String convertPercentageToMyStromCT(PercentType colorTemperature) {
279         int ct = 18 - Math.round(colorTemperature.floatValue() * 17F / 100F);
280         return Integer.toString(limitColorTemperature(ct));
281     }
282
283     private int limitColorTemperature(int colorTemperature) {
284         return Math.max(1, Math.min(colorTemperature, 18));
285     }
286
287     private static class MyStromBulbResponse {
288         public boolean on;
289         public String color = "";
290         public String mode = "";
291         public long ramp;
292
293         @Override
294         public String toString() {
295             return "MyStromBulbResponse{" + "on=" + on + ", color='" + color + '\'' + ", mode='" + mode + '\''
296                     + ", ramp=" + ramp + '}';
297         }
298     }
299
300     private static class MyStromDeviceSpecificInfo extends MyStromBulbResponse {
301         public double power;
302     }
303 }