[mqtt] Simplify homeassistant thing types, and use AbstractStorageBasedTypeProvider (#16600)

* [mqtt.homeassistant] don't dynamically generate channel types

Signed-off-by: Cody Cutrer <cody@cutrer.us>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Cody Cutrer 2024-08-12 13:23:24 -06:00 committed by Ciprian Pascu
parent 1a427f5f8a
commit 1bfa807bee
46 changed files with 862 additions and 567 deletions

View File

@ -14,22 +14,19 @@ package org.openhab.binding.mqtt.generic;
import java.net.URI;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.internal.MqttThingHandlerFactory;
import org.openhab.core.storage.StorageService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.AbstractStorageBasedTypeProvider;
import org.openhab.core.thing.binding.ThingTypeProvider;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeProvider;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeBuilder;
import org.openhab.core.thing.type.ThingTypeRegistry;
@ -45,89 +42,24 @@ import org.osgi.service.component.annotations.Reference;
* This provider is started on-demand only, as soon as {@link MqttThingHandlerFactory} or an extension requires it.
*
* @author David Graeff - Initial contribution
* @author Cody Cutrer - Use AbstractStorageBasedTypeProvider
*
*/
@NonNullByDefault
@Component(immediate = false, service = { ThingTypeProvider.class, ChannelTypeProvider.class,
ChannelGroupTypeProvider.class, MqttChannelTypeProvider.class })
public class MqttChannelTypeProvider implements ThingTypeProvider, ChannelGroupTypeProvider, ChannelTypeProvider {
private final ThingTypeRegistry typeRegistry;
private final Map<ChannelTypeUID, ChannelType> types = new ConcurrentHashMap<>();
private final Map<ChannelGroupTypeUID, ChannelGroupType> groups = new ConcurrentHashMap<>();
private final Map<ThingTypeUID, ThingType> things = new ConcurrentHashMap<>();
public class MqttChannelTypeProvider extends AbstractStorageBasedTypeProvider {
private final ThingTypeRegistry thingTypeRegistry;
@Activate
public MqttChannelTypeProvider(@Reference ThingTypeRegistry typeRegistry) {
super();
this.typeRegistry = typeRegistry;
}
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return types.values();
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
return types.get(channelTypeUID);
}
@Override
public @Nullable ChannelGroupType getChannelGroupType(ChannelGroupTypeUID channelGroupTypeUID,
@Nullable Locale locale) {
return groups.get(channelGroupTypeUID);
}
@Override
public Collection<ChannelGroupType> getChannelGroupTypes(@Nullable Locale locale) {
return groups.values();
}
@Override
public Collection<ThingType> getThingTypes(@Nullable Locale locale) {
return things.values();
}
public Set<ThingTypeUID> getThingTypeUIDs() {
return things.keySet();
}
@Override
public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) {
return things.get(thingTypeUID);
}
public void removeChannelType(ChannelTypeUID uid) {
types.remove(uid);
}
public void removeChannelGroupType(ChannelGroupTypeUID uid) {
groups.remove(uid);
}
public void setChannelGroupType(ChannelGroupTypeUID uid, ChannelGroupType type) {
groups.put(uid, type);
}
public void setChannelType(ChannelTypeUID uid, ChannelType type) {
types.put(uid, type);
}
public void removeThingType(ThingTypeUID uid) {
things.remove(uid);
}
public void setThingType(ThingTypeUID uid, ThingType type) {
things.put(uid, type);
}
public void setThingTypeIfAbsent(ThingTypeUID uid, ThingType type) {
things.putIfAbsent(uid, type);
public MqttChannelTypeProvider(@Reference ThingTypeRegistry thingTypeRegistry,
@Reference StorageService storageService) {
super(storageService);
this.thingTypeRegistry = thingTypeRegistry;
}
public ThingTypeBuilder derive(ThingTypeUID newTypeId, ThingTypeUID baseTypeId) {
ThingType baseType = typeRegistry.getThingType(baseTypeId);
ThingType baseType = thingTypeRegistry.getThingType(baseTypeId);
ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeId, baseType.getLabel())
.withChannelGroupDefinitions(baseType.getChannelGroupDefinitions())
@ -155,4 +87,25 @@ public class MqttChannelTypeProvider implements ThingTypeProvider, ChannelGroupT
return result;
}
public void updateChannelGroupTypesForPrefix(String prefix, Collection<ChannelGroupType> types) {
Collection<ChannelGroupType> oldCgts = channelGroupTypesForPrefix(prefix);
Set<ChannelGroupTypeUID> oldUids = oldCgts.stream().map(ChannelGroupType::getUID).collect(Collectors.toSet());
Collection<ChannelGroupTypeUID> uids = types.stream().map(ChannelGroupType::getUID).toList();
oldUids.removeAll(uids);
// oldUids now contains only UIDs that no longer exist. so remove them
oldUids.forEach(this::removeChannelGroupType);
types.forEach(this::putChannelGroupType);
}
public void removeChannelGroupTypesForPrefix(String prefix) {
channelGroupTypesForPrefix(prefix).forEach(cgt -> removeChannelGroupType(cgt.getUID()));
}
private Collection<ChannelGroupType> channelGroupTypesForPrefix(String prefix) {
return getChannelGroupTypes(null).stream().filter(cgt -> cgt.getUID().getId().startsWith(prefix + "_"))
.toList();
}
}

View File

@ -12,7 +12,10 @@
*/
package org.openhab.binding.mqtt.generic.tools;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
@ -64,6 +67,24 @@ public class ChildMap<T> {
return map.values().stream();
}
/**
* Streams the objects in this map in the order of the given keys.
*
* Extraneous keys are ignored, and missing keys are included at the end in unspecified order.
*
* @param order The keys in the order they should be streamed
*/
public Stream<T> stream(Collection<String> order) {
// need to make a copy to avoid editing `map` itself
Set<String> missingKeys = new HashSet<>(map.keySet());
missingKeys.removeAll(order);
Stream<T> result = order.stream().map(k -> map.get(k)).filter(Objects::nonNull).map(Objects::requireNonNull);
if (!missingKeys.isEmpty()) {
result = Stream.concat(result, missingKeys.stream().map(k -> map.get(k)).map(Objects::requireNonNull));
}
return result;
}
/**
* Modifies the map in way that it matches the entries of the given childIDs.
*
@ -134,4 +155,8 @@ public class ChildMap<T> {
public void put(String key, T value) {
map.put(key, value);
}
public Set<String> keySet() {
return map.keySet();
}
}

View File

@ -28,6 +28,4 @@ public class MqttBindingConstants {
// List of all Thing Type UIDs
public static final ThingTypeUID HOMEASSISTANT_MQTT_THING = new ThingTypeUID(BINDING_ID, "homeassistant");
public static final String CONFIG_HA_CHANNEL = "channel-type:mqtt:ha-channel";
}

View File

@ -18,6 +18,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
@ -26,12 +27,11 @@ import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
/**
@ -43,10 +43,22 @@ import org.osgi.service.component.annotations.Reference;
@Component(service = ThingHandlerFactory.class)
@NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider {
private @NonNullByDefault({}) MqttChannelTypeProvider typeProvider;
private final MqttChannelTypeProvider typeProvider;
private final MqttChannelStateDescriptionProvider stateDescriptionProvider;
private final ChannelTypeRegistry channelTypeRegistry;
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet());
@Activate
public MqttThingHandlerFactory(final @Reference MqttChannelTypeProvider typeProvider,
final @Reference MqttChannelStateDescriptionProvider stateDescriptionProvider,
final @Reference ChannelTypeRegistry channelTypeRegistry) {
this.typeProvider = typeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || isHomeassistantDynamicType(thingTypeUID);
@ -57,33 +69,13 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements
&& thingTypeUID.getId().startsWith(MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId());
}
@Activate
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
}
@Deactivate
@Override
protected void deactivate(ComponentContext componentContext) {
super.deactivate(componentContext);
}
@Reference
protected void setChannelProvider(MqttChannelTypeProvider provider) {
this.typeProvider = provider;
}
protected void unsetChannelProvider(MqttChannelTypeProvider provider) {
this.typeProvider = null;
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, this, 10000, 2000);
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
this, 10000, 2000);
}
return null;
}

View File

@ -12,7 +12,7 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal;
import java.net.URI;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate;
@ -23,10 +23,8 @@ import org.openhab.binding.mqtt.generic.ChannelConfigBuilder;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateTransformation;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
@ -36,12 +34,12 @@ import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelDefinitionBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.CommandDescription;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescription;
/**
* An {@link AbstractComponent}s derived class consists of one or multiple channels.
@ -62,28 +60,22 @@ import org.openhab.core.types.StateDescriptionFragment;
public class ComponentChannel {
private static final String JINJA = "JINJA";
private final ChannelUID channelUID;
private final ChannelState channelState;
private final Channel channel;
private final ChannelType type;
private final ChannelTypeUID channelTypeUID;
private final @Nullable StateDescription stateDescription;
private final @Nullable CommandDescription commandDescription;
private final ChannelStateUpdateListener channelStateUpdateListener;
private ComponentChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type,
ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) {
private ComponentChannel(ChannelState channelState, Channel channel, @Nullable StateDescription stateDescription,
@Nullable CommandDescription commandDescription, ChannelStateUpdateListener channelStateUpdateListener) {
super();
this.channelUID = channelUID;
this.channelState = channelState;
this.channel = channel;
this.type = type;
this.channelTypeUID = channelTypeUID;
this.stateDescription = stateDescription;
this.commandDescription = commandDescription;
this.channelStateUpdateListener = channelStateUpdateListener;
}
public ChannelUID getChannelUID() {
return channelUID;
}
public Channel getChannel() {
return channel;
}
@ -92,6 +84,14 @@ public class ComponentChannel {
return channelState;
}
public @Nullable StateDescription getStateDescription() {
return stateDescription;
}
public @Nullable CommandDescription getCommandDescription() {
return commandDescription;
}
public CompletableFuture<@Nullable Void> stop() {
return channelState.stop();
}
@ -104,16 +104,9 @@ public class ComponentChannel {
return channelState.start(connection, scheduler, timeout);
}
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.setChannelType(channelTypeUID, type);
}
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.removeChannelType(channelTypeUID);
}
public ChannelDefinition type() {
return new ChannelDefinitionBuilder(channelUID.getId(), channelTypeUID).build();
public ChannelDefinition channelDefinition() {
return new ChannelDefinitionBuilder(channel.getUID().getId(),
Objects.requireNonNull(channel.getChannelTypeUID())).withLabel(channel.getLabel()).build();
}
public void resetState() {
@ -123,6 +116,7 @@ public class ComponentChannel {
public static class Builder {
private final AbstractComponent<?> component;
private final String channelID;
private ChannelTypeUID channelTypeUID;
private final Value valueState;
private final String label;
private final ChannelStateUpdateListener channelStateUpdateListener;
@ -141,10 +135,11 @@ public class ComponentChannel {
private String format = "%s";
public Builder(AbstractComponent<?> component, String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
public Builder(AbstractComponent<?> component, String channelID, ChannelTypeUID channelTypeUID,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
this.component = component;
this.channelID = channelID;
this.channelTypeUID = channelTypeUID;
this.valueState = valueState;
this.label = label;
this.isAdvanced = false;
@ -229,12 +224,8 @@ public class ComponentChannel {
ChannelUID channelUID;
ChannelState channelState;
Channel channel;
ChannelType type;
ChannelTypeUID channelTypeUID;
channelUID = component.buildChannelUID(channelID);
channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID,
channelUID.getGroupId() + "_" + channelID);
channelState = new HomeAssistantChannelState(
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic)
.withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(),
@ -244,29 +235,31 @@ public class ComponentChannel {
if (!component.isEnabledByDefault()) {
isAdvanced = true;
}
ChannelTypeBuilder typeBuilder;
if (this.trigger) {
typeBuilder = ChannelTypeBuilder.trigger(channelTypeUID, label);
} else {
StateDescriptionFragment stateDescription = valueState.createStateDescription(commandTopic == null)
.build();
CommandDescription commandDescription = valueState.createCommandDescription().build();
typeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, channelState.getItemType())
.withStateDescriptionFragment(stateDescription).withCommandDescription(commandDescription);
if (isAdvanced) {
channelTypeUID = new ChannelTypeUID(channelTypeUID.getBindingId(),
channelTypeUID.getId() + "-advanced");
}
ChannelKind kind;
StateDescription stateDescription = null;
CommandDescription commandDescription = null;
if (this.trigger) {
kind = ChannelKind.TRIGGER;
} else {
kind = ChannelKind.STATE;
stateDescription = valueState.createStateDescription(commandTopic == null).build().toStateDescription();
commandDescription = valueState.createCommandDescription().build();
}
type = typeBuilder.withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL))
.isAdvanced(isAdvanced).build();
Configuration configuration = new Configuration();
configuration.put("config", component.getChannelConfigurationJson());
component.getHaID().toConfig(configuration);
channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
.withKind(type.getKind()).withLabel(label).withConfiguration(configuration)
.withKind(kind).withLabel(label).withConfiguration(configuration)
.withAutoUpdatePolicy(autoUpdatePolicy).build();
ComponentChannel result = new ComponentChannel(channelUID, channelState, channel, type, channelTypeUID,
ComponentChannel result = new ComponentChannel(channelState, channel, stateDescription, commandDescription,
channelStateUpdateListener);
TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider();

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2024 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.mqtt.homeassistant.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* The types of HomeAssistant channels components.
*
* @author Cody Cutrer - Initial contribution
*/
@NonNullByDefault
public enum ComponentChannelType {
COLOR("ha-color"),
DIMMER("ha-dimmer"),
IMAGE("ha-image"),
NUMBER("ha-number"),
ROLLERSHUTTER("ha-rollershutter"),
STRING("ha-string"),
SWITCH("ha-switch"),
TRIGGER("ha-trigger");
final ChannelTypeUID channelTypeUID;
ComponentChannelType(String id) {
channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID, id);
}
public ChannelTypeUID getChannelTypeUID() {
return channelTypeUID;
}
}

View File

@ -53,6 +53,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
private final ChannelStateUpdateListener updateListener;
private final AvailabilityTracker tracker;
private final TransformationServiceProvider transformationServiceProvider;
private final boolean newStyleChannels;
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson;
@ -79,13 +80,14 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
TransformationServiceProvider transformationServiceProvider) {
TransformationServiceProvider transformationServiceProvider, boolean newStyleChannels) {
this.thingUID = thingUID;
this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener;
this.gson = gson;
this.tracker = tracker;
this.transformationServiceProvider = transformationServiceProvider;
this.newStyleChannels = newStyleChannels;
}
@Override
@ -101,7 +103,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
if (config.length() > 0) {
try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, transformationServiceProvider);
gson, transformationServiceProvider, newStyleChannels);
component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID);

View File

@ -187,9 +187,19 @@ public class HaID {
*
* @return group id
*/
public String getGroupId(@Nullable final String uniqueId) {
public String getGroupId(@Nullable final String uniqueId, boolean newStyleChannels) {
String result = uniqueId;
// newStyleChannels are auto-discovered things with openHAB >= 4.3.0
// assuming the topic has both a node ID and an object ID, simply use
// the component type and object ID - without encoding(!)
// since the only character allowed in object IDs but not allowed in UID
// is `-`. It also doesn't need to be reversible, so it's okay to just
// collapse `-` to `_`.
if (!nodeID.isBlank() && newStyleChannels) {
return component + "_" + objectID.replace('-', '_');
}
// the null test is only here so the compile knows, result is not null afterwards
if (result == null || result.isBlank()) {
StringBuilder str = new StringBuilder();

View File

@ -19,18 +19,18 @@ import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
@ -38,6 +38,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Availability;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AvailabilityMode;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.type.ChannelDefinition;
@ -45,6 +46,8 @@ import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.types.CommandDescription;
import org.openhab.core.types.StateDescription;
import com.google.gson.Gson;
@ -61,7 +64,6 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
// Component location fields
protected final ComponentConfiguration componentConfiguration;
protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
protected final @Nullable ChannelGroupUID channelGroupUID;
protected final HaID haID;
@ -76,15 +78,28 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
protected final C channelConfiguration;
protected boolean configSeen;
protected final boolean singleChannelComponent;
protected final String groupId;
protected final String uniqueId;
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
boolean newStyleChannels) {
this(componentConfiguration, clazz, newStyleChannels, false);
}
/**
* Creates component based on generic configuration and component configuration type.
*
* @param componentConfiguration generic componentConfiguration with not parsed JSON config
* @param clazz target configuration type
* @param newStyleChannels if new style channels should be used
* @param singleChannelComponent if this component only ever has one channel, so should never be in a group
* (only if newStyleChannels is true)
*/
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz) {
public AbstractComponent(ComponentFactory.ComponentConfiguration componentConfiguration, Class<C> clazz,
boolean newStyleChannels, boolean singleChannelComponent) {
this.componentConfiguration = componentConfiguration;
this.singleChannelComponent = newStyleChannels && singleChannelComponent;
this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz);
@ -94,14 +109,15 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
String name = channelConfiguration.getName();
if (name != null && !name.isEmpty()) {
String groupId = this.haID.getGroupId(channelConfiguration.getUniqueId());
groupId = this.haID.getGroupId(channelConfiguration.getUniqueId(), newStyleChannels);
this.channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, groupId);
this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
this.channelGroupUID = this.singleChannelComponent ? null
: new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
} else {
this.channelGroupTypeUID = null;
this.groupId = this.singleChannelComponent ? haID.component : "";
this.channelGroupUID = null;
}
uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
this.configSeen = false;
@ -138,9 +154,13 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
}
}
protected ComponentChannel.Builder buildChannel(String channelID, Value valueState, String label,
ChannelStateUpdateListener channelStateUpdateListener) {
return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener);
protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
if (singleChannelComponent) {
channelID = groupId;
}
return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
channelStateUpdateListener);
}
public void setConfigSeen() {
@ -177,30 +197,21 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
}
/**
* Add all channel types to the channel type provider.
* Add all state and command descriptions to the state description provider.
*
* @param channelTypeProvider The channel type provider
* @param stateDescriptionProvider The state description provider
*/
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
if (groupTypeUID != null) {
channelTypeProvider.setChannelGroupType(groupTypeUID, Objects.requireNonNull(getType()));
}
channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider));
}
/**
* Removes all channels from the channel type provider.
* Call this if the corresponding Thing handler gets disposed.
*
* @param channelTypeProvider The channel type provider
*/
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channels.values().forEach(v -> v.removeChannelTypes(channelTypeProvider));
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
if (groupTypeUID != null) {
channelTypeProvider.removeChannelGroupType(groupTypeUID);
}
public void addStateDescriptions(MqttChannelStateDescriptionProvider stateDescriptionProvider) {
channels.values().forEach(channel -> {
StateDescription stateDescription = channel.getStateDescription();
if (stateDescription != null) {
stateDescriptionProvider.setDescription(channel.getChannel().getUID(), stateDescription);
}
CommandDescription commandDescription = channel.getCommandDescription();
if (commandDescription != null) {
stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
}
});
}
public ChannelUID buildChannelUID(String channelID) {
@ -211,18 +222,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
return new ChannelUID(componentConfiguration.getThingUID(), channelID);
}
/**
* Each HomeAssistant component corresponds to a Channel Group Type.
*/
public @Nullable ChannelGroupTypeUID getGroupTypeUID() {
return channelGroupTypeUID;
}
/**
* The unique id of this component.
*/
public @Nullable ChannelGroupUID getGroupUID() {
return channelGroupUID;
public String getGroupId() {
return groupId;
}
/**
@ -270,19 +271,27 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
/**
* Return the channel group type.
*/
public @Nullable ChannelGroupType getType() {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
if (groupTypeUID == null) {
public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
if (channelGroupUID == null) {
return null;
}
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type)
.collect(Collectors.toList());
return ChannelGroupTypeBuilder.instance(groupTypeUID, getName()).withChannelDefinitions(channelDefinitions)
.build();
return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
.withChannelDefinitions(getAllChannelDefinitions()).build();
}
public List<ChannelDefinition> getChannels() {
return channels.values().stream().map(ComponentChannel::type).collect(Collectors.toList());
public List<ChannelDefinition> getChannelDefinitions() {
if (channelGroupUID != null) {
return List.of();
}
return getAllChannelDefinitions();
}
private List<ChannelDefinition> getAllChannelDefinitions() {
return channels.values().stream().map(ComponentChannel::channelDefinition).toList();
}
public List<Channel> getChannels() {
return channels.values().stream().map(ComponentChannel::getChannel).toList();
}
/**
@ -296,12 +305,15 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
/**
* Return the channel group definition for this component.
*/
public @Nullable ChannelGroupDefinition getGroupDefinition() {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID;
if (groupTypeUID == null) {
public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
if (channelGroupUID == null) {
return null;
}
return new ChannelGroupDefinition(channelGroupUID.getId(), groupTypeUID, getName(), null);
return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), getName(), null);
}
public boolean hasGroup() {
return channelGroupUID != null;
}
public HaID getHaID() {
@ -324,4 +336,12 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
public Gson getGson() {
return componentConfiguration.getGson();
}
public C getChannelConfiguration() {
return channelConfiguration;
}
private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + uniqueId);
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
@ -31,11 +32,12 @@ abstract class AbstractRawSchemaLight extends Light {
protected ComponentChannel rawChannel;
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder) {
super(builder);
hiddenChannels.add(rawChannel = buildChannel(RAW_CHANNEL_ID, new TextValue(), "Raw state", this)
.stateTopic(channelConfiguration.stateTopic).commandTopic(channelConfiguration.commandTopic,
channelConfiguration.isRetain(), channelConfiguration.getQos())
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, newStyleChannels);
hiddenChannels.add(rawChannel = buildChannel(RAW_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(),
"Raw state", this).stateTopic(channelConfiguration.stateTopic)
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import com.google.gson.annotations.SerializedName;
@ -68,28 +69,30 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
protected String payloadArmAway = "ARM_AWAY";
}
public AlarmControlPanel(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public AlarmControlPanel(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
final String[] stateEnum = { channelConfiguration.stateDisarmed, channelConfiguration.stateArmedHome,
channelConfiguration.stateArmedAway, channelConfiguration.statePending,
channelConfiguration.stateTriggered };
buildChannel(STATE_CHANNEL_ID, new TextValue(stateEnum), getName(), componentConfiguration.getUpdateListener())
buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(stateEnum), getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.build();
String commandTopic = channelConfiguration.commandTopic;
if (commandTopic != null) {
buildChannel(SWITCH_DISARM_CHANNEL_ID, new TextValue(new String[] { channelConfiguration.payloadDisarm }),
getName(), componentConfiguration.getUpdateListener())
buildChannel(SWITCH_DISARM_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(new String[] { channelConfiguration.payloadDisarm }), getName(),
componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build();
buildChannel(SWITCH_ARM_HOME_CHANNEL_ID,
buildChannel(SWITCH_ARM_HOME_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(new String[] { channelConfiguration.payloadArmHome }), getName(),
componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build();
buildChannel(SWITCH_ARM_AWAY_CHANNEL_ID,
buildChannel(SWITCH_ARM_AWAY_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(new String[] { channelConfiguration.payloadArmAway }), getName(),
componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build();

View File

@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateStateListener;
@ -67,12 +68,13 @@ public class BinarySensor extends AbstractComponent<BinarySensor.ChannelConfigur
protected @Nullable List<String> jsonAttributes;
}
public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
buildChannel(SENSOR_CHANNEL_ID, value, "value", getListener(componentConfiguration, value))
buildChannel(SENSOR_CHANNEL_ID, ComponentChannelType.SWITCH, value, getName(),
getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.type.AutoUpdatePolicy;
@ -46,12 +47,13 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
protected String payloadPress = "PRESS";
}
public Button(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Button(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadPress });
buildChannel(BUTTON_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(BUTTON_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
componentConfiguration.getUpdateListener())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();

View File

@ -14,6 +14,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.ImageValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/**
@ -38,12 +39,12 @@ public class Camera extends AbstractComponent<Camera.ChannelConfiguration> {
protected String topic = "";
}
public Camera(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Camera(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
ImageValue value = new ImageValue();
buildChannel(CAMERA_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic).build();
buildChannel(CAMERA_CHANNEL_ID, ComponentChannelType.IMAGE, value, getName(),
componentConfiguration.getUpdateListener()).stateTopic(channelConfiguration.topic).build();
}
}

View File

@ -28,6 +28,7 @@ import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
@ -210,83 +211,88 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
protected Boolean sendIfOff = true;
}
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
: channelConfiguration.temperatureUnit.getDefaultPrecision();
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID,
ComponentChannel actionChannel = buildOptionalChannel(ACTION_CH_ID, ComponentChannelType.STRING,
new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
: getCommandFilter(actionChannel);
buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.auxCommandTopic,
channelConfiguration.auxStateTemplate, channelConfiguration.auxStateTopic, commandFilter);
buildOptionalChannel(AUX_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.auxCommandTopic, channelConfiguration.auxStateTemplate,
channelConfiguration.auxStateTopic, commandFilter);
buildOptionalChannel(AWAY_MODE_CH_ID, new OnOffValue(), updateListener, null,
buildOptionalChannel(AWAY_MODE_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
channelConfiguration.awayModeStateTopic, commandFilter);
buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID,
buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
null, null, channelConfiguration.currentTemperatureTemplate,
channelConfiguration.currentTemperatureTopic, commandFilter);
buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fanModes.toArray(new String[0])),
updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
buildOptionalChannel(FAN_MODE_CH_ID, ComponentChannelType.STRING,
new TextValue(channelConfiguration.fanModes.toArray(new String[0])), updateListener,
channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
List<String> holdModes = channelConfiguration.holdModes;
if (holdModes != null && !holdModes.isEmpty()) {
buildOptionalChannel(HOLD_CH_ID, new TextValue(holdModes.toArray(new String[0])), updateListener,
buildOptionalChannel(HOLD_CH_ID, ComponentChannelType.STRING,
new TextValue(holdModes.toArray(new String[0])), updateListener,
channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic,
channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
}
buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])),
updateListener, channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
buildOptionalChannel(MODE_CH_ID, ComponentChannelType.STRING,
new TextValue(channelConfiguration.modes.toArray(new String[0])), updateListener,
channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swingModes.toArray(new String[0])),
updateListener, channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
buildOptionalChannel(SWING_CH_ID, ComponentChannelType.STRING,
new TextValue(channelConfiguration.swingModes.toArray(new String[0])), updateListener,
channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_CH_ID,
buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureCommandTemplate,
channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
channelConfiguration.temperatureStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_HIGH_CH_ID,
buildOptionalChannel(TEMPERATURE_HIGH_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureHighCommandTemplate,
channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
channelConfiguration.temperatureHighStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_LOW_CH_ID,
buildOptionalChannel(TEMPERATURE_LOW_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureLowCommandTemplate,
channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
channelConfiguration.temperatureLowStateTopic, commandFilter);
buildOptionalChannel(POWER_CH_ID, new OnOffValue(), updateListener, null,
buildOptionalChannel(POWER_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.powerCommandTopic, null, null, null);
}
@Nullable
private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
@Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
@Nullable Predicate<Command> commandFilter) {
if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
return buildChannel(channelId, valueState, getName(), channelStateUpdateListener)
return buildChannel(channelId, channelType, valueState, getName(), channelStateUpdateListener)
.stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate)

View File

@ -48,45 +48,46 @@ public class ComponentFactory {
*/
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler,
Gson gson, TransformationServiceProvider transformationServiceProvider) throws ConfigurationException {
Gson gson, TransformationServiceProvider transformationServiceProvider, boolean newStyleChannels)
throws ConfigurationException {
ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, updateListener, tracker, scheduler)
.transformationProvider(transformationServiceProvider);
switch (haID.component) {
case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration);
return new AlarmControlPanel(componentConfiguration, newStyleChannels);
case "binary_sensor":
return new BinarySensor(componentConfiguration);
return new BinarySensor(componentConfiguration, newStyleChannels);
case "button":
return new Button(componentConfiguration);
return new Button(componentConfiguration, newStyleChannels);
case "camera":
return new Camera(componentConfiguration);
return new Camera(componentConfiguration, newStyleChannels);
case "cover":
return new Cover(componentConfiguration);
return new Cover(componentConfiguration, newStyleChannels);
case "fan":
return new Fan(componentConfiguration);
return new Fan(componentConfiguration, newStyleChannels);
case "climate":
return new Climate(componentConfiguration);
return new Climate(componentConfiguration, newStyleChannels);
case "device_automation":
return new DeviceTrigger(componentConfiguration);
return new DeviceTrigger(componentConfiguration, newStyleChannels);
case "light":
return Light.create(componentConfiguration);
return Light.create(componentConfiguration, newStyleChannels);
case "lock":
return new Lock(componentConfiguration);
return new Lock(componentConfiguration, newStyleChannels);
case "number":
return new Number(componentConfiguration);
return new Number(componentConfiguration, newStyleChannels);
case "scene":
return new Scene(componentConfiguration);
return new Scene(componentConfiguration, newStyleChannels);
case "select":
return new Select(componentConfiguration);
return new Select(componentConfiguration, newStyleChannels);
case "sensor":
return new Sensor(componentConfiguration);
return new Sensor(componentConfiguration, newStyleChannels);
case "switch":
return new Switch(componentConfiguration);
return new Switch(componentConfiguration, newStyleChannels);
case "update":
return new Update(componentConfiguration);
return new Update(componentConfiguration, newStyleChannels);
case "vacuum":
return new Vacuum(componentConfiguration);
return new Vacuum(componentConfiguration, newStyleChannels);
default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!");
}

View File

@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.RollershutterValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType;
@ -84,8 +85,8 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
@Nullable
ComponentChannel stateChannel = null;
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
String stateTopic = channelConfiguration.stateTopic;
@ -95,13 +96,13 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
TextValue value = new TextValue(new String[] { channelConfiguration.stateClosed,
channelConfiguration.stateClosing, channelConfiguration.stateOpen,
channelConfiguration.stateOpening, channelConfiguration.stateStopped });
buildChannel(STATE_CHANNEL_ID, value, "State", componentConfiguration.getUpdateListener())
.stateTopic(stateTopic).isAdvanced(true).build();
buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, value, "State",
componentConfiguration.getUpdateListener()).stateTopic(stateTopic).isAdvanced(true).build();
}
if (channelConfiguration.commandTopic != null) {
hiddenChannels.add(stateChannel = buildChannel(STATE_CHANNEL_ID, new TextValue(), "State",
componentConfiguration.getUpdateListener())
hiddenChannels.add(stateChannel = buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(), "State", componentConfiguration.getUpdateListener())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
@ -132,8 +133,8 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
channelConfiguration.payloadClose, channelConfiguration.payloadStop, channelConfiguration.stateOpen,
channelConfiguration.stateClosed, inverted, channelConfiguration.setPositionTopic == null);
buildChannel(COVER_CHANNEL_ID, value, "Cover", componentConfiguration.getUpdateListener())
.stateTopic(rollershutterStateTopic, stateTemplate)
buildChannel(COVER_CHANNEL_ID, ComponentChannelType.ROLLERSHUTTER, value, "Cover",
componentConfiguration.getUpdateListener()).stateTopic(rollershutterStateTopic, stateTemplate)
.commandTopic(rollershutterCommandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(command -> {
if (stateChannel == null) {

View File

@ -22,6 +22,7 @@ import org.openhab.binding.mqtt.generic.mapping.ColorMode;
import org.openhab.binding.mqtt.generic.values.ColorValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
@ -53,14 +54,15 @@ public class DefaultSchemaLight extends Light {
protected @Nullable ComponentChannel rgbChannel;
protected @Nullable ComponentChannel xyChannel;
public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) {
super(builder);
public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, newStyleChannels);
}
@Override
protected void buildChannels() {
ComponentChannel localOnOffChannel;
localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue,
"On/Off State", this)
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -69,8 +71,8 @@ public class DefaultSchemaLight extends Light {
@Nullable
ComponentChannel localBrightnessChannel = null;
if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) {
localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue,
"Brightness", this)
localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID,
ComponentChannelType.DIMMER, brightnessValue, "Brightness", this)
.stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate)
.commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -78,20 +80,22 @@ public class DefaultSchemaLight extends Light {
}
if (channelConfiguration.whiteCommandTopic != null) {
buildChannel(WHITE_CHANNEL_ID, brightnessValue, "Go directly to white of a specific brightness", this)
buildChannel(WHITE_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
"Go directly to white of a specific brightness", this)
.commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.isAdvanced(true).build();
}
if (channelConfiguration.colorModeStateTopic != null) {
buildChannel(COLOR_MODE_CHANNEL_ID, new TextValue(), "Current color mode", this)
buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "Current color mode",
this)
.stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
.build();
}
if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) {
buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this)
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", this)
.stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
.commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -100,7 +104,8 @@ public class DefaultSchemaLight extends Light {
if (effectValue != null
&& (channelConfiguration.effectStateTopic != null || channelConfiguration.effectCommandTopic != null)) {
buildChannel(EFFECT_CHANNEL_ID, Objects.requireNonNull(effectValue), "Lighting Effect", this)
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
"Lighting Effect", this)
.stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
.commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -109,8 +114,8 @@ public class DefaultSchemaLight extends Light {
if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, new ColorValue(ColorMode.RGB, null, null, 100),
"RGB state", this)
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, ComponentChannelType.COLOR,
new ColorValue(ColorMode.RGB, null, null, 100), "RGB state", this)
.stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
.commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -119,35 +124,38 @@ public class DefaultSchemaLight extends Light {
if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(buildChannel(RGBW_CHANNEL_ID, new TextValue(), "RGBW state", this)
.stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate)
.commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.rgbwwStateTopic != null || channelConfiguration.rgbwwCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(buildChannel(RGBWW_CHANNEL_ID, new TextValue(), "RGBWW state", this)
.stateTopic(channelConfiguration.rgbwwStateTopic, channelConfiguration.rgbwwValueTemplate)
.commandTopic(channelConfiguration.rgbwwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(
xyChannel = buildChannel(XY_CHANNEL_ID, new ColorValue(ColorMode.XYY, null, null, 100), "XY State",
this).stateTopic(channelConfiguration.xyStateTopic, channelConfiguration.xyValueTemplate)
.commandTopic(channelConfiguration.xyCommandTopic, channelConfiguration.isRetain(),
hiddenChannels
.add(buildChannel(RGBW_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "RGBW state", this)
.stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate)
.commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.rgbwwStateTopic != null || channelConfiguration.rgbwwCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(
buildChannel(RGBWW_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "RGBWW state", this)
.stateTopic(channelConfiguration.rgbwwStateTopic, channelConfiguration.rgbwwValueTemplate)
.commandTopic(channelConfiguration.rgbwwCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(xyChannel = buildChannel(XY_CHANNEL_ID, ComponentChannelType.COLOR,
new ColorValue(ColorMode.XYY, null, null, 100), "XY State", this)
.stateTopic(channelConfiguration.xyStateTopic, channelConfiguration.xyValueTemplate)
.commandTopic(channelConfiguration.xyCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false));
}
if (channelConfiguration.hsStateTopic != null || channelConfiguration.hsCommandTopic != null) {
hasColorChannel = true;
hiddenChannels.add(this.hsChannel = buildChannel(HS_CHANNEL_ID, new TextValue(), "Hue and Saturation", this)
hiddenChannels.add(this.hsChannel = buildChannel(HS_CHANNEL_ID, ComponentChannelType.STRING,
new TextValue(), "Hue and Saturation", this)
.stateTopic(channelConfiguration.hsStateTopic, channelConfiguration.hsValueTemplate)
.commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -159,7 +167,7 @@ public class DefaultSchemaLight extends Light {
if (localBrightnessChannel != null) {
hiddenChannels.add(localBrightnessChannel);
}
buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this)
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(this::handleColorCommand).build();
} else if (localBrightnessChannel != null) {

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -44,8 +45,8 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
protected @Nullable String payload;
}
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
if (!"trigger".equals(channelConfiguration.automationType)) {
throw new ConfigurationException("Component:DeviceTrigger must have automation_type 'trigger'");
@ -65,7 +66,8 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
value = new TextValue();
}
buildChannel(channelConfiguration.type, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(channelConfiguration.type, ComponentChannelType.TRIGGER, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.topic, channelConfiguration.getValueTemplate()).trigger(true).build();
}
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import com.google.gson.annotations.SerializedName;
@ -50,11 +51,12 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
protected String payloadOff = "OFF";
}
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff);
buildChannel(SWITCH_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)

View File

@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
@ -73,8 +74,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
TextValue colorModeValue;
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder) {
super(builder);
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, newStyleChannels);
colorModeValue = new TextValue();
}
@ -84,7 +85,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
if (supportedColorModes != null && supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
colorModeValue = new TextValue(
supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
buildChannel(COLOR_MODE_CHANNEL_ID, colorModeValue, "Color Mode", this).isAdvanced(true).build();
buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, colorModeValue, "Color Mode", this)
.isAdvanced(true).build();
}
if (channelConfiguration.colorMode) {
@ -98,26 +100,27 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
}
if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
.build();
buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
this).commandTopic(DUMMY_TOPIC, true, 1)
.commandFilter(command -> handleColorTempCommand(command)).build();
}
}
if (hasColorChannel) {
buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this).commandTopic(DUMMY_TOPIC, true, 1)
.commandFilter(this::handleCommand).build();
buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} else if (channelConfiguration.brightness) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, "Brightness", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
"Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} else {
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
}
if (effectValue != null) {
buildChannel(EFFECT_CHANNEL_ID, Objects.requireNonNull(effectValue), "Lighting Effect", this)
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command)).build();
buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
"Lighting Effect", this).commandTopic(DUMMY_TOPIC, true, 1)
.commandFilter(command -> handleEffectCommand(command)).build();
}
}

View File

@ -245,21 +245,22 @@ public abstract class Light extends AbstractComponent<Light.ChannelConfiguration
protected final ChannelStateUpdateListener channelStateUpdateListener;
public static Light create(ComponentFactory.ComponentConfiguration builder) throws UnsupportedComponentException {
public static Light create(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels)
throws UnsupportedComponentException {
String schema = builder.getConfig(ChannelConfiguration.class).schema;
switch (schema) {
case DEFAULT_SCHEMA:
return new DefaultSchemaLight(builder);
return new DefaultSchemaLight(builder, newStyleChannels);
case JSON_SCHEMA:
return new JSONSchemaLight(builder);
return new JSONSchemaLight(builder, newStyleChannels);
default:
throw new UnsupportedComponentException(
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
}
}
protected Light(ComponentFactory.ComponentConfiguration builder) {
super(builder, ChannelConfiguration.class);
protected Light(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, ChannelConfiguration.class, newStyleChannels);
this.channelStateUpdateListener = builder.getUpdateListener();
@Nullable

View File

@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
@ -72,8 +73,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
private OnOffValue lockValue;
private TextValue stateValue;
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
this.optimistic = channelConfiguration.optimistic || channelConfiguration.stateTopic.isBlank();
@ -82,7 +83,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
channelConfiguration.stateUnlocking, channelConfiguration.stateJammed },
channelConfiguration.payloadLock, channelConfiguration.payloadUnlock);
buildChannel(LOCK_CHANNEL_ID, lockValue, "Lock", componentConfiguration.getUpdateListener())
buildChannel(LOCK_CHANNEL_ID, ComponentChannelType.SWITCH, lockValue, "Lock",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
@ -103,7 +105,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
stateValue = new TextValue(new String[] { channelConfiguration.stateJammed, channelConfiguration.stateLocked,
channelConfiguration.stateLocking, channelConfiguration.stateUnlocked,
channelConfiguration.stateUnlocking }, commands);
buildChannel(STATE_CHANNEL_ID, stateValue, "State", componentConfiguration.getUpdateListener())
buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, stateValue, "State",
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())

View File

@ -17,6 +17,7 @@ import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.types.util.UnitUtils;
@ -69,8 +70,8 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate;
}
public Number(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank();
@ -82,7 +83,8 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max,
channelConfiguration.step, UnitUtils.parseUnit(channelConfiguration.unitOfMeasurement));
buildChannel(NUMBER_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(NUMBER_CHANNEL_ID, ComponentChannelType.NUMBER, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.type.AutoUpdatePolicy;
@ -44,12 +45,13 @@ public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
protected String payloadOn = "ON";
}
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadOn });
buildChannel(SCENE_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(SCENE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
componentConfiguration.getUpdateListener())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build();

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -54,8 +55,8 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate;
}
public Select(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank();
@ -66,7 +67,8 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
TextValue value = new TextValue(channelConfiguration.options);
buildChannel(SELECT_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener())
buildChannel(SELECT_CHANNEL_ID, ComponentChannelType.STRING, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate)

View File

@ -21,6 +21,7 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
import org.openhab.core.types.util.UnitUtils;
@ -67,28 +68,32 @@ public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
protected @Nullable List<String> jsonAttributes;
}
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
Value value;
String uom = channelConfiguration.unitOfMeasurement;
String sc = channelConfiguration.stateClass;
ComponentChannelType type;
if (uom != null && !uom.isBlank()) {
value = new NumberValue(null, null, null, UnitUtils.parseUnit(uom));
type = ComponentChannelType.NUMBER;
} else if (sc != null && !sc.isBlank()) {
// see state_class at https://developers.home-assistant.io/docs/core/entity/sensor#properties
// > If not None, the sensor is assumed to be numerical
value = new NumberValue(null, null, null, null);
type = ComponentChannelType.NUMBER;
} else {
value = new TextValue();
type = ComponentChannelType.STRING;
}
String icon = channelConfiguration.getIcon();
boolean trigger = TRIGGER_ICONS.matcher(icon).matches();
buildChannel(SENSOR_CHANNEL_ID, value, getName(), getListener(componentConfiguration, value))
buildChannel(SENSOR_CHANNEL_ID, type, value, getName(), getListener(componentConfiguration, value))
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build();
}

View File

@ -15,6 +15,7 @@ package org.openhab.binding.mqtt.homeassistant.internal.component;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -59,8 +60,8 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate;
}
public Switch(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Switch(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank();
@ -72,7 +73,8 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff,
channelConfiguration.payloadOn, channelConfiguration.payloadOff);
buildChannel(SWITCH_CHANNEL_ID, value, "state", componentConfiguration.getUpdateListener())
buildChannel(SWITCH_CHANNEL_ID, ComponentChannelType.SWITCH, value, getName(),
componentConfiguration.getUpdateListener())
.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())

View File

@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.types.StringType;
@ -142,14 +143,14 @@ public class Update extends AbstractComponent<Update.ChannelConfiguration> imple
private ReleaseState state = new ReleaseState();
private @Nullable ReleaseStateListener listener = null;
public Update(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Update(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TextValue value = new TextValue();
String commandTopic = channelConfiguration.commandTopic;
String payloadInstall = channelConfiguration.payloadInstall;
var builder = buildChannel(UPDATE_CHANNEL_ID, value, getName(), this);
var builder = buildChannel(UPDATE_CHANNEL_ID, ComponentChannelType.STRING, value, getName(), this);
if (channelConfiguration.stateTopic != null) {
builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
}
@ -162,7 +163,8 @@ public class Update extends AbstractComponent<Update.ChannelConfiguration> imple
if (channelConfiguration.latestVersionTopic != null) {
value = new TextValue();
latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, value, getName(), this)
latestVersionChannel = buildChannel(LATEST_VERSION_CHANNEL_ID, ComponentChannelType.STRING, value,
getName(), this)
.stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
.build(false);
}

View File

@ -27,6 +27,7 @@ import org.openhab.binding.mqtt.generic.values.PercentageValue;
import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -188,8 +189,8 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
*
* @param componentConfiguration generic componentConfiguration with not parsed JSON config
*/
public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration) {
super(componentConfiguration, ChannelConfiguration.class);
public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
final var allowedSupportedFeatures = channelConfiguration.schema == Schema.LEGACY ? LEGACY_SUPPORTED_FEATURES
@ -226,8 +227,8 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
addPayloadToList(deviceSupportedFeatures, FEATURE_START, channelConfiguration.payloadStart, commands);
}
buildOptionalChannel(COMMAND_CH_ID, new TextValue(commands.toArray(new String[0])), updateListener, null,
channelConfiguration.commandTopic, null, null);
buildOptionalChannel(COMMAND_CH_ID, ComponentChannelType.STRING, new TextValue(commands.toArray(new String[0])),
updateListener, null, channelConfiguration.commandTopic, null, null);
final var fanSpeedList = channelConfiguration.fanSpeedList;
if (deviceSupportedFeatures.contains(FEATURE_FAN_SPEED) && fanSpeedList != null && !fanSpeedList.isEmpty()) {
@ -236,48 +237,50 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
}
var fanSpeedValue = new TextValue(fanSpeedList.toArray(new String[0]));
if (channelConfiguration.schema == Schema.LEGACY) {
buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
buildOptionalChannel(FAN_SPEED_CH_ID, ComponentChannelType.STRING, fanSpeedValue, updateListener, null,
channelConfiguration.setFanSpeedTopic, channelConfiguration.fanSpeedTemplate,
channelConfiguration.fanSpeedTopic);
} else if (deviceSupportedFeatures.contains(FEATURE_STATUS)) {
buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
buildOptionalChannel(FAN_SPEED_CH_ID, ComponentChannelType.STRING, fanSpeedValue, updateListener, null,
channelConfiguration.setFanSpeedTopic, "{{ value_json.fan_speed }}",
channelConfiguration.stateTopic);
} else {
LOGGER.info("Status feature is disabled, unable to get fan speed.");
buildOptionalChannel(FAN_SPEED_CH_ID, fanSpeedValue, updateListener, null,
buildOptionalChannel(FAN_SPEED_CH_ID, ComponentChannelType.STRING, fanSpeedValue, updateListener, null,
channelConfiguration.setFanSpeedTopic, null, null);
}
}
if (deviceSupportedFeatures.contains(FEATURE_SEND_COMMAND)) {
buildOptionalChannel(CUSTOM_COMMAND_CH_ID, new TextValue(), updateListener, null,
channelConfiguration.sendCommandTopic, null, null);
buildOptionalChannel(CUSTOM_COMMAND_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener,
null, channelConfiguration.sendCommandTopic, null, null);
}
if (channelConfiguration.schema == Schema.LEGACY) {
// I assume, that if these topics defined in config, then we don't need to check features
buildOptionalChannel(BATTERY_LEVEL_CH_ID,
buildOptionalChannel(BATTERY_LEVEL_CH_ID, ComponentChannelType.DIMMER,
new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
updateListener, null, null, channelConfiguration.batteryLevelTemplate,
channelConfiguration.batteryLevelTopic);
buildOptionalChannel(CHARGING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
channelConfiguration.chargingTemplate, channelConfiguration.chargingTopic);
buildOptionalChannel(CLEANING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
channelConfiguration.cleaningTemplate, channelConfiguration.cleaningTopic);
buildOptionalChannel(DOCKED_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null,
channelConfiguration.dockedTemplate, channelConfiguration.dockedTopic);
buildOptionalChannel(ERROR_CH_ID, new TextValue(), updateListener, null, null,
buildOptionalChannel(CHARGING_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(TRUE, FALSE),
updateListener, null, null, channelConfiguration.chargingTemplate,
channelConfiguration.chargingTopic);
buildOptionalChannel(CLEANING_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(TRUE, FALSE),
updateListener, null, null, channelConfiguration.cleaningTemplate,
channelConfiguration.cleaningTopic);
buildOptionalChannel(DOCKED_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(TRUE, FALSE), updateListener,
null, null, channelConfiguration.dockedTemplate, channelConfiguration.dockedTopic);
buildOptionalChannel(ERROR_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener, null, null,
channelConfiguration.errorTemplate, channelConfiguration.errorTopic);
} else {
if (deviceSupportedFeatures.contains(FEATURE_STATUS)) {
// state key is mandatory
buildOptionalChannel(STATE_CH_ID,
buildOptionalChannel(STATE_CH_ID, ComponentChannelType.STRING,
new TextValue(new String[] { STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, STATE_IDLE,
STATE_RETURNING, STATE_ERROR }),
updateListener, null, null, "{{ value_json.state }}", channelConfiguration.stateTopic);
if (deviceSupportedFeatures.contains(FEATURE_BATTERY)) {
buildOptionalChannel(BATTERY_LEVEL_CH_ID,
buildOptionalChannel(BATTERY_LEVEL_CH_ID, ComponentChannelType.DIMMER,
new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
updateListener, null, null, "{{ value_json.battery_level }}",
channelConfiguration.stateTopic);
@ -285,16 +288,16 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
}
}
buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, new TextValue(), updateListener, null, null,
channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener, null,
null, channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
}
@Nullable
private ComponentChannel buildOptionalChannel(String channelId, Value valueState,
private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
@Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic) {
if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) {
return buildChannel(channelId, valueState, getName(), channelStateUpdateListener)
return buildChannel(channelId, channelType, valueState, getName(), channelStateUpdateListener)
.stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate)

View File

@ -145,7 +145,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
@Override
public Set<ThingTypeUID> getSupportedThingTypes() {
return typeProvider.getThingTypeUIDs();
return typeProvider.getThingTypes(null).stream().map(ThingType::getUID).collect(Collectors.toSet());
}
/**
@ -206,11 +206,8 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
.fromString(new String(payload, StandardCharsets.UTF_8), gson);
final String thingID = config.getThingId(haID.objectID);
final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID,
MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge,
thingID);
thingIDPerTopic.put(topic, thingUID);
@ -241,6 +238,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
properties = handlerConfig.appendToProperties(properties);
properties = config.appendToProperties(properties);
properties.put("deviceId", thingID);
properties.put("newStyleChannels", "true");
// Because we need the new properties map with the updated "components" list
results.put(thingUID.getAsString(),
@ -282,10 +280,6 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
results.clear();
componentsPerThingID.clear();
for (DiscoveryResult result : localResults) {
final ThingTypeUID typeID = result.getThingTypeUID();
ThingType type = typeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING).build();
typeProvider.setThingTypeIfAbsent(typeID, type);
thingDiscovered(result);
}
}

View File

@ -13,7 +13,6 @@
package org.openhab.binding.mqtt.homeassistant.internal.handler;
import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@ -24,12 +23,12 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
import org.openhab.binding.mqtt.generic.ChannelState;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing;
@ -48,7 +47,6 @@ import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationEx
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -56,10 +54,7 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.util.ThingHelper;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -86,13 +81,16 @@ import com.google.gson.GsonBuilder;
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
public static final String AVAILABILITY_CHANNEL = "availability";
private static final Comparator<Channel> CHANNEL_COMPARATOR_BY_UID = Comparator
.comparing(channel -> channel.getUID().toString());
private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
.comparing((AbstractComponent<?> component) -> component.hasGroup())
.thenComparing(AbstractComponent::getName);
private static final URI UPDATABLE_CONFIG_DESCRIPTION_URI = URI.create("thing-type:mqtt:homeassistant-updatable");
private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
protected final MqttChannelTypeProvider channelTypeProvider;
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry;
public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing;
protected final DiscoverComponents discoverComponents;
@ -106,6 +104,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final TransformationServiceProvider transformationServiceProvider;
private boolean started;
private boolean newStyleChannels;
private @Nullable Update updateComponent;
/**
@ -118,16 +117,22 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
* @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds.
*/
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
int attributeReceiveTimeout) {
super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.transformationServiceProvider = transformationServiceProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler);
newStyleChannels = "true".equals(thing.getProperties().get("newStyleChannels"));
this.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson,
this.transformationServiceProvider);
this.transformationServiceProvider, newStyleChannels);
}
@Override
@ -141,16 +146,12 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
discoveryHomeAssistantIDs.addAll(HaID.fromConfig(config));
ThingTypeUID typeID = getThing().getThingTypeUID();
for (Channel channel : thing.getChannels()) {
final String groupID = channel.getUID().getGroupId();
// Already restored component?
@Nullable
AbstractComponent<?> component = haComponents.get(groupID);
if (component != null) {
// the types may have been removed in dispose() so we need to add them again
component.addChannelTypes(channelTypeProvider);
continue;
}
HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration());
discoveryHomeAssistantIDs.add(haID);
@ -161,29 +162,27 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} else {
try {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
scheduler, gson, transformationServiceProvider);
final ChannelGroupUID groupUID = component.getGroupUID();
String id = null;
if (groupUID != null) {
id = groupUID.getId();
scheduler, gson, transformationServiceProvider, newStyleChannels);
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(component);
}
haComponents.put(id, component);
component.addChannelTypes(channelTypeProvider);
haComponents.put(component.getGroupId(), component);
} catch (ConfigurationException e) {
logger.error("Cannot not restore component {}: {}", thing, e.getMessage());
logger.error("Cannot restore component {}: {}", thing, e.getMessage());
}
}
}
updateThingType();
super.initialize();
if (updateThingType(typeID)) {
super.initialize();
}
}
@Override
public void dispose() {
removeStateDescriptions();
// super.dispose() calls stop()
super.dispose();
haComponents.values().forEach(c -> c.removeChannelTypes(channelTypeProvider));
}
@Override
@ -234,13 +233,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
@Override
public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
String groupID = channelUID.getGroupId();
String componentId;
if (channelUID.isInGroup()) {
componentId = channelUID.getGroupId();
} else {
componentId = channelUID.getId();
}
AbstractComponent<?> component;
synchronized (haComponents) { // sync whenever discoverComponents is started
component = haComponents.get(groupID);
component = haComponents.get(componentId);
}
if (component == null) {
return null;
component = haComponents.get("");
if (component == null) {
return null;
}
}
ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
if (componentChannel == null) {
@ -269,12 +276,12 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
synchronized (haComponents) { // sync whenever discoverComponents is started
ThingTypeUID typeID = getThing().getThingTypeUID();
for (AbstractComponent<?> discovered : discoveredComponentsList) {
final ChannelGroupUID groupUID = discovered.getGroupUID();
String id = null;
if (groupUID != null) {
id = groupUID.getId();
if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
typeID = calculateThingTypeUID(discovered);
}
String id = discovered.getGroupId();
AbstractComponent<?> known = haComponents.get(id);
// Is component already known?
if (known != null) {
@ -288,8 +295,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
}
// Add channel and group types to the types registry
discovered.addChannelTypes(channelTypeProvider);
// Add component to the component map
haComponents.put(id, discovered);
// Start component / Subscribe to channel topics
@ -302,47 +307,11 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
updateComponent = (Update) discovered;
updateComponent.setReleaseStateUpdateListener(this::releaseStateUpdated);
}
List<Channel> discoveredChannels = discovered.getChannelMap().values().stream()
.map(ComponentChannel::getChannel).collect(Collectors.toList());
if (known != null) {
// We had previously known component with different config hash
// We remove all conflicting old channels, they will be re-added below based on the new discovery
logger.debug(
"Received component {} with slightly different config. Making sure we re-create conflicting channels...",
discovered.getHaID());
removeJustRediscoveredChannels(discoveredChannels);
}
// Add newly discovered channels. We sort the channels
// for (mostly) consistent jsondb serialization
discoveredChannels.sort(CHANNEL_COMPARATOR_BY_UID);
ThingHelper.addChannelsToThing(thing, discoveredChannels);
}
updateThingType();
updateThingType(typeID);
}
}
private void removeJustRediscoveredChannels(List<Channel> discoveredChannels) {
ArrayList<Channel> mutableChannels = new ArrayList<>(getThing().getChannels());
Set<ChannelUID> newChannelUIDs = discoveredChannels.stream().map(Channel::getUID).collect(Collectors.toSet());
// Take current channels but remove those channels that were just re-discovered
List<Channel> existingChannelsWithNewlyDiscoveredChannelsRemoved = mutableChannels.stream()
.filter(existingChannel -> !newChannelUIDs.contains(existingChannel.getUID()))
.collect(Collectors.toList());
if (existingChannelsWithNewlyDiscoveredChannelsRemoved.size() < mutableChannels.size()) {
// We sort the channels for (mostly) consistent jsondb serialization
existingChannelsWithNewlyDiscoveredChannelsRemoved.sort(CHANNEL_COMPARATOR_BY_UID);
updateThingChannels(existingChannelsWithNewlyDiscoveredChannelsRemoved);
}
}
private void updateThingChannels(List<Channel> channelList) {
ThingBuilder thingBuilder = editThing();
thingBuilder.withChannels(channelList);
updateThing(thingBuilder.build());
}
@Override
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
if (availabilityTopicsSeen.orElse(messageReceived)) {
@ -372,28 +341,72 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
super.handleConfigurationUpdate(configurationParameters);
}
private void updateThingType() {
private boolean updateThingType(ThingTypeUID typeID) {
// if this is a dynamic type, then we update the type
ThingTypeUID typeID = thing.getThingTypeUID();
if (!MqttBindingConstants.HOMEASSISTANT_MQTT_THING.equals(typeID)) {
List<ChannelGroupDefinition> groupDefs;
List<ChannelDefinition> channelDefs;
synchronized (haComponents) { // sync whenever discoverComponents is started
groupDefs = haComponents.values().stream().map(AbstractComponent::getGroupDefinition)
.filter(Objects::nonNull).map(Objects::requireNonNull).collect(Collectors.toList());
channelDefs = haComponents.values().stream().map(AbstractComponent::getChannels).flatMap(List::stream)
.collect(Collectors.toList());
}
var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING)
.withChannelDefinitions(channelDefs).withChannelGroupDefinitions(groupDefs);
Update updateComponent = this.updateComponent;
if (updateComponent != null && updateComponent.isUpdatable()) {
builder.withConfigDescriptionURI(UPDATABLE_CONFIG_DESCRIPTION_URI);
}
ThingType thingType = builder.build();
var thingTypeBuilder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING);
channelTypeProvider.setThingType(typeID, thingType);
if (getThing().getThingTypeUID().equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
logger.debug("Migrating Home Assistant thing {} from generic type to dynamic type {}",
getThing().getUID(), typeID);
// just create an empty thing type for now; channel configurations won't follow over
// to the re-created Thing, so we need to re-discover them all anyway
channelTypeProvider.putThingType(thingTypeBuilder.build());
changeThingType(typeID, getConfig());
return false;
}
synchronized (haComponents) { // sync whenever discoverComponents is started
var sortedComponents = haComponents.values().stream().sorted(COMPONENT_COMPARATOR).toList();
var channelGroupTypes = sortedComponents.stream().map(c -> c.getChannelGroupType(typeID.getId()))
.filter(Objects::nonNull).map(Objects::requireNonNull).toList();
channelTypeProvider.updateChannelGroupTypesForPrefix(typeID.getId(), channelGroupTypes);
var groupDefs = sortedComponents.stream().map(c -> c.getGroupDefinition(typeID.getId()))
.filter(Objects::nonNull).map(Objects::requireNonNull).toList();
var channelDefs = sortedComponents.stream().map(AbstractComponent::getChannelDefinitions)
.flatMap(List::stream).toList();
thingTypeBuilder.withChannelDefinitions(channelDefs).withChannelGroupDefinitions(groupDefs);
Update updateComponent = this.updateComponent;
if (updateComponent != null && updateComponent.isUpdatable()) {
thingTypeBuilder.withConfigDescriptionURI(UPDATABLE_CONFIG_DESCRIPTION_URI);
}
channelTypeProvider.putThingType(thingTypeBuilder.build());
removeStateDescriptions();
sortedComponents.stream().forEach(c -> c.addStateDescriptions(stateDescriptionProvider));
ThingBuilder thingBuilder = editThing().withChannels();
sortedComponents.stream().map(AbstractComponent::getChannels).flatMap(List::stream)
.forEach(c -> thingBuilder.withChannel(c));
updateThing(thingBuilder.build());
}
}
return true;
}
private ThingTypeUID calculateThingTypeUID(AbstractComponent component) {
return new ThingTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId()
+ "_" + component.getChannelConfiguration().getThingId(component.getHaID().objectID));
}
@Override
public void handleRemoval() {
synchronized (haComponents) {
channelTypeProvider.removeThingType(thing.getThingTypeUID());
channelTypeProvider.removeChannelGroupTypesForPrefix(thing.getThingTypeUID().getId());
removeStateDescriptions();
}
super.handleRemoval();
}
private void removeStateDescriptions() {
thing.getChannels().stream().forEach(c -> stateDescriptionProvider.remove(c.getUID()));
}
private void releaseStateUpdated(Update.ReleaseState state) {

View File

@ -7,7 +7,7 @@
<config-description uri="channel-type:mqtt:ha-channel">
<parameter name="component" type="text" readOnly="true" required="true">
<label>Component</label>
<description>HomeAssistant component type (e.g. binary_sensor, switch, light)</description>
<description>Home Assistant component type (e.g. binary_sensor, switch, light)</description>
<default></default>
</parameter>
<parameter name="nodeid" type="text" readOnly="true">
@ -17,12 +17,12 @@
</parameter>
<parameter name="objectid" type="text" readOnly="true" required="true">
<label>Object ID</label>
<description>Object id of the component</description>
<description>Object ID of the component</description>
<default></default>
</parameter>
<parameter name="config" type="text" readOnly="true" required="true">
<label>Json Configuration</label>
<description>The json configuration string received by the component via MQTT.</description>
<label>JSON Configuration</label>
<description>The JSON configuration string received by the component via MQTT.</description>
<default></default>
</parameter>
</config-description>

View File

@ -9,6 +9,39 @@ thing-type.config.mqtt.homeassistant.basetopic.label = MQTT Base Prefix
thing-type.config.mqtt.homeassistant.basetopic.description = MQTT base prefix
thing-type.config.mqtt.homeassistant.topics.label = MQTT Config Topic
thing-type.config.mqtt.homeassistant.topics.description = List of Home Assistant configuration topics (e.g. button/my-device/restart)
# channel types
channel-type.mqtt.ha-color-advanced.label = Color
channel-type.mqtt.ha-color.label = Color
channel-type.mqtt.ha-dimmer-advanced.label = Dimmer
channel-type.mqtt.ha-dimmer.label = Dimmer
channel-type.mqtt.ha-image-advanced.label = Image
channel-type.mqtt.ha-image.label = Image
channel-type.mqtt.ha-number-advanced.label = Number
channel-type.mqtt.ha-number.label = Number
channel-type.mqtt.ha-rollershutter-advanced.label = Rollershutter
channel-type.mqtt.ha-rollershutter.label = Rollershutter
channel-type.mqtt.ha-string-advanced.label = String
channel-type.mqtt.ha-string.label = String
channel-type.mqtt.ha-switch-advanced.label = Switch
channel-type.mqtt.ha-switch.label = Switch
channel-type.mqtt.ha-trigger-advanced.label = Trigger
channel-type.mqtt.ha-trigger.label = Trigger
# channel types config
channel-type.config.mqtt.ha-channel.component.label = Component
channel-type.config.mqtt.ha-channel.component.description = Home Assistant component type (e.g. binary_sensor, switch, light)
channel-type.config.mqtt.ha-channel.config.label = JSON Configuration
channel-type.config.mqtt.ha-channel.config.description = The JSON configuration string received by the component via MQTT.
channel-type.config.mqtt.ha-channel.nodeid.label = Node ID
channel-type.config.mqtt.ha-channel.nodeid.description = Optional node name of the component
channel-type.config.mqtt.ha-channel.objectid.label = Object ID
channel-type.config.mqtt.ha-channel.objectid.description = Object ID of the component
# thing types config
thing-type.config.mqtt.homeassistant-updatable.basetopic.label = MQTT Base Prefix
thing-type.config.mqtt.homeassistant-updatable.basetopic.description = MQTT base prefix
thing-type.config.mqtt.homeassistant-updatable.topics.label = MQTT Config Topic

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mqtt"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="ha-color">
<item-type>Color</item-type>
<label>Color</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-dimmer">
<item-type>Dimmer</item-type>
<label>Dimmer</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-image">
<item-type>Image</item-type>
<label>Image</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-number">
<item-type>Number</item-type>
<label>Number</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-rollershutter">
<item-type>Rollershutter</item-type>
<label>Rollershutter</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-string">
<item-type>String</item-type>
<label>String</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-switch">
<item-type>Switch</item-type>
<label>Switch</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-trigger">
<kind>trigger</kind>
<label>Trigger</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-color-advanced" advanced="true">
<item-type>Color</item-type>
<label>Color</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-dimmer-advanced" advanced="true">
<item-type>Dimmer</item-type>
<label>Dimmer</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-image-advanced" advanced="true">
<item-type>Image</item-type>
<label>Image</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-number-advanced" advanced="true">
<item-type>Number</item-type>
<label>Number</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-rollershutter-advanced" advanced="true">
<item-type>Rollershutter</item-type>
<label>Rollershutter</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-string-advanced" advanced="true">
<item-type>String</item-type>
<label>String</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-switch-advanced" advanced="true">
<item-type>Switch</item-type>
<label>Switch</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
<channel-type id="ha-trigger-advanced" advanced="true">
<kind>trigger</kind>
<label>Trigger</label>
<config-description-ref uri="channel-type:mqtt:ha-channel"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -35,12 +35,15 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.handler.BrokerHandler;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.test.java.JavaTest;
import org.openhab.core.test.storage.VolatileStorageService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -50,6 +53,8 @@ import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeBuilder;
import org.openhab.core.thing.type.ThingTypeRegistry;
import org.openhab.transform.jinja.internal.JinjaTransformationService;
@ -72,17 +77,20 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
public static final String BRIDGE_ID = UUID.randomUUID().toString();
public static final ThingUID BRIDGE_UID = new ThingUID(BRIDGE_TYPE_UID, BRIDGE_ID);
public static final String HA_TYPE_ID = "homeassistant";
public static final String HA_TYPE_LABEL = "Homeassistant";
public static final ThingTypeUID HA_TYPE_UID = new ThingTypeUID(BINDING_ID, HA_TYPE_ID);
public static final String HA_TYPE_LABEL = "Home Assistant Thing";
public static final ThingTypeUID HA_TYPE_UID = new ThingTypeUID(BINDING_ID, "homeassistant_dynamic_type");
public static final String HA_ID = UUID.randomUUID().toString();
public static final ThingUID HA_UID = new ThingUID(HA_TYPE_UID, HA_ID);
public static final ThingUID HA_UID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_ID);
public static final ThingType HA_THING_TYPE = ThingTypeBuilder
.instance(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, HA_TYPE_LABEL).build();
protected @Mock @NonNullByDefault({}) MqttBrokerConnection bridgeConnection;
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider;
protected @NonNullByDefault({}) MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected @NonNullByDefault({}) ChannelTypeRegistry channelTypeRegistry;
protected final Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build();
protected final BrokerHandler bridgeHandler = spy(new BrokerHandler(bridgeThing));
@ -95,13 +103,14 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
public void beforeEachAbstractHomeAssistantTests() {
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
when(thingTypeRegistry.getThingType(HA_TYPE_UID))
.thenReturn(ThingTypeBuilder.instance(HA_TYPE_UID, HA_TYPE_LABEL).build());
when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE);
when(transformationServiceProvider
.getTransformationService(JinjaTransformationProfile.PROFILE_TYPE_UID.getId()))
.thenReturn(jinjaTransformationService);
channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry));
channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService()));
stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider());
channelTypeRegistry = spy(new ChannelTypeRegistry());
setupConnection();

View File

@ -32,6 +32,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value;
@ -45,6 +46,7 @@ import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
@ -76,8 +78,8 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, transformationServiceProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler);
@ -193,7 +195,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
*/
protected void assertTriggered(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, String trigger) {
verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannelUID()), eq(trigger));
verify(thingHandler).triggerChannel(eq(component.getChannel(channelId).getChannel().getUID()), eq(trigger));
}
/**
@ -277,7 +279,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component,
String channelId, Command command) {
var channel = Objects.requireNonNull(component.getChannel(channelId));
thingHandler.handleCommand(channel.getChannelUID(), command);
thingHandler.handleCommand(channel.getChannel().getUID(), command);
}
protected static class LatchThingHandler extends HomeAssistantThingHandler {
@ -285,9 +287,11 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
int attributeReceiveTimeout) {
super(thing, channelTypeProvider, transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry,
transformationServiceProvider, subscribeTimeout, attributeReceiveTimeout);
}
@Override

View File

@ -65,9 +65,9 @@ public class BinarySensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("onoffsensor"));
assertThat(component.getGroupUID().getId(), is("sn1"));
assertThat(component.getGroupId(), is("sn1"));
assertChannel(component, BinarySensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "value",
assertChannel(component, BinarySensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "onoffsensor",
OnOffValue.class);
publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }");

View File

@ -65,7 +65,7 @@ public class SensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("sensor1"));
assertThat(component.getGroupUID().getId(), is("sn1"));
assertThat(component.getGroupId(), is("sn1"));
assertChannel(component, Sensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "sensor1",
NumberValue.class);

View File

@ -66,8 +66,8 @@ public class SwitchTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock"));
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/auto_lock", "state",
OnOffValue.class);
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/auto_lock",
"th1 auto lock", OnOffValue.class);
publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.OFF);
@ -111,7 +111,7 @@ public class SwitchTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock"));
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "", "state", OnOffValue.class);
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "", "th1 auto lock", OnOffValue.class);
publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.OFF);
@ -151,7 +151,7 @@ public class SwitchTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock"));
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "", "zigbee2mqtt/th1/set/auto_lock", "state",
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "", "zigbee2mqtt/th1/set/auto_lock", "th1 auto lock",
OnOffValue.class);
component.getChannel(Switch.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.OFF);

View File

@ -37,6 +37,7 @@ import org.openhab.binding.mqtt.homeassistant.internal.component.Sensor;
import org.openhab.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.StateDescription;
/**
* Tests for {@link HomeAssistantThingHandler}
@ -73,8 +74,8 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, transformationServiceProvider,
SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, transformationServiceProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@ -105,9 +106,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(6));
verify(channelTypeProvider, times(6)).setChannelType(any(), any());
verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any());
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(6));
verify(stateDescriptionProvider, times(6)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(1)).putChannelGroupType(any());
configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config";
thingHandler.discoverComponents.processMessage(configTopic,
@ -116,9 +117,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
verify(channelTypeProvider, times(7)).setChannelType(any(), any());
verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(3)).putChannelGroupType(any());
}
/**
@ -170,7 +171,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
thingHandler.delayedProcessing.forceProcessNow();
waitForAssert(() -> {
assertThat("1 channel created", thingHandler.getThing().getChannels().size() == 1);
assertThat("1 channel created", nonSpyThingHandler.getThing().getChannels().size() == 1);
});
//
@ -186,7 +187,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
thingHandler.delayedProcessing.forceProcessNow();
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempOutside)), any(Sensor.class));
waitForAssert(() -> {
assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
assertThat("2 channel created", nonSpyThingHandler.getThing().getChannels().size() == 2);
});
//
@ -201,7 +202,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
thingHandler.delayedProcessing.forceProcessNow();
waitForAssert(() -> {
assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
assertThat("2 channel created", nonSpyThingHandler.getThing().getChannels().size() == 2);
});
//
@ -219,7 +220,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(2)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
waitForAssert(() -> {
assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2);
assertThat("2 channel created", nonSpyThingHandler.getThing().getChannels().size() == 2);
});
}
@ -239,8 +240,8 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(7));
verify(channelTypeProvider, times(7)).setChannelType(any(), any());
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
// When dispose
thingHandler.dispose();
@ -249,9 +250,31 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
MQTT_TOPICS.forEach(t -> {
verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any());
});
}
// Expect channel types removed, 6 for climate and 1 for switch
verify(channelTypeProvider, times(7)).removeChannelType(any());
@Test
public void testRemoveThing() {
thingHandler.initialize();
// Expect subscription on each topic from config
CONFIG_TOPICS.forEach(t -> {
var fullTopic = HandlerConfiguration.DEFAULT_BASETOPIC + "/" + t + "/config";
verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(fullTopic), any());
});
thingHandler.discoverComponents.processMessage(
"homeassistant/climate/0x847127fffe11dd6a_climate_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601ClimateThermostat.json"));
thingHandler.discoverComponents.processMessage(
"homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json"));
thingHandler.delayedProcessing.forceProcessNow();
assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
// When dispose
nonSpyThingHandler.handleRemoval();
// Expect channel descriptions removed, 6 for climate and 1 for switch
verify(stateDescriptionProvider, times(7)).remove(any());
// Expect channel group types removed, 1 for each component
verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
}

View File

@ -12,6 +12,7 @@
*/
package org.openhab.binding.mqtt.homie.internal.handler;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -19,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -140,6 +142,8 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
if (config.removetopics) {
this.removeRetainedTopics();
}
channelTypeProvider.removeThingType(thing.getThingTypeUID());
channelTypeProvider.removeChannelGroupTypesForPrefix(thing.getThingTypeUID().getId());
super.handleRemoval();
}
@ -169,7 +173,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
}
delayedProcessing.join();
device.stop();
channelTypeProvider.removeThingType(device.thingTypeUID);
super.stop();
}
@ -216,7 +219,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
@Override
public void nodeRemoved(Node node) {
channelTypeProvider.removeChannelGroupType(node.channelGroupTypeUID);
delayedProcessing.accept(node);
}
@ -228,7 +230,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
@Override
public void nodeAddedOrChanged(Node node) {
channelTypeProvider.setChannelGroupType(node.channelGroupTypeUID, node.type());
delayedProcessing.accept(node);
}
@ -288,27 +289,42 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
private void updateThingType() {
// Make sure any dynamic channel types exist (i.e. ones created for a number channel with a specific dimension)
device.nodes.stream().flatMap(n -> n.properties.stream()).map(Property::getChannelType).filter(Objects::nonNull)
.forEach(ct -> channelTypeProvider.setChannelType(ct.getUID(), ct));
.forEach(ct -> channelTypeProvider.putChannelType(Objects.requireNonNull(ct)));
// if this is a dynamic type, then we update the type
ThingTypeUID typeID = device.thingTypeUID;
if (!MqttBindingConstants.HOMIE300_MQTT_THING.equals(typeID)) {
device.nodes.stream()
.forEach(n -> channelTypeProvider.setChannelGroupType(n.channelGroupTypeUID, n.type()));
channelTypeProvider.updateChannelGroupTypesForPrefix(thing.getThingTypeUID().getId(), device.nodes.stream()
.map(n -> n.type(thing.getThingTypeUID().getId(), channelTypeProvider)).toList());
List<ChannelGroupDefinition> groupDefs = device.nodes().stream().map(Node::getChannelGroupDefinition)
.collect(Collectors.toList());
List<ChannelGroupDefinition> groupDefs = device.nodes.stream(nodeOrder())
.map(n -> n.getChannelGroupDefinition(thing.getThingTypeUID().getId())).toList();
var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMIE300_MQTT_THING)
.withChannelGroupDefinitions(groupDefs);
ThingType thingType = builder.build();
channelTypeProvider.setThingType(typeID, thingType);
channelTypeProvider.putThingType(builder.build());
}
}
private void updateChannels() {
List<Channel> channels = device.nodes().stream().flatMap(n -> n.properties.stream())
.map(p -> p.getChannel(channelTypeRegistry)).collect(Collectors.toList());
List<Channel> channels = device.nodes.stream(nodeOrder())
.flatMap(node -> node.properties
.stream(node.propertyOrder(thing.getThingTypeUID().getId(), channelTypeProvider))
.map(p -> p.getChannel(channelTypeRegistry)))
.toList();
updateThing(editThing().withChannels(channels).build());
}
private Collection<String> nodeOrder() {
String[] nodes = device.attributes.nodes;
if (nodes != null) {
return Stream.of(nodes).toList();
}
ThingType thingType = channelTypeProvider.getThingType(thing.getThingTypeUID(), null);
if (thingType != null) {
return thingType.getChannelGroupDefinitions().stream().map(ChannelGroupDefinition::getId).toList();
}
return device.nodes.keySet();
}
}

View File

@ -13,6 +13,7 @@
package org.openhab.binding.mqtt.homie.internal.homie300;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@ -22,6 +23,7 @@ import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass;
import org.openhab.binding.mqtt.generic.tools.ChildMap;
import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants;
@ -55,7 +57,6 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
// Runtime
public final DeviceCallback callback;
protected final ChannelGroupUID channelGroupUID;
public final ChannelGroupTypeUID channelGroupTypeUID;
private final String topic;
private boolean initialized = false;
@ -72,7 +73,6 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
this.topic = topic + "/" + nodeID;
this.nodeID = nodeID;
this.callback = callback;
channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, UIDUtils.encode(this.topic));
channelGroupUID = new ChannelGroupUID(thingUID, UIDUtils.encode(nodeID));
properties = new ChildMap<>();
}
@ -117,15 +117,16 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
/**
* Return the channel group type for this Node.
*/
public ChannelGroupType type() {
final List<ChannelDefinition> channelDefinitions = properties.stream()
.map(p -> Objects.requireNonNull(p.getChannelDefinition())).collect(Collectors.toList());
return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, attributes.name)
public ChannelGroupType type(String prefix, MqttChannelTypeProvider channelTypeProvider) {
final List<ChannelDefinition> channelDefinitions = properties.stream(propertyOrder(prefix, channelTypeProvider))
.map(p -> Objects.requireNonNull(p.getChannelDefinition())).toList();
return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), attributes.name)
.withChannelDefinitions(channelDefinitions).build();
}
public ChannelGroupDefinition getChannelGroupDefinition() {
return new ChannelGroupDefinition(channelGroupUID.getId(), channelGroupTypeUID, attributes.name, null);
public ChannelGroupDefinition getChannelGroupDefinition(String prefix) {
return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), attributes.name,
null);
}
/**
@ -220,4 +221,21 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
return topics;
}
public Collection<String> propertyOrder(String prefix, MqttChannelTypeProvider channelTypeProvider) {
String[] properties = attributes.properties;
if (properties != null) {
return Stream.of(properties).toList();
}
ChannelGroupType channelGroupType = channelTypeProvider.getChannelGroupType(getChannelGroupTypeUID(prefix),
null);
if (channelGroupType != null) {
return channelGroupType.getChannelDefinitions().stream().map(ChannelDefinition::getId).toList();
}
return this.properties.keySet();
}
private ChannelGroupTypeUID getChannelGroupTypeUID(String prefix) {
return new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, prefix + "_" + UIDUtils.encode(this.topic));
}
}

View File

@ -62,6 +62,7 @@ import org.openhab.binding.mqtt.homie.internal.homie300.PropertyAttributes.DataT
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.types.StringType;
import org.openhab.core.test.storage.VolatileStorageService;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -99,7 +100,8 @@ public class HomieThingHandlerTests {
private @NonNullByDefault({}) Thing thing;
private @NonNullByDefault({}) HomieThingHandler thingHandler;
private final MqttChannelTypeProvider channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistryMock));
private final MqttChannelTypeProvider channelTypeProvider = spy(
new MqttChannelTypeProvider(thingTypeRegistryMock, new VolatileStorageService()));
private final MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
private final String deviceID = ThingChannelConstants.TEST_HOMIE_THING.getId();

View File

@ -84,7 +84,7 @@ public class DiscoverComponentsTest extends JavaOSGiTest {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider));
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider, true));
HandlerConfiguration config = new HandlerConfiguration("homeassistant", List.of("switch/object"));

View File

@ -22,7 +22,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
@ -56,7 +55,6 @@ import org.openhab.core.io.transport.mqtt.MqttConnectionState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.UIDUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -152,17 +150,14 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING,
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider));
scheduler, channelStateUpdateListener, availabilityTracker, gson, transformationServiceProvider, true));
// The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map
// and add the types to the channelTypeProvider, like in the real Thing handler.
final CountDownLatch latch = new CountDownLatch(1);
ComponentDiscovered cd = (haID, c) -> {
haComponents.put(c.getGroupUID().getId(), c);
c.addChannelTypes(channelTypeProvider);
channelTypeProvider.setChannelGroupType(Objects.requireNonNull(c.getGroupTypeUID()),
Objects.requireNonNull(c.getType()));
haComponents.put(c.getGroupId(), c);
latch.countDown();
};
@ -181,15 +176,10 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
assertNull(failure);
assertThat(haComponents.size(), is(1));
// For the switch component we should have one channel group type and one channel type
// setChannelGroupType is called once above
verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
verify(channelTypeProvider, times(1)).setChannelType(any(), any());
String channelGroupId = "switch_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId();
String channelId = Switch.SWITCH_CHANNEL_ID;
String channelGroupId = UIDUtils
.encode("node_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId() + "_switch");
State value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache()
State value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache()
.getChannelState();
assertThat(value, is(UnDefType.UNDEF));
@ -203,8 +193,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any());
// Value should be ON now.
value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache()
.getChannelState();
value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache().getChannelState();
assertThat(value, is(OnOffType.ON));
}
}