]> git.basschouten.com Git - openhab-addons.git/commitdiff
[mystrom] Add support for myStrom Bulb (#9910)
authorFredo70 <fchastagnol@yahoo.com>
Tue, 23 Feb 2021 22:33:12 +0000 (23:33 +0100)
committerGitHub <noreply@github.com>
Tue, 23 Feb 2021 22:33:12 +0000 (23:33 +0100)
* Add support to myStrom Bulb

Add properties to myStrom devices and an action to refresh the properties.

Signed-off-by: Frederic Chastagnol <fchastagnol@fredoware.ch>
* Fixes according to review comments

Signed-off-by: Frederic Chastagnol <fchastagnol@fredoware.ch>
* Update bundles/org.openhab.binding.mystrom/README.md

Co-authored-by: J-N-K <J-N-K@users.noreply.github.com>
* Fixes according to review comments

Signed-off-by: Frederic Chastagnol <fchastagnol@fredoware.ch>
* Use system color temperature channel type

channel type system.color-temperature is used and values mapped from 1-18 to 0-100%

Signed-off-by: Frederic Chastagnol <fchastagnol@fredoware.ch>
* Better tracking of colour and brightness values

Format power state
Signed-off-by: Frederic Chastagnol <fchastagnol@fredoware.ch>
Co-authored-by: J-N-K <J-N-K@users.noreply.github.com>
bundles/org.openhab.binding.mystrom/README.md
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/AbstractMyStromHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBindingConstants.java
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromBulbHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromDeviceInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java [deleted file]
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandlerFactory.java
bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.mystrom/src/main/resources/OH-INF/thing/thing-types.xml

index a4de343bb95e6262e0e2002a808708a6de16c90b..a0389947e8f20b8e26fcbfd7d290fc772a6d9160 100644 (file)
@@ -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 (file)
index 0000000..1be559b
--- /dev/null
@@ -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<String, String> 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();
+}
index 99969fc837b9acff7f5972593026f9738178007c..8e73faf290216adc4a7c63b1f1dd1956a1bd219c 100644 (file)
@@ -20,6 +20,7 @@ import org.openhab.core.thing.ThingTypeUID;
  * 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 (file)
index 0000000..2591362
--- /dev/null
@@ -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<HashMap<String, MyStromDeviceSpecificInfo>>() {
+    }.getType();
+
+    private final Logger logger = LoggerFactory.getLogger(MyStromBulbHandler.class);
+
+    private final ExpiringCache<Map<String, MyStromDeviceSpecificInfo>> 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<String, MyStromDeviceSpecificInfo> 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<String, MyStromDeviceSpecificInfo> getReport() {
+        try {
+            String returnContent = sendHttpRequest(HttpMethod.GET, "/api/v1/device", null);
+            Map<String, MyStromDeviceSpecificInfo> 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<String, MyStromDeviceSpecificInfo> 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 <UINT 0..360>;<UINT 0..100>;<UINT 0..100>)
+     * @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 (file)
index 0000000..893b464
--- /dev/null
@@ -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/MyStromHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromHandler.java
deleted file mode 100644 (file)
index 191a45f..0000000
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * 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_POWER;
-import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_SWITCH;
-import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_TEMPERATURE;
-import static org.openhab.core.library.unit.SIUnits.CELSIUS;
-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;
-import org.openhab.core.library.types.QuantityType;
-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.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
- * sent to one of the channels.
- *
- * @author Paul Frank - Initial contribution
- */
-@NonNullByDefault
-public class MyStromHandler extends BaseThingHandler {
-
-    private static class MyStromReport {
-
-        public float power;
-        public boolean relay;
-        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 @Nullable ScheduledFuture<?> pollingJob;
-    private ExpiringCache<MyStromReport> cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport);
-    private final Gson gson = new Gson();
-
-    public MyStromHandler(Thing thing, HttpClient httpClient) {
-        super(thing);
-        this.httpClient = httpClient;
-    }
-
-    @Override
-    public void handleCommand(ChannelUID channelUID, Command command) {
-        try {
-            if (command instanceof RefreshType) {
-                pollDevice();
-            } else {
-                if (command instanceof OnOffType && CHANNEL_SWITCH.equals(channelUID.getId())) {
-                    sendHttpGet("relay?state=" + (command == OnOffType.ON ? "1" : "0"));
-                    scheduler.schedule(this::pollDevice, 500, TimeUnit.MILLISECONDS);
-                }
-            }
-        } catch (MyStromException e) {
-            logger.warn("Error while handling command {}", e.getMessage());
-        }
-    }
-
-    private @Nullable MyStromReport getReport() {
-        try {
-            String returnContent = sendHttpGet("report");
-            MyStromReport report = gson.fromJson(returnContent, MyStromReport.class);
-            updateStatus(ThingStatus.ONLINE);
-            return report;
-        } catch (MyStromException e) {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-            return null;
-        }
-    }
-
-    private void pollDevice() {
-        MyStromReport report = cache.getValue();
-        if (report != null) {
-            updateState(CHANNEL_SWITCH, report.relay ? OnOffType.ON : OnOffType.OFF);
-            updateState(CHANNEL_POWER, QuantityType.valueOf(report.power, WATT));
-            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();
-    }
-}
index d0a2aeb669cabe6c31c3b3542c77048bb40b1ee9..3c47ee92f4637e47ed729a06f4c3b75457f4d140 100644 (file)
@@ -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 @@ import org.osgi.service.component.annotations.Reference;
  * 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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_PLUG);
+    private static final Set<ThingTypeUID> 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 class MyStromHandlerFactory extends BaseThingHandlerFactory {
         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/MyStromPlugHandler.java b/bundles/org.openhab.binding.mystrom/src/main/java/org/openhab/binding/mystrom/internal/MyStromPlugHandler.java
new file mode 100644 (file)
index 0000000..6bdb9e1
--- /dev/null
@@ -0,0 +1,102 @@
+/**
+ * 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_POWER;
+import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_SWITCH;
+import static org.openhab.binding.mystrom.internal.MyStromBindingConstants.CHANNEL_TEMPERATURE;
+import static org.openhab.core.library.unit.SIUnits.CELSIUS;
+import static org.openhab.core.library.unit.Units.WATT;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+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.openhab.core.cache.ExpiringCache;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+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;
+
+/**
+ * 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 MyStromPlugHandler extends AbstractMyStromHandler {
+
+    private static class MyStromReport {
+
+        public float power;
+        public boolean relay;
+        public float temperature;
+    }
+
+    private final Logger logger = LoggerFactory.getLogger(MyStromPlugHandler.class);
+
+    private final ExpiringCache<MyStromReport> cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport);
+
+    public MyStromPlugHandler(Thing thing, HttpClient httpClient) {
+        super(thing, httpClient);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        try {
+            if (command instanceof RefreshType) {
+                pollDevice();
+            } else {
+                if (command instanceof OnOffType && CHANNEL_SWITCH.equals(channelUID.getId())) {
+                    sendHttpRequest(HttpMethod.GET, "/relay?state=" + (command == OnOffType.ON ? "1" : "0"), null);
+                    scheduler.schedule(this::pollDevice, 500, TimeUnit.MILLISECONDS);
+                }
+            }
+        } catch (MyStromException e) {
+            logger.warn("Error while handling command {}", e.getMessage());
+        }
+    }
+
+    private @Nullable MyStromReport getReport() {
+        try {
+            String returnContent = sendHttpRequest(HttpMethod.GET, "/report", null);
+            MyStromReport report = gson.fromJson(returnContent, MyStromReport.class);
+            updateStatus(ThingStatus.ONLINE);
+            return report;
+        } catch (MyStromException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            return null;
+        }
+    }
+
+    @Override
+    protected void pollDevice() {
+        MyStromReport report = cache.getValue();
+        if (report != null) {
+            updateState(CHANNEL_SWITCH, report.relay ? OnOffType.ON : OnOffType.OFF);
+            updateState(CHANNEL_POWER, QuantityType.valueOf(report.power, WATT));
+            updateState(CHANNEL_TEMPERATURE, QuantityType.valueOf(report.temperature, CELSIUS));
+        }
+    }
+}
index 52735564eb0c815b5feec158d5693f91fd13a7aa..f9c9f8bf3c1ced863a2a404468dd176d4004bb71 100644 (file)
                        <channel id="temperature" typeId="temperature-channel"/>
                </channels>
 
+               <properties>
+                       <property name="mac"/>
+                       <property name="version"/>
+                       <property name="type"/>
+                       <property name="ssid"/>
+                       <property name="ip"/>
+                       <property name="mask"/>
+                       <property name="gw"/>
+                       <property name="dns"/>
+                       <property name="static"/>
+                       <property name="connected"/>
+               </properties>
+
+               <representation-property>mac</representation-property>
+
                <config-description>
                        <parameter name="hostname" type="text">
                                <label>Hostname</label>
 
        </thing-type>
 
+       <thing-type id="mystrombulb">
+               <label>myStrom Bulb</label>
+               <description>Controls the myStrom bulb</description>
+
+               <channels>
+                       <channel id="switch" typeId="system.power"/>
+                       <channel id="power" typeId="power-channel"/>
+                       <channel id="color" typeId="system.color"/>
+                       <channel id="colorTemperature" typeId="system.color-temperature"/>
+                       <channel id="brightness" typeId="system.brightness"/>
+                       <channel id="ramp" typeId="ramp-channel"/>
+                       <channel id="mode" typeId="mode-channel"/>
+               </channels>
+
+               <properties>
+                       <property name="mac"/>
+                       <property name="version"/>
+                       <property name="type"/>
+                       <property name="ssid"/>
+                       <property name="ip"/>
+                       <property name="mask"/>
+                       <property name="gw"/>
+                       <property name="dns"/>
+                       <property name="static"/>
+                       <property name="connected"/>
+               </properties>
+
+               <representation-property>mac</representation-property>
+
+               <config-description>
+                       <parameter name="hostname" type="text">
+                               <label>Hostname</label>
+                               <description>The host name or IP address of the myStrom bulb.</description>
+                               <context>network-address</context>
+                               <default>localhost</default>
+                               <required>true</required>
+                       </parameter>
+                       <parameter name="refresh" type="integer" unit="s" min="1">
+                               <label>Refresh Interval</label>
+                               <description>Specifies the refresh interval in seconds.</description>
+                               <default>10</default>
+                               <required>true</required>
+                       </parameter>
+               </config-description>
+
+       </thing-type>
+
+
        <channel-type id="power-channel">
                <item-type>Number:Power</item-type>
                <label>Power Consumption</label>
                <description>The current power delivered by the plug</description>
-               <state readOnly="true"/>
+               <state pattern="%.3f %unit%" readOnly="true"/>
        </channel-type>
 
        <channel-type id="temperature-channel">
                <description>The current temperature at the plug</description>
                <state readOnly="true"/>
        </channel-type>
+
+       <channel-type id="ramp-channel">
+               <item-type>Number:Time</item-type>
+               <label>Ramp</label>
+               <description>Transition time from the light’s current state to the new state.</description>
+               <state pattern="%d %unit%"/>
+       </channel-type>
+
+       <channel-type id="mode-channel">
+               <item-type>String</item-type>
+               <label>Mode</label>
+               <description>The color mode we want the Bulb to set to</description>
+               <command>
+                       <options>
+                               <option value="rgb">RGB</option>
+                               <option value="hsv">HSB (HSV)</option>
+                               <option value="mono">MONO</option>
+                       </options>
+               </command>
+       </channel-type>
+
 </thing:thing-descriptions>