[mystrom] Add support for myStrom Bulb (#9910)

* 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>
This commit is contained in:
Fredo70 2021-02-23 23:33:12 +01:00 committed by GitHub
parent fd1f7ebe75
commit 7050a1478e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 656 additions and 78 deletions

View 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 lights 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

View File

@ -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();
}

View 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";
}

View File

@ -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 lights 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;
}
}

View File

@ -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;
}

View 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;

View File

@ -19,15 +19,11 @@ 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;
@ -36,22 +32,20 @@ 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
* 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 @@ public class MyStromHandler extends BaseThingHandler {
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(MyStromPlugHandler.class);
private final Logger logger = LoggerFactory.getLogger(MyStromHandler.class);
private final ExpiringCache<MyStromReport> cache = new ExpiringCache<>(Duration.ofSeconds(3), this::getReport);
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;
public MyStromPlugHandler(Thing thing, HttpClient httpClient) {
super(thing, httpClient);
}
@Override
@ -85,7 +69,7 @@ public class MyStromHandler extends BaseThingHandler {
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 class MyStromHandler extends BaseThingHandler {
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 class MyStromHandler extends BaseThingHandler {
}
}
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 @@ public class MyStromHandler extends BaseThingHandler {
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();
}
}

View File

@ -14,6 +14,21 @@
<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>
@ -30,11 +45,59 @@
</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">
@ -43,4 +106,25 @@
<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 lights 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>