diff --git a/bundles/org.openhab.binding.mystrom/README.md b/bundles/org.openhab.binding.mystrom/README.md index a4de343bb95e6..a0389947e8f20 100644 --- a/bundles/org.openhab.binding.mystrom/README.md +++ b/bundles/org.openhab.binding.mystrom/README.md @@ -9,6 +9,9 @@ This bundle adds the following thing types: | Thing | ThingTypeID | Description | | ------------------ | ----------- | -------------------------------------------------- | | myStrom Smart Plug | mystromplug | A myStrom smart plug | +| myStrom Bulb | mystrombulb | A myStrom bulb | + +According to the myStrom API documentation all request specific to the myStrom Bulb are also work on the LED strip. ## Discovery @@ -24,13 +27,37 @@ The following parameters are valid for all thing types: | hostname | string | yes | localhost | The IP address or hostname of the myStrom smart plug | | refresh | integer | no | 10 | Poll interval in seconds. Increase this if you encounter connection errors | +## Properties + +In addition to the configuration a myStrom thing has the following properties. +The properties are updated during initialize. +Disabling/enabling the thing can be used to update the properties. + +| Property-Name | Description | +| ------------- | --------------------------------------------------------------------- | +| version | Current firmware version | +| type | The type of the device (i.e. bulb = 102) | +| ssid | SSID of the currently connected network | +| ip | Current ip address | +| mask | Mask of the current network | +| gateway | Gateway of the current network | +| dns | DNS of the current network | +| static | Whether or not the ip address is static | +| connected | Whether or not the device is connected to the internet | +| mac | The mac address of the bridge in upper case letters without delimiter | + ## Channels -| Channel ID | Item Type | Read only | Description | -| ---------------- | -------------------- | --------- | ------------------------------------------------------------- | -| switch | Switch | false | Turn the smart plug on or off | -| power | Number:Power | true | The currently delivered power | -| temperature | Number:Temperature | true | The temperature at the plug | +| Channel ID | Item Type | Read only | Description | Thing types supporting this channel | +| ---------------- | -------------------- | --------- | --------------------------------------------------------------------- |-------------------------------------| +| switch | Switch | false | Turn the device on or off | mystromplug, mystrombulb | +| power | Number:Power | true | The currently delivered power | mystromplug, mystrombulb | +| temperature | Number:Temperature | true | The temperature at the plug | mystromplug | +| color | Color | false | The color we set the bulb to (mode 'hsv') | mystrombulb | +| colorTemperature | Dimmer | false | The color temperature of the bulb in mode 'mono' (percentage) | mystrombulb | +| brightness | Dimmer | false | The brightness of the bulb in mode 'mono' | mystrombulb | +| ramp | Number:Time | false | Transition time from the light’s current state to the new state. [ms] | mystrombulb | +| mode | String | false | The color mode we want the Bulb to set to (rgb, hsv or mono) | mystrombulb | ## Full Example diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java new file mode 100644 index 0000000000000..1be559b58b42b --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mystrom.internal; + +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_CONNECTED; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_DNS; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_GW; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_IP; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_LAST_REFRESH; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_MAC; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_MASK; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_SSID; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_STATIC; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_TYPE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.PROPERTY_VERSION; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; + +import com.google.gson.Gson; + +/** + * The {@link AbstractMyStromHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public abstract class AbstractMyStromHandler extends BaseThingHandler { + protected static final String COMMUNICATION_ERROR = "Error while communicating to the myStrom plug: "; + protected static final String HTTP_REQUEST_URL_PREFIX = "http://"; + + protected final HttpClient httpClient; + protected String hostname = ""; + protected String mac = ""; + + private @Nullable ScheduledFuture pollingJob; + protected final Gson gson = new Gson(); + + public AbstractMyStromHandler(Thing thing, HttpClient httpClient) { + super(thing); + this.httpClient = httpClient; + } + + @Override + public final void initialize() { + MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); + this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname; + + updateStatus(ThingStatus.UNKNOWN); + scheduler.schedule(this::initializeInternal, 0, TimeUnit.SECONDS); + } + + @Override + public final void dispose() { + ScheduledFuture pollingJob = this.pollingJob; + if (pollingJob != null) { + pollingJob.cancel(true); + this.pollingJob = null; + } + super.dispose(); + } + + private void updateProperties() throws MyStromException { + String json = sendHttpRequest(HttpMethod.GET, "/api/v1/info", null); + MyStromDeviceInfo deviceInfo = gson.fromJson(json, MyStromDeviceInfo.class); + if (deviceInfo == null) { + throw new MyStromException("Cannot retrieve device info from myStrom device " + getThing().getUID()); + } + this.mac = deviceInfo.mac; + Map properties = editProperties(); + properties.put(PROPERTY_MAC, deviceInfo.mac); + properties.put(PROPERTY_VERSION, deviceInfo.version); + properties.put(PROPERTY_TYPE, Long.toString(deviceInfo.type)); + properties.put(PROPERTY_SSID, deviceInfo.ssid); + properties.put(PROPERTY_IP, deviceInfo.ip); + properties.put(PROPERTY_MASK, deviceInfo.mask); + properties.put(PROPERTY_GW, deviceInfo.gw); + properties.put(PROPERTY_DNS, deviceInfo.dns); + properties.put(PROPERTY_STATIC, Boolean.toString(deviceInfo.staticState)); + properties.put(PROPERTY_CONNECTED, Boolean.toString(deviceInfo.connected)); + Calendar calendar = Calendar.getInstance(); + DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, Locale.getDefault()); + properties.put(PROPERTY_LAST_REFRESH, formatter.format(calendar.getTime())); + updateProperties(properties); + } + + /** + * Calls the API with the given http method, request path and actual data. + * + * @param method the http method to make the call with + * @param path The path of the API endpoint + * @param requestData the actual raw data to send in the request body, may be {@code null} + * @return String contents of the response for the GET request. + * @throws MyStromException Throws on communication error + */ + protected final String sendHttpRequest(HttpMethod method, String path, @Nullable String requestData) + throws MyStromException { + String url = hostname + path; + try { + Request request = httpClient.newRequest(url).timeout(10, TimeUnit.SECONDS).method(method); + if (requestData != null) { + request = request.content(new StringContentProvider(requestData)).header(HttpHeader.CONTENT_TYPE, + "application/x-www-form-urlencoded"); + } + ContentResponse response = request.send(); + if (response.getStatus() != HttpStatus.OK_200) { + throw new MyStromException("Error sending HTTP " + method + " request to " + url + + ". Got response code: " + response.getStatus()); + } + return response.getContentAsString(); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + throw new MyStromException(COMMUNICATION_ERROR + e.getMessage()); + } + } + + private void initializeInternal() { + try { + updateProperties(); + updateStatus(ThingStatus.ONLINE); + MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); + pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevice, 0, config.refresh, TimeUnit.SECONDS); + } catch (MyStromException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + protected abstract void pollDevice(); +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java index 99969fc837b9a..8e73faf290216 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java @@ -20,6 +20,7 @@ * used across the whole binding. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Add constants for myStrom bulb support */ @NonNullByDefault public class MyStromBindingConstants { @@ -30,9 +31,36 @@ public class MyStromBindingConstants { // List of all Thing Type UIDs public static final ThingTypeUID THING_TYPE_PLUG = new ThingTypeUID(BINDING_ID, "mystromplug"); + public static final ThingTypeUID THING_TYPE_BULB = new ThingTypeUID(BINDING_ID, "mystrombulb"); // List of all Channel ids public static final String CHANNEL_SWITCH = "switch"; public static final String CHANNEL_POWER = "power"; public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_COLOR = "color"; + public static final String CHANNEL_RAMP = "ramp"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_COLOR_TEMPERATURE = "colorTemperature"; + public static final String CHANNEL_BRIGHTNESS = "brightness"; + + // Config + public static final String CONFIG_MAC = "mac"; + + // List of all Properties + public static final String PROPERTY_MAC = "mac"; + public static final String PROPERTY_VERSION = "version"; + public static final String PROPERTY_TYPE = "type"; + public static final String PROPERTY_SSID = "ssid"; + public static final String PROPERTY_IP = "ip"; + public static final String PROPERTY_MASK = "mask"; + public static final String PROPERTY_GW = "gw"; + public static final String PROPERTY_DNS = "dns"; + public static final String PROPERTY_STATIC = "static"; + public static final String PROPERTY_CONNECTED = "connected"; + public static final String PROPERTY_LAST_REFRESH = "lastRefresh"; + + // myStrom Bulb modes + public static final String RGB = "rgb"; + public static final String HSV = "hsv"; + public static final String MONO = "mono"; } diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java new file mode 100644 index 0000000000000..259136291438e --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mystrom.internal; + +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_BRIGHTNESS; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_COLOR_TEMPERATURE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_MODE; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_POWER; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_RAMP; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_SWITCH; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.HSV; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.MONO; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.RGB; +import static org.openhab.core.library.unit.Units.SECOND; +import static org.openhab.core.library.unit.Units.WATT; + +import java.lang.reflect.Type; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.Fields; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.MetricPrefix; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.reflect.TypeToken; + +/** + * The {@link MyStromBulbHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public class MyStromBulbHandler extends AbstractMyStromHandler { + + private static final Type DEVICE_INFO_MAP_TYPE = new TypeToken>() { + }.getType(); + + private final Logger logger = LoggerFactory.getLogger(MyStromBulbHandler.class); + + private final ExpiringCache> cache = new ExpiringCache<>( + Duration.ofSeconds(3), this::getReport); + + private PercentType lastBrightness = PercentType.HUNDRED; + private PercentType lastColorTemperature = new PercentType(50); + private String lastMode = MONO; + private HSBType lastColor = HSBType.WHITE; + + public MyStromBulbHandler(Thing thing, HttpClient httpClient) { + super(thing, httpClient); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + try { + if (command instanceof RefreshType) { + pollDevice(); + } else { + String sResp = null; + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + if (command instanceof OnOffType) { + sResp = sendToBulb(command == OnOffType.ON ? "on" : "off", null, null, null); + } + break; + case CHANNEL_COLOR: + if (command instanceof HSBType) { + if (Objects.equals(((HSBType) command).as(OnOffType.class), OnOffType.OFF)) { + sResp = sendToBulb("off", null, null, null); + } else { + String hsv = command.toString().replaceAll(",", ";"); + sResp = sendToBulb("on", hsv, null, HSV); + } + } + break; + case CHANNEL_BRIGHTNESS: + if (command instanceof PercentType) { + if (Objects.equals(((PercentType) command).as(OnOffType.class), OnOffType.OFF)) { + sResp = sendToBulb("off", null, null, null); + } else { + if (lastMode.equals(MONO)) { + String mono = convertPercentageToMyStromCT(lastColorTemperature) + ";" + + command.toString(); + sResp = sendToBulb("on", mono, null, MONO); + } else { + String hsv = lastColor.getHue().intValue() + ";" + lastColor.getSaturation() + ";" + + command.toString(); + sResp = sendToBulb("on", hsv, null, HSV); + } + } + } + break; + case CHANNEL_COLOR_TEMPERATURE: + if (command instanceof PercentType) { + String mono = convertPercentageToMyStromCT((PercentType) command) + ";" + + lastBrightness.toString(); + sResp = sendToBulb("on", mono, null, MONO); + } + break; + case CHANNEL_RAMP: + if (command instanceof DecimalType) { + sResp = sendToBulb(null, null, command.toString(), null); + } + break; + case CHANNEL_MODE: + if (command instanceof StringType) { + sResp = sendToBulb(null, null, null, command.toString()); + } + break; + default: + } + + if (sResp != null) { + Map report = gson.fromJson(sResp, DEVICE_INFO_MAP_TYPE); + if (report != null) { + report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst() + .ifPresent(info -> updateDevice(info.getValue())); + } + } + } + } catch (MyStromException e) { + logger.warn("Error while handling command {}", e.getMessage()); + } + } + + private @Nullable Map getReport() { + try { + String returnContent = sendHttpRequest(HttpMethod.GET, "/api/v1/device", null); + Map report = gson.fromJson(returnContent, DEVICE_INFO_MAP_TYPE); + updateStatus(ThingStatus.ONLINE); + return report; + } catch (MyStromException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return null; + } + } + + @Override + protected void pollDevice() { + Map report = cache.getValue(); + if (report != null) { + report.entrySet().stream().filter(e -> e.getKey().equals(mac)).findFirst() + .ifPresent(info -> updateDevice(info.getValue())); + } + } + + private void updateDevice(@Nullable MyStromBulbResponse deviceInfo) { + if (deviceInfo != null) { + updateState(CHANNEL_SWITCH, deviceInfo.on ? OnOffType.ON : OnOffType.OFF); + updateState(CHANNEL_RAMP, QuantityType.valueOf(deviceInfo.ramp, MetricPrefix.MILLI(SECOND))); + if (deviceInfo instanceof MyStromDeviceSpecificInfo) { + updateState(CHANNEL_POWER, QuantityType.valueOf(((MyStromDeviceSpecificInfo) deviceInfo).power, WATT)); + } + if (deviceInfo.on) { + try { + lastMode = deviceInfo.mode; + long numSemicolon = deviceInfo.color.chars().filter(c -> c == ';').count(); + if (numSemicolon == 1 && deviceInfo.mode.equals(MONO)) { + String[] xy = deviceInfo.color.split(";"); + lastColorTemperature = new PercentType(convertMyStromCTToPercentage(xy[0])); + lastBrightness = PercentType.valueOf(xy[1]); + lastColor = new HSBType(lastColor.getHue() + ",0," + lastBrightness); + updateState(CHANNEL_COLOR_TEMPERATURE, lastColorTemperature); + } else if (numSemicolon == 2 && deviceInfo.mode.equals(HSV)) { + lastColor = HSBType.valueOf(deviceInfo.color.replaceAll(";", ",")); + lastBrightness = lastColor.getBrightness(); + } else if (!deviceInfo.color.equals("") && deviceInfo.mode.equals(RGB)) { + int r = Integer.parseInt(deviceInfo.color.substring(2, 4), 16); + int g = Integer.parseInt(deviceInfo.color.substring(4, 6), 16); + int b = Integer.parseInt(deviceInfo.color.substring(6, 8), 16); + lastColor = HSBType.fromRGB(r, g, b); + lastBrightness = lastColor.getBrightness(); + } + updateState(CHANNEL_COLOR, lastColor); + updateState(CHANNEL_BRIGHTNESS, lastBrightness); + updateState(CHANNEL_MODE, StringType.valueOf(lastMode)); + } catch (IllegalArgumentException e) { + logger.warn("Error while updating {}", e.getMessage()); + } + } + } + } + + /** + * Given a URL and a set parameters, send a HTTP POST request to the URL location + * created by the URL and parameters. + * + * @param action The action we want to take (on,off or toggle) + * @param color The color we set the bulb to (When using RGBW mode the first two hex numbers are used for the + * white channel! hsv is of form ;;) + * @param ramp Transition time from the light’s current state to the new state. [ms] + * @param mode The color mode we want the Bulb to set to (rgb or hsv or mono) + * @return String contents of the response for the GET request. + * @throws MyStromException Throws on communication error + */ + private String sendToBulb(@Nullable String action, @Nullable String color, @Nullable String ramp, + @Nullable String mode) throws MyStromException { + Fields fields = new Fields(); + if (action != null) { + fields.put("action", action); + } + if (color != null) { + fields.put("color", color); + } + if (ramp != null) { + fields.put("ramp", ramp); + } + if (mode != null) { + fields.put("mode", mode); + } + StringBuilder builder = new StringBuilder(fields.getSize() * 32); + for (Fields.Field field : fields) { + for (String value : field.getValues()) { + if (builder.length() > 0) { + builder.append("&"); + } + builder.append(field.getName()).append("=").append(value); + } + } + return sendHttpRequest(HttpMethod.POST, "/api/v1/device/" + mac, builder.toString()); + } + + /** + * Convert the color temperature from myStrom (1-18) to openHAB (percentage) + * + * @param ctValue Color temperature in myStrom: "1" = warm to "18" = cold. + * @return Color temperature (0-100%). 0% is the coldest setting. + * @throws NumberFormatException if the argument is not an integer + */ + private int convertMyStromCTToPercentage(String ctValue) throws NumberFormatException { + int ct = Integer.parseInt(ctValue); + return Math.round((18 - limitColorTemperature(ct)) / 17F * 100F); + } + + /** + * Convert the color temperature from openHAB (percentage) to myStrom (1-18) + * + * @param colorTemperature Color temperature from openHab. 0 = coldest, 100 = warmest + * @return Color temperature from myStrom. 1 = warmest, 18 = coldest + */ + private String convertPercentageToMyStromCT(PercentType colorTemperature) { + int ct = 18 - Math.round(colorTemperature.floatValue() * 17F / 100F); + return Integer.toString(limitColorTemperature(ct)); + } + + private int limitColorTemperature(int colorTemperature) { + return Math.max(1, Math.min(colorTemperature, 18)); + } + + private static class MyStromBulbResponse { + public boolean on; + public String color = ""; + public String mode = ""; + public long ramp; + + @Override + public String toString() { + return "MyStromBulbResponse{" + "on=" + on + ", color='" + color + '\'' + ", mode='" + mode + '\'' + + ", ramp=" + ramp + '}'; + } + } + + private static class MyStromDeviceSpecificInfo extends MyStromBulbResponse { + public double power; + } +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java new file mode 100644 index 0000000000000..893b464bf6965 --- /dev/null +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mystrom.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link MyStromDeviceInfo} class contains fields mapping thing thing properties + * + * @author Frederic Chastagnol - Initial contribution + */ +@NonNullByDefault +public class MyStromDeviceInfo { + public String version = ""; + public String mac = ""; + public long type; + public String ssid = ""; + public String ip = ""; + public String mask = ""; + public String gw = ""; + public String dns = ""; + @SerializedName("static") + public boolean staticState = false; + public boolean connected = false; +} diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java index d0a2aeb669cab..3c47ee92f4637 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java @@ -12,9 +12,9 @@ */ package org.openhab.binding.mystrom.internal; +import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.THING_TYPE_BULB; import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.THING_TYPE_PLUG; -import java.util.Collections; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -34,14 +34,15 @@ * handlers. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Add support for myStrom bulb */ @NonNullByDefault @Component(configurationPid = "binding.mystrom", service = ThingHandlerFactory.class) public class MyStromHandlerFactory extends BaseThingHandlerFactory { - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLUG); + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_PLUG, THING_TYPE_BULB); - private HttpClientFactory httpClientFactory; + private final HttpClientFactory httpClientFactory; @Activate public MyStromHandlerFactory(@Reference HttpClientFactory httpClientFactory) { @@ -58,7 +59,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_PLUG.equals(thingTypeUID)) { - return new MyStromHandler(thing, httpClientFactory.getCommonHttpClient()); + return new MyStromPlugHandler(thing, httpClientFactory.getCommonHttpClient()); + } else if (THING_TYPE_BULB.equals(thingTypeUID)) { + return new MyStromBulbHandler(thing, httpClientFactory.getCommonHttpClient()); } return null; diff --git a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java similarity index 53% rename from bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java rename to bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java index 191a45fa8cc19..6bdb9e130dade 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java +++ b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java @@ -19,15 +19,11 @@ import static org.openhab.core.library.unit.Units.WATT; import java.time.Duration; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpMethod; import org.openhab.core.cache.ExpiringCache; import org.openhab.core.library.types.OnOffType; @@ -36,22 +32,20 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; - /** - * The {@link MyStromHandler} is responsible for handling commands, which are + * The {@link MyStromPlugHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Paul Frank - Initial contribution + * @author Frederic Chastagnol - Extends from new abstract class */ @NonNullByDefault -public class MyStromHandler extends BaseThingHandler { +public class MyStromPlugHandler extends AbstractMyStromHandler { private static class MyStromReport { @@ -60,22 +54,12 @@ private static class MyStromReport { public float temperature; } - private static final int HTTP_OK_CODE = 200; - private static final String COMMUNICATION_ERROR = "Error while communicating to the myStrom plug: "; - private static final String HTTP_REQUEST_URL_PREFIX = "http://"; - - private final Logger logger = LoggerFactory.getLogger(MyStromHandler.class); - - private HttpClient httpClient; - private String hostname = ""; + private final Logger logger = LoggerFactory.getLogger(MyStromPlugHandler.class); - private @Nullable ScheduledFuture pollingJob; - private ExpiringCache cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport); - private final Gson gson = new Gson(); + private final ExpiringCache cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport); - public MyStromHandler(Thing thing, HttpClient httpClient) { - super(thing); - this.httpClient = httpClient; + public MyStromPlugHandler(Thing thing, HttpClient httpClient) { + super(thing, httpClient); } @Override @@ -85,7 +69,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { pollDevice(); } else { if (command instanceof OnOffType && CHANNEL_SWITCH.equals(channelUID.getId())) { - sendHttpGet("relay?state=" + (command == OnOffType.ON ? "1" : "0")); + sendHttpRequest(HttpMethod.GET, "/relay?state=" + (command == OnOffType.ON ? "1" : "0"), null); scheduler.schedule(this::pollDevice, 500, TimeUnit.MILLISECONDS); } } @@ -96,7 +80,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { private @Nullable MyStromReport getReport() { try { - String returnContent = sendHttpGet("report"); + String returnContent = sendHttpRequest(HttpMethod.GET, "/report", null); MyStromReport report = gson.fromJson(returnContent, MyStromReport.class); updateStatus(ThingStatus.ONLINE); return report; @@ -106,7 +90,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - private void pollDevice() { + @Override + protected void pollDevice() { MyStromReport report = cache.getValue(); if (report != null) { updateState(CHANNEL_SWITCH, report.relay ? OnOffType.ON : OnOffType.OFF); @@ -114,46 +99,4 @@ private void pollDevice() { updateState(CHANNEL_TEMPERATURE, QuantityType.valueOf(report.temperature, CELSIUS)); } } - - @Override - public void initialize() { - MyStromConfiguration config = getConfigAs(MyStromConfiguration.class); - this.hostname = HTTP_REQUEST_URL_PREFIX + config.hostname; - - updateStatus(ThingStatus.UNKNOWN); - pollingJob = scheduler.scheduleWithFixedDelay(this::pollDevice, 0, config.refresh, TimeUnit.SECONDS); - } - - @Override - public void dispose() { - if (pollingJob != null) { - pollingJob.cancel(true); - pollingJob = null; - } - super.dispose(); - } - - /** - * Given a URL and a set parameters, send a HTTP GET request to the URL location - * created by the URL and parameters. - * - * @param url The URL to send a GET request to. - * @return String contents of the response for the GET request. - * @throws Exception - */ - public String sendHttpGet(String action) throws MyStromException { - String url = hostname + "/" + action; - ContentResponse response = null; - try { - response = httpClient.newRequest(url).timeout(10, TimeUnit.SECONDS).method(HttpMethod.GET).send(); - } catch (InterruptedException | TimeoutException | ExecutionException e) { - throw new MyStromException(COMMUNICATION_ERROR + e.getMessage()); - } - - if (response.getStatus() != HTTP_OK_CODE) { - throw new MyStromException( - "Error sending HTTP GET request to " + url + ". Got response code: " + response.getStatus()); - } - return response.getContentAsString(); - } } diff --git a/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml index 52735564eb0c8..f9c9f8bf3c1ce 100644 --- a/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,6 +14,21 @@ + + + + + + + + + + + + + + mac + @@ -30,11 +45,59 @@ + + + Controls the myStrom bulb + + + + + + + + + + + + + + + + + + + + + + + + + mac + + + + + The host name or IP address of the myStrom bulb. + network-address + localhost + true + + + + Specifies the refresh interval in seconds. + 10 + true + + + + + + Number:Power The current power delivered by the plug - + @@ -43,4 +106,25 @@ The current temperature at the plug + + + Number:Time + + Transition time from the light’s current state to the new state. + + + + + String + + The color mode we want the Bulb to set to + + + + + + + + +