Skip to content

Commit

Permalink
[deconz] Add dynamic state descriptions and fix property updates (ope…
Browse files Browse the repository at this point in the history
…nhab#8055)

* add dynamic state descriptions and fix property updates
* fixes and improvements

Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
  • Loading branch information
J-N-K authored and markus7017 committed Sep 18, 2020
1 parent 5a6b086 commit 2fe2b11
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 32 deletions.
4 changes: 3 additions & 1 deletion bundles/org.openhab.binding.deconz/pom.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
private final Gson gson;
private final WebSocketFactory webSocketFactory;
private final HttpClientFactory httpClientFactory;
private final StateDescriptionProvider stateDescriptionProvider;

@Activate
public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
final @Reference HttpClientFactory httpClientFactory) {
final @Reference HttpClientFactory httpClientFactory,
final @Reference StateDescriptionProvider stateDescriptionProvider) {
this.webSocketFactory = webSocketFactory;
this.httpClientFactory = httpClientFactory;
this.stateDescriptionProvider = stateDescriptionProvider;

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer());
Expand All @@ -85,7 +88,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return new DeconzBridgeHandler((Bridge) thing, webSocketFactory,
new AsyncHttpClient(httpClientFactory.getCommonHttpClient()), gson);
} else if (LightThingHandler.SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID)) {
return new LightThingHandler(thing, gson);
return new LightThingHandler(thing, gson, stateDescriptionProvider);
} else if (SensorThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return new SensorThingHandler(thing, gson);
} else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 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.deconz.internal;

import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider;
import org.eclipse.smarthome.core.types.StateDescription;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Dynamic channel state description provider.
* Overrides the state description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, StateDescriptionProvider.class }, immediate = true)
public class StateDescriptionProvider implements DynamicStateDescriptionProvider {

private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(StateDescriptionProvider.class);

/**
* Set a state description for a channel. This description will be used when preparing the channel state by
* the framework for presentation. A previous description, if existed, will be replaced.
*
* @param channelUID
* channel UID
* @param description
* state description for the channel
*/
public void setDescription(ChannelUID channelUID, StateDescription description) {
logger.trace("adding state description for channel {}", channelUID);
descriptions.put(channelUID, description);
}

/**
* remove all descriptions for a given thing
*
* @param thingUID the thing's UID
*/
public void removeDescriptionsForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID);
descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
}

@Override
public @Nullable StateDescription getStateDescription(Channel channel,
@Nullable StateDescription originalStateDescription, @Nullable Locale locale) {
if (descriptions.containsKey(channel.getUID())) {
logger.trace("returning new stateDescription for {}", channel.getUID());
return descriptions.get(channel.getUID());
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,4 @@ public static int kelvinToMired(int kelvinValue) {
public static int constrainToRange(int intValue, int min, int max) {
return Math.max(min, Math.min(intValue, max));
}

public static int parseIntWithFallback(String text, int defaultValue) {
if (text == null || text.isEmpty()) {
return defaultValue;
}
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerService;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.BridgeFullState;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.SensorMessage;
Expand Down Expand Up @@ -120,11 +121,10 @@ private void addLight(String lightID, LightMessage light) {
properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);

if (light.ctmax != null && light.ctmin != null) {
int ctmax = (light.ctmax > ZCL_CT_MAX) ? ZCL_CT_MAX : light.ctmax;
properties.put(PROPERTY_CT_MAX, Integer.toString(ctmax));

int ctmin = (light.ctmin < ZCL_CT_MIN) ? ZCL_CT_MIN : light.ctmin;
properties.put(PROPERTY_CT_MIN, Integer.toString(ctmin));
properties.put(PROPERTY_CT_MAX,
Integer.toString(Util.constrainToRange(light.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
properties.put(PROPERTY_CT_MIN,
Integer.toString(Util.constrainToRange(light.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
}

switch (lightType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
this.http = bridgeHandler.getHttp();
this.bridgeConfig = bridgeHandler.getBridgeConfig();

updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING);
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE);

// Real-time data
registerListener();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import static org.openhab.binding.deconz.internal.BindingConstants.*;
import static org.openhab.binding.deconz.internal.Util.*;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -33,6 +36,10 @@
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.StateDescription;
import org.eclipse.smarthome.core.types.StateDescriptionFragmentBuilder;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.dto.LightState;
Expand Down Expand Up @@ -70,21 +77,51 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {

private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);

private final StateDescriptionProvider stateDescriptionProvider;

private long lastCommandExpireTimestamp = 0;
private boolean needsPropertyUpdate = false;

/**
* The light state. Contains all possible fields for all supported lights
*/
private LightState lightStateCache = new LightState();
private LightState lastCommand = new LightState();

private final int ct_max;
private final int ct_min;
// set defaults, we can override them later if we receive better values
private int ctMax = ZCL_CT_MAX;
private int ctMin = ZCL_CT_MIN;

public LightThingHandler(Thing thing, Gson gson) {
public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
super(thing, gson);
ct_max = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MAX), ZCL_CT_MAX);
ct_min = parseIntWithFallback(thing.getProperties().get(PROPERTY_CT_MIN), ZCL_CT_MIN);

this.stateDescriptionProvider = stateDescriptionProvider;
}

@Override
public void initialize() {
if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT)
|| thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) {
try {
Map<String, String> properties = thing.getProperties();
ctMax = Integer.parseInt(properties.get(PROPERTY_CT_MAX));
ctMin = Integer.parseInt(properties.get(PROPERTY_CT_MIN));

// minimum and maximum are inverted due to mired/kelvin conversion!
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
.withMinimum(new BigDecimal(miredToKelvin(ctMax)))
.withMaximum(new BigDecimal(miredToKelvin(ctMin))).build().toStateDescription();
if (stateDescription != null) {
stateDescriptionProvider.setDescription(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE),
stateDescription);
} else {
logger.warn("Failed to create state description in thing {}", thing.getUID());
}
} catch (NumberFormatException e) {
needsPropertyUpdate = true;
}
}
super.initialize();
}

@Override
Expand Down Expand Up @@ -174,8 +211,8 @@ public void handleCommand(ChannelUID channelUID, Command command) {
break;
case CHANNEL_COLOR_TEMPERATURE:
if (command instanceof DecimalType) {
int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue,ct_min, ct_max);
int miredValue = kelvinToMired(((DecimalType) command).intValue());
newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);

if (currentOn != null && !currentOn) {
// sending new color temperature is only allowed when light is on
Expand Down Expand Up @@ -240,7 +277,23 @@ public void handleCommand(ChannelUID channelUID, Command command) {
if (r.getResponseCode() == 403) {
return null;
} else if (r.getResponseCode() == 200) {
return gson.fromJson(r.getBody(), LightMessage.class);
LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
if (lightMessage != null && needsPropertyUpdate) {
// if we did not receive an ctmin/ctmax, then we probably don't need it
needsPropertyUpdate = false;

if (lightMessage.ctmin != null && lightMessage.ctmax != null) {
Map<String, String> properties = new HashMap<>(thing.getProperties());
properties.put(PROPERTY_CT_MAX,
Integer.toString(Util.constrainToRange(lightMessage.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
properties.put(PROPERTY_CT_MIN,
Integer.toString(Util.constrainToRange(lightMessage.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));

logger.warn("properties new {}", properties);
updateProperties(properties);
}
}
return lightMessage;
} else {
throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
}
Expand Down Expand Up @@ -291,7 +344,7 @@ private void valueUpdated(String channelId, LightState newState) {
break;
case CHANNEL_COLOR_TEMPERATURE:
Integer ct = newState.ct;
if (ct != null && ct >= ct_min && ct <= ct_max) {
if (ct != null && ct >= ctMin && ct <= ctMax) {
updateState(channelId, new DecimalType(miredToKelvin(ct)));
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,20 @@
*/
package org.openhab.binding.deconz;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.MockitoAnnotations.initMocks;
import static org.openhab.binding.deconz.internal.BindingConstants.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingUID;
Expand All @@ -32,6 +37,7 @@
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
Expand All @@ -54,6 +60,9 @@ public class LightsTest {
@Mock
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;

@Mock
private @NonNullByDefault({}) StateDescriptionProvider stateDescriptionProvider;

@Before
public void initialize() {
initMocks(this);
Expand All @@ -76,14 +85,40 @@ public void colorTemperatureLightUpdateTest() throws IOException {
Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson);
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);

lightThingHandler.messageReceived("", lightMessage);
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_bri), eq(new PercentType("21")));
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID_ct), eq(new DecimalType("2500")));
}

@Test
public void colorTemperatureLightStateDescriptionProviderTest() {
ThingUID thingUID = new ThingUID("deconz", "light");
ChannelUID channelUID_bri = new ChannelUID(thingUID, CHANNEL_BRIGHTNESS);
ChannelUID channelUID_ct = new ChannelUID(thingUID, CHANNEL_COLOR_TEMPERATURE);

Map<String, String> properties = new HashMap<>();
properties.put(PROPERTY_CT_MAX, "500");
properties.put(PROPERTY_CT_MIN, "200");

Thing light = ThingBuilder.create(THING_TYPE_COLOR_TEMPERATURE_LIGHT, thingUID).withProperties(properties)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build())
.withChannel(ChannelBuilder.create(channelUID_ct, "Number").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider) {
// avoid warning when initializing
@Override
public @Nullable Bridge getBridge() {
return null;
}
};

lightThingHandler.initialize();

Mockito.verify(stateDescriptionProvider).setDescription(eq(channelUID_ct), any());
}

@Test
public void dimmableLightUpdateTest() throws IOException {
LightMessage lightMessage = DeconzTest.getObjectFromJson("dimmable.json", LightMessage.class, gson);
Expand All @@ -94,7 +129,7 @@ public void dimmableLightUpdateTest() throws IOException {

Thing light = ThingBuilder.create(THING_TYPE_DIMMABLE_LIGHT, thingUID)
.withChannel(ChannelBuilder.create(channelUID_bri, "Dimmer").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson);
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);

lightThingHandler.messageReceived("", lightMessage);
Expand All @@ -111,7 +146,7 @@ public void windowCoveringUpdateTest() throws IOException {

Thing light = ThingBuilder.create(THING_TYPE_WINDOW_COVERING, thingUID)
.withChannel(ChannelBuilder.create(channelUID_pos, "Rollershutter").build()).build();
LightThingHandler lightThingHandler = new LightThingHandler(light, gson);
LightThingHandler lightThingHandler = new LightThingHandler(light, gson, stateDescriptionProvider);
lightThingHandler.setCallback(thingHandlerCallback);

lightThingHandler.messageReceived("", lightMessage);
Expand Down

0 comments on commit 2fe2b11

Please sign in to comment.