Added support for publishing ChannelDescriptionChangedEvents (#10900)

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
Christoph Weitkamp 2021-06-23 22:14:49 +02:00 committed by GitHub
parent 089a78ff6f
commit 225e2ae15a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 106 additions and 130 deletions

View File

@ -1,77 +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.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.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.openhab.core.types.CommandDescription;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dynamic channel command description provider.
* Overrides the command description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, CommandDescriptionProvider.class })
public class CommandDescriptionProvider implements DynamicCommandDescriptionProvider {
private final Map<ChannelUID, CommandDescription> descriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(CommandDescriptionProvider.class);
/**
* Set a command description for a channel. This description will be used when preparing the channel command 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, CommandDescription description) {
logger.trace("adding command 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 CommandDescription getCommandDescription(Channel channel,
@Nullable CommandDescription 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;
}
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.deconz.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicCommandDescriptionProvider;
import org.openhab.core.thing.type.DynamicCommandDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Dynamic channel command description provider.
* Overrides the command description for the controls, which receive its configuration in the runtime.
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicCommandDescriptionProvider.class, DeconzDynamicCommandDescriptionProvider.class })
public class DeconzDynamicCommandDescriptionProvider extends BaseDynamicCommandDescriptionProvider {
private final Logger logger = LoggerFactory.getLogger(DeconzDynamicCommandDescriptionProvider.class);
/**
* 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);
channelOptionsMap.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
}
}

View File

@ -14,6 +14,7 @@ package org.openhab.binding.deconz.internal;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -21,8 +22,11 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.events.ThingEventFactory;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragment;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,11 +38,11 @@ import org.slf4j.LoggerFactory;
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
@Component(service = { DynamicStateDescriptionProvider.class, StateDescriptionProvider.class })
public class StateDescriptionProvider implements DynamicStateDescriptionProvider {
@Component(service = { DynamicStateDescriptionProvider.class, DeconzDynamicStateDescriptionProvider.class })
public class DeconzDynamicStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
private final Logger logger = LoggerFactory.getLogger(DeconzDynamicStateDescriptionProvider.class);
private final Map<ChannelUID, StateDescription> descriptions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(StateDescriptionProvider.class);
private final Map<ChannelUID, StateDescriptionFragment> stateDescriptionFragments = new ConcurrentHashMap<>();
/**
* Set a state description for a channel. This description will be used when preparing the channel state by
@ -46,12 +50,18 @@ public class StateDescriptionProvider implements DynamicStateDescriptionProvider
*
* @param channelUID
* channel UID
* @param description
* @param stateDescriptionFragment
* state description for the channel
*/
public void setDescription(ChannelUID channelUID, StateDescription description) {
logger.trace("adding state description for channel {}", channelUID);
descriptions.put(channelUID, description);
public void setDescriptionFragment(ChannelUID channelUID, StateDescriptionFragment stateDescriptionFragment) {
StateDescriptionFragment oldStateDescriptionFragment = stateDescriptionFragments.get(channelUID);
if (!stateDescriptionFragment.equals(oldStateDescriptionFragment)) {
logger.trace("adding state description for channel {}", channelUID);
stateDescriptionFragments.put(channelUID, stateDescriptionFragment);
postEvent(ThingEventFactory.createChannelDescriptionChangedEvent(channelUID,
itemChannelLinkRegistry != null ? itemChannelLinkRegistry.getLinkedItemNames(channelUID) : Set.of(),
stateDescriptionFragment, oldStateDescriptionFragment));
}
}
/**
@ -61,17 +71,18 @@ public class StateDescriptionProvider implements DynamicStateDescriptionProvider
*/
public void removeDescriptionsForThing(ThingUID thingUID) {
logger.trace("removing state description for thing {}", thingUID);
descriptions.entrySet().removeIf(entry -> entry.getKey().getThingUID().equals(thingUID));
stateDescriptionFragments.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())) {
StateDescriptionFragment stateDescriptionFragment = stateDescriptionFragments.get(channel.getUID());
if (stateDescriptionFragment != null) {
logger.trace("returning new stateDescription for {}", channel.getUID());
return descriptions.get(channel.getUID());
return stateDescriptionFragment.toStateDescription();
} else {
return null;
return super.getStateDescription(channel, originalStateDescription, locale);
}
}
}

View File

@ -65,14 +65,14 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory {
private final Gson gson;
private final WebSocketFactory webSocketFactory;
private final HttpClientFactory httpClientFactory;
private final StateDescriptionProvider stateDescriptionProvider;
private final CommandDescriptionProvider commandDescriptionProvider;
private final DeconzDynamicStateDescriptionProvider stateDescriptionProvider;
private final DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
@Activate
public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory,
final @Reference HttpClientFactory httpClientFactory,
final @Reference StateDescriptionProvider stateDescriptionProvider,
final @Reference CommandDescriptionProvider commandDescriptionProvider) {
final @Reference DeconzDynamicStateDescriptionProvider stateDescriptionProvider,
final @Reference DeconzDynamicCommandDescriptionProvider commandDescriptionProvider) {
this.webSocketFactory = webSocketFactory;
this.httpClientFactory = httpClientFactory;
this.stateDescriptionProvider = stateDescriptionProvider;

View File

@ -13,6 +13,7 @@
package org.openhab.binding.deconz.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.types.CommandOption;
/**
* The {@link Scene} is send by the websocket connection as well as the Rest API.
@ -24,4 +25,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class Scene {
public String id = "";
public String name = "";
public CommandOption toCommandOption() {
return new CommandOption(name, name);
}
@Override
public String toString() {
return "Scene{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}';
}
}

View File

@ -19,12 +19,13 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.GroupAction;
import org.openhab.binding.deconz.internal.dto.GroupMessage;
import org.openhab.binding.deconz.internal.dto.GroupState;
import org.openhab.binding.deconz.internal.dto.Scene;
import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
@ -36,8 +37,6 @@ import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandDescriptionBuilder;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -56,12 +55,13 @@ import com.google.gson.Gson;
public class GroupThingHandler extends DeconzBaseThingHandler {
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_LIGHTGROUP);
private final Logger logger = LoggerFactory.getLogger(GroupThingHandler.class);
private final CommandDescriptionProvider commandDescriptionProvider;
private final DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
private Map<String, String> scenes = Map.of();
private GroupState groupStateCache = new GroupState();
public GroupThingHandler(Thing thing, Gson gson, CommandDescriptionProvider commandDescriptionProvider) {
public GroupThingHandler(Thing thing, Gson gson,
DeconzDynamicCommandDescriptionProvider commandDescriptionProvider) {
super(thing, gson, ResourceType.GROUPS);
this.commandDescriptionProvider = commandDescriptionProvider;
}
@ -142,10 +142,8 @@ public class GroupThingHandler extends DeconzBaseThingHandler {
GroupMessage groupMessage = (GroupMessage) stateResponse;
scenes = groupMessage.scenes.stream().collect(Collectors.toMap(scene -> scene.name, scene -> scene.id));
ChannelUID channelUID = new ChannelUID(thing.getUID(), CHANNEL_SCENE);
commandDescriptionProvider.setDescription(channelUID,
CommandDescriptionBuilder.create().withCommandOptions(groupMessage.scenes.stream()
.map(scene -> new CommandOption(scene.name, scene.name)).collect(Collectors.toList()))
.build());
commandDescriptionProvider.setCommandOptions(channelUID,
groupMessage.scenes.stream().map(Scene::toCommandOption).collect(Collectors.toList()));
}
messageReceived(config.id, stateResponse);

View File

@ -24,8 +24,8 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
import org.openhab.binding.deconz.internal.DeconzDynamicStateDescriptionProvider;
import org.openhab.binding.deconz.internal.Util;
import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
import org.openhab.binding.deconz.internal.dto.LightMessage;
@ -49,10 +49,9 @@ import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandDescriptionBuilder;
import org.openhab.core.types.CommandOption;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
@ -85,8 +84,8 @@ public class LightThingHandler extends DeconzBaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
private final StateDescriptionProvider stateDescriptionProvider;
private final CommandDescriptionProvider commandDescriptionProvider;
private final DeconzDynamicStateDescriptionProvider stateDescriptionProvider;
private final DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
private long lastCommandExpireTimestamp = 0;
private boolean needsPropertyUpdate = false;
@ -104,8 +103,8 @@ public class LightThingHandler extends DeconzBaseThingHandler {
private int ctMax = ZCL_CT_MAX;
private int ctMin = ZCL_CT_MIN;
public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider,
CommandDescriptionProvider commandDescriptionProvider) {
public LightThingHandler(Thing thing, Gson gson, DeconzDynamicStateDescriptionProvider stateDescriptionProvider,
DeconzDynamicCommandDescriptionProvider commandDescriptionProvider) {
super(thing, gson, ResourceType.LIGHTS);
this.stateDescriptionProvider = stateDescriptionProvider;
this.commandDescriptionProvider = commandDescriptionProvider;
@ -123,15 +122,11 @@ public class LightThingHandler extends DeconzBaseThingHandler {
ctMin = ctMinString == null ? ZCL_CT_MIN : Integer.parseInt(ctMinString);
// minimum and maximum are inverted due to mired/kelvin conversion!
StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
StateDescriptionFragment stateDescriptionFragment = 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());
}
.withMaximum(new BigDecimal(miredToKelvin(ctMin))).build();
stateDescriptionProvider.setDescriptionFragment(
new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE), stateDescriptionFragment);
} catch (NumberFormatException e) {
needsPropertyUpdate = true;
}
@ -370,20 +365,16 @@ public class LightThingHandler extends DeconzBaseThingHandler {
List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks",
"flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival",
"glow");
commandDescriptionProvider.setDescription(effectChannelUID,
CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options));
break;
case TINT_MUELLER:
options = List.of("none", "colorloop", "sunset", "party", "worklight", "campfire", "romance",
"nightlight");
commandDescriptionProvider.setDescription(effectChannelUID,
CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options));
break;
default:
options = List.of("none", "colorloop");
commandDescriptionProvider.setDescription(effectChannelUID,
CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
commandDescriptionProvider.setCommandOptions(effectChannelUID, toCommandOptionList(options));
}
}

View File

@ -28,8 +28,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
import org.openhab.binding.deconz.internal.StateDescriptionProvider;
import org.openhab.binding.deconz.internal.DeconzDynamicCommandDescriptionProvider;
import org.openhab.binding.deconz.internal.DeconzDynamicStateDescriptionProvider;
import org.openhab.binding.deconz.internal.dto.LightMessage;
import org.openhab.binding.deconz.internal.handler.LightThingHandler;
import org.openhab.binding.deconz.internal.types.LightType;
@ -60,8 +60,8 @@ public class LightsTest {
private @NonNullByDefault({}) Gson gson;
private @Mock @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
private @Mock @NonNullByDefault({}) StateDescriptionProvider stateDescriptionProvider;
private @Mock @NonNullByDefault({}) CommandDescriptionProvider commandDescriptionProvider;
private @Mock @NonNullByDefault({}) DeconzDynamicStateDescriptionProvider stateDescriptionProvider;
private @Mock @NonNullByDefault({}) DeconzDynamicCommandDescriptionProvider commandDescriptionProvider;
@BeforeEach
public void initialize() {
@ -116,7 +116,7 @@ public class LightsTest {
lightThingHandler.initialize();
Mockito.verify(stateDescriptionProvider).setDescription(eq(channelUID_ct), any());
Mockito.verify(stateDescriptionProvider).setDescriptionFragment(eq(channelUID_ct), any());
}
@Test