[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>
pull/17240/head
Cody Cutrer 2024-08-12 13:23:24 -06:00 committed by GitHub
parent 2bc890c2c7
commit 73a3f151c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Set; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.internal.MqttThingHandlerFactory; 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.ThingTypeUID;
import org.openhab.core.thing.binding.AbstractStorageBasedTypeProvider;
import org.openhab.core.thing.binding.ThingTypeProvider; import org.openhab.core.thing.binding.ThingTypeProvider;
import org.openhab.core.thing.type.ChannelGroupType; import org.openhab.core.thing.type.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeProvider; import org.openhab.core.thing.type.ChannelGroupTypeProvider;
import org.openhab.core.thing.type.ChannelGroupTypeUID; 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.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.thing.type.ThingType; import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.type.ThingTypeBuilder; import org.openhab.core.thing.type.ThingTypeBuilder;
import org.openhab.core.thing.type.ThingTypeRegistry; 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. * This provider is started on-demand only, as soon as {@link MqttThingHandlerFactory} or an extension requires it.
* *
* @author David Graeff - Initial contribution * @author David Graeff - Initial contribution
* @author Cody Cutrer - Use AbstractStorageBasedTypeProvider
* *
*/ */
@NonNullByDefault @NonNullByDefault
@Component(immediate = false, service = { ThingTypeProvider.class, ChannelTypeProvider.class, @Component(immediate = false, service = { ThingTypeProvider.class, ChannelTypeProvider.class,
ChannelGroupTypeProvider.class, MqttChannelTypeProvider.class }) ChannelGroupTypeProvider.class, MqttChannelTypeProvider.class })
public class MqttChannelTypeProvider implements ThingTypeProvider, ChannelGroupTypeProvider, ChannelTypeProvider { public class MqttChannelTypeProvider extends AbstractStorageBasedTypeProvider {
private final ThingTypeRegistry typeRegistry; private final ThingTypeRegistry thingTypeRegistry;
private final Map<ChannelTypeUID, ChannelType> types = new ConcurrentHashMap<>();
private final Map<ChannelGroupTypeUID, ChannelGroupType> groups = new ConcurrentHashMap<>();
private final Map<ThingTypeUID, ThingType> things = new ConcurrentHashMap<>();
@Activate @Activate
public MqttChannelTypeProvider(@Reference ThingTypeRegistry typeRegistry) { public MqttChannelTypeProvider(@Reference ThingTypeRegistry thingTypeRegistry,
super(); @Reference StorageService storageService) {
this.typeRegistry = typeRegistry; super(storageService);
} this.thingTypeRegistry = thingTypeRegistry;
@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 ThingTypeBuilder derive(ThingTypeUID newTypeId, ThingTypeUID baseTypeId) { public ThingTypeBuilder derive(ThingTypeUID newTypeId, ThingTypeUID baseTypeId) {
ThingType baseType = typeRegistry.getThingType(baseTypeId); ThingType baseType = thingTypeRegistry.getThingType(baseTypeId);
ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeId, baseType.getLabel()) ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeId, baseType.getLabel())
.withChannelGroupDefinitions(baseType.getChannelGroupDefinitions()) .withChannelGroupDefinitions(baseType.getChannelGroupDefinitions())
@ -155,4 +87,25 @@ public class MqttChannelTypeProvider implements ThingTypeProvider, ChannelGroupT
return result; 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; package org.openhab.binding.mqtt.generic.tools;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -64,6 +67,24 @@ public class ChildMap<T> {
return map.values().stream(); 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. * 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) { public void put(String key, T value) {
map.put(key, 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 // List of all Thing Type UIDs
public static final ThingTypeUID HOMEASSISTANT_MQTT_THING = new ThingTypeUID(BINDING_ID, "homeassistant"); 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler; 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.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; 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.TransformationHelper;
import org.openhab.core.transform.TransformationService; 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.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
/** /**
@ -43,10 +43,22 @@ import org.osgi.service.component.annotations.Reference;
@Component(service = ThingHandlerFactory.class) @Component(service = ThingHandlerFactory.class)
@NonNullByDefault @NonNullByDefault
public class MqttThingHandlerFactory extends BaseThingHandlerFactory implements TransformationServiceProvider { 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 private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(MqttBindingConstants.HOMEASSISTANT_MQTT_THING).collect(Collectors.toSet()); .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 @Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) { public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || isHomeassistantDynamicType(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()); && 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 @Override
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (supportsThingType(thingTypeUID)) { if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, this, 10000, 2000); return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
this, 10000, 2000);
} }
return null; return null;
} }

View File

@ -12,7 +12,7 @@
*/ */
package org.openhab.binding.mqtt.homeassistant.internal; package org.openhab.binding.mqtt.homeassistant.internal;
import java.net.URI; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Predicate; 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.ChannelState;
import org.openhab.binding.mqtt.generic.ChannelStateTransformation; import org.openhab.binding.mqtt.generic.ChannelStateTransformation;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; 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.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value; 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.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.core.config.core.Configuration; import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; 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.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelDefinition; import org.openhab.core.thing.type.ChannelDefinition;
import org.openhab.core.thing.type.ChannelDefinitionBuilder; 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.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.CommandDescription; 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. * 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 { public class ComponentChannel {
private static final String JINJA = "JINJA"; private static final String JINJA = "JINJA";
private final ChannelUID channelUID;
private final ChannelState channelState; private final ChannelState channelState;
private final Channel channel; private final Channel channel;
private final ChannelType type; private final @Nullable StateDescription stateDescription;
private final ChannelTypeUID channelTypeUID; private final @Nullable CommandDescription commandDescription;
private final ChannelStateUpdateListener channelStateUpdateListener; private final ChannelStateUpdateListener channelStateUpdateListener;
private ComponentChannel(ChannelUID channelUID, ChannelState channelState, Channel channel, ChannelType type, private ComponentChannel(ChannelState channelState, Channel channel, @Nullable StateDescription stateDescription,
ChannelTypeUID channelTypeUID, ChannelStateUpdateListener channelStateUpdateListener) { @Nullable CommandDescription commandDescription, ChannelStateUpdateListener channelStateUpdateListener) {
super(); super();
this.channelUID = channelUID;
this.channelState = channelState; this.channelState = channelState;
this.channel = channel; this.channel = channel;
this.type = type; this.stateDescription = stateDescription;
this.channelTypeUID = channelTypeUID; this.commandDescription = commandDescription;
this.channelStateUpdateListener = channelStateUpdateListener; this.channelStateUpdateListener = channelStateUpdateListener;
} }
public ChannelUID getChannelUID() {
return channelUID;
}
public Channel getChannel() { public Channel getChannel() {
return channel; return channel;
} }
@ -92,6 +84,14 @@ public class ComponentChannel {
return channelState; return channelState;
} }
public @Nullable StateDescription getStateDescription() {
return stateDescription;
}
public @Nullable CommandDescription getCommandDescription() {
return commandDescription;
}
public CompletableFuture<@Nullable Void> stop() { public CompletableFuture<@Nullable Void> stop() {
return channelState.stop(); return channelState.stop();
} }
@ -104,16 +104,9 @@ public class ComponentChannel {
return channelState.start(connection, scheduler, timeout); return channelState.start(connection, scheduler, timeout);
} }
public void addChannelTypes(MqttChannelTypeProvider channelTypeProvider) { public ChannelDefinition channelDefinition() {
channelTypeProvider.setChannelType(channelTypeUID, type); return new ChannelDefinitionBuilder(channel.getUID().getId(),
} Objects.requireNonNull(channel.getChannelTypeUID())).withLabel(channel.getLabel()).build();
public void removeChannelTypes(MqttChannelTypeProvider channelTypeProvider) {
channelTypeProvider.removeChannelType(channelTypeUID);
}
public ChannelDefinition type() {
return new ChannelDefinitionBuilder(channelUID.getId(), channelTypeUID).build();
} }
public void resetState() { public void resetState() {
@ -123,6 +116,7 @@ public class ComponentChannel {
public static class Builder { public static class Builder {
private final AbstractComponent<?> component; private final AbstractComponent<?> component;
private final String channelID; private final String channelID;
private ChannelTypeUID channelTypeUID;
private final Value valueState; private final Value valueState;
private final String label; private final String label;
private final ChannelStateUpdateListener channelStateUpdateListener; private final ChannelStateUpdateListener channelStateUpdateListener;
@ -141,10 +135,11 @@ public class ComponentChannel {
private String format = "%s"; private String format = "%s";
public Builder(AbstractComponent<?> component, String channelID, Value valueState, String label, public Builder(AbstractComponent<?> component, String channelID, ChannelTypeUID channelTypeUID,
ChannelStateUpdateListener channelStateUpdateListener) { Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
this.component = component; this.component = component;
this.channelID = channelID; this.channelID = channelID;
this.channelTypeUID = channelTypeUID;
this.valueState = valueState; this.valueState = valueState;
this.label = label; this.label = label;
this.isAdvanced = false; this.isAdvanced = false;
@ -229,12 +224,8 @@ public class ComponentChannel {
ChannelUID channelUID; ChannelUID channelUID;
ChannelState channelState; ChannelState channelState;
Channel channel; Channel channel;
ChannelType type;
ChannelTypeUID channelTypeUID;
channelUID = component.buildChannelUID(channelID); channelUID = component.buildChannelUID(channelID);
channelTypeUID = new ChannelTypeUID(MqttBindingConstants.BINDING_ID,
channelUID.getGroupId() + "_" + channelID);
channelState = new HomeAssistantChannelState( channelState = new HomeAssistantChannelState(
ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic) ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic)
.withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(), .withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(),
@ -244,29 +235,31 @@ public class ComponentChannel {
if (!component.isEnabledByDefault()) { if (!component.isEnabledByDefault()) {
isAdvanced = true; isAdvanced = true;
} }
if (isAdvanced) {
ChannelTypeBuilder typeBuilder; channelTypeUID = new ChannelTypeUID(channelTypeUID.getBindingId(),
if (this.trigger) { channelTypeUID.getId() + "-advanced");
typeBuilder = ChannelTypeBuilder.trigger(channelTypeUID, label); }
} else {
StateDescriptionFragment stateDescription = valueState.createStateDescription(commandTopic == null) ChannelKind kind;
.build(); StateDescription stateDescription = null;
CommandDescription commandDescription = valueState.createCommandDescription().build(); CommandDescription commandDescription = null;
typeBuilder = ChannelTypeBuilder.state(channelTypeUID, label, channelState.getItemType()) if (this.trigger) {
.withStateDescriptionFragment(stateDescription).withCommandDescription(commandDescription); 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 configuration = new Configuration();
configuration.put("config", component.getChannelConfigurationJson()); configuration.put("config", component.getChannelConfigurationJson());
component.getHaID().toConfig(configuration); component.getHaID().toConfig(configuration);
channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID) channel = ChannelBuilder.create(channelUID, channelState.getItemType()).withType(channelTypeUID)
.withKind(type.getKind()).withLabel(label).withConfiguration(configuration) .withKind(kind).withLabel(label).withConfiguration(configuration)
.withAutoUpdatePolicy(autoUpdatePolicy).build(); .withAutoUpdatePolicy(autoUpdatePolicy).build();
ComponentChannel result = new ComponentChannel(channelUID, channelState, channel, type, channelTypeUID, ComponentChannel result = new ComponentChannel(channelState, channel, stateDescription, commandDescription,
channelStateUpdateListener); channelStateUpdateListener);
TransformationServiceProvider transformationProvider = component.getTransformationServiceProvider(); 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 ChannelStateUpdateListener updateListener;
private final AvailabilityTracker tracker; private final AvailabilityTracker tracker;
private final TransformationServiceProvider transformationServiceProvider; private final TransformationServiceProvider transformationServiceProvider;
private final boolean newStyleChannels;
protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>(); protected final CompletableFuture<@Nullable Void> discoverFinishedFuture = new CompletableFuture<>();
private final Gson gson; private final Gson gson;
@ -79,13 +80,14 @@ public class DiscoverComponents implements MqttMessageSubscriber {
*/ */
public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler, public DiscoverComponents(ThingUID thingUID, ScheduledExecutorService scheduler,
ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson, ChannelStateUpdateListener channelStateUpdateListener, AvailabilityTracker tracker, Gson gson,
TransformationServiceProvider transformationServiceProvider) { TransformationServiceProvider transformationServiceProvider, boolean newStyleChannels) {
this.thingUID = thingUID; this.thingUID = thingUID;
this.scheduler = scheduler; this.scheduler = scheduler;
this.updateListener = channelStateUpdateListener; this.updateListener = channelStateUpdateListener;
this.gson = gson; this.gson = gson;
this.tracker = tracker; this.tracker = tracker;
this.transformationServiceProvider = transformationServiceProvider; this.transformationServiceProvider = transformationServiceProvider;
this.newStyleChannels = newStyleChannels;
} }
@Override @Override
@ -101,7 +103,7 @@ public class DiscoverComponents implements MqttMessageSubscriber {
if (config.length() > 0) { if (config.length() > 0) {
try { try {
component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler, component = ComponentFactory.createComponent(thingUID, haID, config, updateListener, tracker, scheduler,
gson, transformationServiceProvider); gson, transformationServiceProvider, newStyleChannels);
component.setConfigSeen(); component.setConfigSeen();
logger.trace("Found HomeAssistant component {}", haID); logger.trace("Found HomeAssistant component {}", haID);

View File

@ -187,9 +187,19 @@ public class HaID {
* *
* @return group id * @return group id
*/ */
public String getGroupId(@Nullable final String uniqueId) { public String getGroupId(@Nullable final String uniqueId, boolean newStyleChannels) {
String result = uniqueId; 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 // the null test is only here so the compile knows, result is not null afterwards
if (result == null || result.isBlank()) { if (result == null || result.isBlank()) {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();

View File

@ -19,18 +19,18 @@ import java.util.Objects;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AvailabilityTracker; import org.openhab.binding.mqtt.generic.AvailabilityTracker;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; 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.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homeassistant.generic.internal.MqttBindingConstants;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration; import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory.ComponentConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration; 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.AvailabilityMode;
import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; 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.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.type.ChannelDefinition; 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.ChannelGroupType;
import org.openhab.core.thing.type.ChannelGroupTypeBuilder; import org.openhab.core.thing.type.ChannelGroupTypeBuilder;
import org.openhab.core.thing.type.ChannelGroupTypeUID; import org.openhab.core.thing.type.ChannelGroupTypeUID;
import org.openhab.core.types.CommandDescription;
import org.openhab.core.types.StateDescription;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -61,7 +64,6 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
// Component location fields // Component location fields
protected final ComponentConfiguration componentConfiguration; protected final ComponentConfiguration componentConfiguration;
protected final @Nullable ChannelGroupTypeUID channelGroupTypeUID;
protected final @Nullable ChannelGroupUID channelGroupUID; protected final @Nullable ChannelGroupUID channelGroupUID;
protected final HaID haID; protected final HaID haID;
@ -76,15 +78,28 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
protected final C channelConfiguration; protected final C channelConfiguration;
protected boolean configSeen; 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. * Creates component based on generic configuration and component configuration type.
* *
* @param componentConfiguration generic componentConfiguration with not parsed JSON config * @param componentConfiguration generic componentConfiguration with not parsed JSON config
* @param clazz target configuration type * @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.componentConfiguration = componentConfiguration;
this.singleChannelComponent = newStyleChannels && singleChannelComponent;
this.channelConfigurationJson = componentConfiguration.getConfigJSON(); this.channelConfigurationJson = componentConfiguration.getConfigJSON();
this.channelConfiguration = componentConfiguration.getConfig(clazz); this.channelConfiguration = componentConfiguration.getConfig(clazz);
@ -94,14 +109,15 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
String name = channelConfiguration.getName(); String name = channelConfiguration.getName();
if (name != null && !name.isEmpty()) { 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 = this.singleChannelComponent ? null
this.channelGroupUID = new ChannelGroupUID(componentConfiguration.getThingUID(), groupId); : new ChannelGroupUID(componentConfiguration.getThingUID(), groupId);
} else { } else {
this.channelGroupTypeUID = null; this.groupId = this.singleChannelComponent ? haID.component : "";
this.channelGroupUID = null; this.channelGroupUID = null;
} }
uniqueId = this.haID.getGroupId(channelConfiguration.getUniqueId(), false);
this.configSeen = 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, protected ComponentChannel.Builder buildChannel(String channelID, ComponentChannelType channelType,
ChannelStateUpdateListener channelStateUpdateListener) { Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) {
return new ComponentChannel.Builder(this, channelID, valueState, label, channelStateUpdateListener); if (singleChannelComponent) {
channelID = groupId;
}
return new ComponentChannel.Builder(this, channelID, channelType.getChannelTypeUID(), valueState, label,
channelStateUpdateListener);
} }
public void setConfigSeen() { 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) { public void addStateDescriptions(MqttChannelStateDescriptionProvider stateDescriptionProvider) {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID; channels.values().forEach(channel -> {
if (groupTypeUID != null) { StateDescription stateDescription = channel.getStateDescription();
channelTypeProvider.setChannelGroupType(groupTypeUID, Objects.requireNonNull(getType())); if (stateDescription != null) {
} stateDescriptionProvider.setDescription(channel.getChannel().getUID(), stateDescription);
channels.values().forEach(v -> v.addChannelTypes(channelTypeProvider)); }
} CommandDescription commandDescription = channel.getCommandDescription();
if (commandDescription != null) {
/** stateDescriptionProvider.setDescription(channel.getChannel().getUID(), commandDescription);
* 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 ChannelUID buildChannelUID(String channelID) { public ChannelUID buildChannelUID(String channelID) {
@ -211,18 +222,8 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
return new ChannelUID(componentConfiguration.getThingUID(), channelID); return new ChannelUID(componentConfiguration.getThingUID(), channelID);
} }
/** public String getGroupId() {
* Each HomeAssistant component corresponds to a Channel Group Type. return groupId;
*/
public @Nullable ChannelGroupTypeUID getGroupTypeUID() {
return channelGroupTypeUID;
}
/**
* The unique id of this component.
*/
public @Nullable ChannelGroupUID getGroupUID() {
return channelGroupUID;
} }
/** /**
@ -270,19 +271,27 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
/** /**
* Return the channel group type. * Return the channel group type.
*/ */
public @Nullable ChannelGroupType getType() { public @Nullable ChannelGroupType getChannelGroupType(String prefix) {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID; if (channelGroupUID == null) {
if (groupTypeUID == null) {
return null; return null;
} }
final List<ChannelDefinition> channelDefinitions = channels.values().stream().map(ComponentChannel::type) return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), getName())
.collect(Collectors.toList()); .withChannelDefinitions(getAllChannelDefinitions()).build();
return ChannelGroupTypeBuilder.instance(groupTypeUID, getName()).withChannelDefinitions(channelDefinitions)
.build();
} }
public List<ChannelDefinition> getChannels() { public List<ChannelDefinition> getChannelDefinitions() {
return channels.values().stream().map(ComponentChannel::type).collect(Collectors.toList()); 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. * Return the channel group definition for this component.
*/ */
public @Nullable ChannelGroupDefinition getGroupDefinition() { public @Nullable ChannelGroupDefinition getGroupDefinition(String prefix) {
ChannelGroupTypeUID groupTypeUID = channelGroupTypeUID; if (channelGroupUID == null) {
if (groupTypeUID == null) {
return 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() { public HaID getHaID() {
@ -324,4 +336,12 @@ public abstract class AbstractComponent<C extends AbstractChannelConfiguration>
public Gson getGson() { public Gson getGson() {
return componentConfiguration.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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.HSBType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
@ -31,11 +32,12 @@ abstract class AbstractRawSchemaLight extends Light {
protected ComponentChannel rawChannel; protected ComponentChannel rawChannel;
public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder) { public AbstractRawSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder); super(builder, newStyleChannels);
hiddenChannels.add(rawChannel = buildChannel(RAW_CHANNEL_ID, new TextValue(), "Raw state", this) hiddenChannels.add(rawChannel = buildChannel(RAW_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(),
.stateTopic(channelConfiguration.stateTopic).commandTopic(channelConfiguration.commandTopic, "Raw state", this).stateTopic(channelConfiguration.stateTopic)
channelConfiguration.isRetain(), channelConfiguration.getQos()) .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos())
.build(false)); .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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
@ -68,28 +69,30 @@ public class AlarmControlPanel extends AbstractComponent<AlarmControlPanel.Chann
protected String payloadArmAway = "ARM_AWAY"; protected String payloadArmAway = "ARM_AWAY";
} }
public AlarmControlPanel(ComponentFactory.ComponentConfiguration componentConfiguration) { public AlarmControlPanel(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
final String[] stateEnum = { channelConfiguration.stateDisarmed, channelConfiguration.stateArmedHome, final String[] stateEnum = { channelConfiguration.stateDisarmed, channelConfiguration.stateArmedHome,
channelConfiguration.stateArmedAway, channelConfiguration.statePending, channelConfiguration.stateArmedAway, channelConfiguration.statePending,
channelConfiguration.stateTriggered }; 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())// .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.build(); .build();
String commandTopic = channelConfiguration.commandTopic; String commandTopic = channelConfiguration.commandTopic;
if (commandTopic != null) { if (commandTopic != null) {
buildChannel(SWITCH_DISARM_CHANNEL_ID, new TextValue(new String[] { channelConfiguration.payloadDisarm }), buildChannel(SWITCH_DISARM_CHANNEL_ID, ComponentChannelType.STRING,
getName(), componentConfiguration.getUpdateListener()) new TextValue(new String[] { channelConfiguration.payloadDisarm }), getName(),
componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build(); .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(), new TextValue(new String[] { channelConfiguration.payloadArmHome }), getName(),
componentConfiguration.getUpdateListener()) componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build(); .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(), new TextValue(new String[] { channelConfiguration.payloadArmAway }), getName(),
componentConfiguration.getUpdateListener()) componentConfiguration.getUpdateListener())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()).build(); .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.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue; import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.Value; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener; import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
import org.openhab.binding.mqtt.homeassistant.internal.listener.OffDelayUpdateStateListener; 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; protected @Nullable List<String> jsonAttributes;
} }
public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration) { public BinarySensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.thing.type.AutoUpdatePolicy;
@ -46,12 +47,13 @@ public class Button extends AbstractComponent<Button.ChannelConfiguration> {
protected String payloadPress = "PRESS"; protected String payloadPress = "PRESS";
} }
public Button(ComponentFactory.ComponentConfiguration componentConfiguration) { public Button(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadPress }); 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(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mqtt.generic.values.ImageValue; 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; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
/** /**
@ -38,12 +39,12 @@ public class Camera extends AbstractComponent<Camera.ChannelConfiguration> {
protected String topic = ""; protected String topic = "";
} }
public Camera(ComponentFactory.ComponentConfiguration componentConfiguration) { public Camera(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
ImageValue value = new ImageValue(); ImageValue value = new ImageValue();
buildChannel(CAMERA_CHANNEL_ID, value, getName(), componentConfiguration.getUpdateListener()) buildChannel(CAMERA_CHANNEL_ID, ComponentChannelType.IMAGE, value, getName(),
.stateTopic(channelConfiguration.topic).build(); 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.TextValue;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits; import org.openhab.core.library.unit.ImperialUnits;
@ -210,83 +211,88 @@ public class Climate extends AbstractComponent<Climate.ChannelConfiguration> {
protected Boolean sendIfOff = true; protected Boolean sendIfOff = true;
} }
public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) { public Climate(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision BigDecimal precision = channelConfiguration.precision != null ? channelConfiguration.precision
: channelConfiguration.temperatureUnit.getDefaultPrecision(); : channelConfiguration.temperatureUnit.getDefaultPrecision();
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener(); 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, new TextValue(ACTION_MODES.toArray(new String[0])), updateListener, null, null,
channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null); channelConfiguration.actionTemplate, channelConfiguration.actionTopic, null);
final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null final Predicate<Command> commandFilter = channelConfiguration.sendIfOff ? null
: getCommandFilter(actionChannel); : getCommandFilter(actionChannel);
buildOptionalChannel(AUX_CH_ID, new OnOffValue(), updateListener, null, channelConfiguration.auxCommandTopic, buildOptionalChannel(AUX_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(), updateListener, null,
channelConfiguration.auxStateTemplate, channelConfiguration.auxStateTopic, commandFilter); 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.awayModeCommandTopic, channelConfiguration.awayModeStateTemplate,
channelConfiguration.awayModeStateTopic, commandFilter); channelConfiguration.awayModeStateTopic, commandFilter);
buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID, buildOptionalChannel(CURRENT_TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener, new NumberValue(null, null, precision, channelConfiguration.temperatureUnit.getUnit()), updateListener,
null, null, channelConfiguration.currentTemperatureTemplate, null, null, channelConfiguration.currentTemperatureTemplate,
channelConfiguration.currentTemperatureTopic, commandFilter); channelConfiguration.currentTemperatureTopic, commandFilter);
buildOptionalChannel(FAN_MODE_CH_ID, new TextValue(channelConfiguration.fanModes.toArray(new String[0])), buildOptionalChannel(FAN_MODE_CH_ID, ComponentChannelType.STRING,
updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic, new TextValue(channelConfiguration.fanModes.toArray(new String[0])), updateListener,
channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic,
channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter); channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter);
List<String> holdModes = channelConfiguration.holdModes; List<String> holdModes = channelConfiguration.holdModes;
if (holdModes != null && !holdModes.isEmpty()) { 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.holdCommandTemplate, channelConfiguration.holdCommandTopic,
channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter); channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter);
} }
buildOptionalChannel(MODE_CH_ID, new TextValue(channelConfiguration.modes.toArray(new String[0])), buildOptionalChannel(MODE_CH_ID, ComponentChannelType.STRING,
updateListener, channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic, new TextValue(channelConfiguration.modes.toArray(new String[0])), updateListener,
channelConfiguration.modeCommandTemplate, channelConfiguration.modeCommandTopic,
channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter); channelConfiguration.modeStateTemplate, channelConfiguration.modeStateTopic, commandFilter);
buildOptionalChannel(SWING_CH_ID, new TextValue(channelConfiguration.swingModes.toArray(new String[0])), buildOptionalChannel(SWING_CH_ID, ComponentChannelType.STRING,
updateListener, channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic, new TextValue(channelConfiguration.swingModes.toArray(new String[0])), updateListener,
channelConfiguration.swingCommandTemplate, channelConfiguration.swingCommandTopic,
channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter); channelConfiguration.swingStateTemplate, channelConfiguration.swingStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_CH_ID, buildOptionalChannel(TEMPERATURE_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureCommandTemplate, updateListener, channelConfiguration.temperatureCommandTemplate,
channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate, channelConfiguration.temperatureCommandTopic, channelConfiguration.temperatureStateTemplate,
channelConfiguration.temperatureStateTopic, commandFilter); channelConfiguration.temperatureStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_HIGH_CH_ID, buildOptionalChannel(TEMPERATURE_HIGH_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureHighCommandTemplate, updateListener, channelConfiguration.temperatureHighCommandTemplate,
channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate, channelConfiguration.temperatureHighCommandTopic, channelConfiguration.temperatureHighStateTemplate,
channelConfiguration.temperatureHighStateTopic, commandFilter); channelConfiguration.temperatureHighStateTopic, commandFilter);
buildOptionalChannel(TEMPERATURE_LOW_CH_ID, buildOptionalChannel(TEMPERATURE_LOW_CH_ID, ComponentChannelType.NUMBER,
new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp, new NumberValue(channelConfiguration.minTemp, channelConfiguration.maxTemp,
channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()), channelConfiguration.tempStep, channelConfiguration.temperatureUnit.getUnit()),
updateListener, channelConfiguration.temperatureLowCommandTemplate, updateListener, channelConfiguration.temperatureLowCommandTemplate,
channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate, channelConfiguration.temperatureLowCommandTopic, channelConfiguration.temperatureLowStateTemplate,
channelConfiguration.temperatureLowStateTopic, commandFilter); 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); channelConfiguration.powerCommandTopic, null, null, null);
} }
@Nullable @Nullable
private ComponentChannel buildOptionalChannel(String channelId, Value valueState, private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate, ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
@Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic, @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic,
@Nullable Predicate<Command> commandFilter) { @Nullable Predicate<Command> commandFilter) {
if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) { 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()) .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(), .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate) commandTemplate)

View File

@ -48,45 +48,46 @@ public class ComponentFactory {
*/ */
public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON, public static AbstractComponent<?> createComponent(ThingUID thingUID, HaID haID, String channelConfigurationJSON,
ChannelStateUpdateListener updateListener, AvailabilityTracker tracker, ScheduledExecutorService scheduler, 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, ComponentConfiguration componentConfiguration = new ComponentConfiguration(thingUID, haID,
channelConfigurationJSON, gson, updateListener, tracker, scheduler) channelConfigurationJSON, gson, updateListener, tracker, scheduler)
.transformationProvider(transformationServiceProvider); .transformationProvider(transformationServiceProvider);
switch (haID.component) { switch (haID.component) {
case "alarm_control_panel": case "alarm_control_panel":
return new AlarmControlPanel(componentConfiguration); return new AlarmControlPanel(componentConfiguration, newStyleChannels);
case "binary_sensor": case "binary_sensor":
return new BinarySensor(componentConfiguration); return new BinarySensor(componentConfiguration, newStyleChannels);
case "button": case "button":
return new Button(componentConfiguration); return new Button(componentConfiguration, newStyleChannels);
case "camera": case "camera":
return new Camera(componentConfiguration); return new Camera(componentConfiguration, newStyleChannels);
case "cover": case "cover":
return new Cover(componentConfiguration); return new Cover(componentConfiguration, newStyleChannels);
case "fan": case "fan":
return new Fan(componentConfiguration); return new Fan(componentConfiguration, newStyleChannels);
case "climate": case "climate":
return new Climate(componentConfiguration); return new Climate(componentConfiguration, newStyleChannels);
case "device_automation": case "device_automation":
return new DeviceTrigger(componentConfiguration); return new DeviceTrigger(componentConfiguration, newStyleChannels);
case "light": case "light":
return Light.create(componentConfiguration); return Light.create(componentConfiguration, newStyleChannels);
case "lock": case "lock":
return new Lock(componentConfiguration); return new Lock(componentConfiguration, newStyleChannels);
case "number": case "number":
return new Number(componentConfiguration); return new Number(componentConfiguration, newStyleChannels);
case "scene": case "scene":
return new Scene(componentConfiguration); return new Scene(componentConfiguration, newStyleChannels);
case "select": case "select":
return new Select(componentConfiguration); return new Select(componentConfiguration, newStyleChannels);
case "sensor": case "sensor":
return new Sensor(componentConfiguration); return new Sensor(componentConfiguration, newStyleChannels);
case "switch": case "switch":
return new Switch(componentConfiguration); return new Switch(componentConfiguration, newStyleChannels);
case "update": case "update":
return new Update(componentConfiguration); return new Update(componentConfiguration, newStyleChannels);
case "vacuum": case "vacuum":
return new Vacuum(componentConfiguration); return new Vacuum(componentConfiguration, newStyleChannels);
default: default:
throw new UnsupportedComponentException("Component '" + haID + "' is unsupported!"); 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.RollershutterValue;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.StopMoveType; import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
@ -84,8 +85,8 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
@Nullable @Nullable
ComponentChannel stateChannel = null; ComponentChannel stateChannel = null;
public Cover(ComponentFactory.ComponentConfiguration componentConfiguration) { public Cover(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
String stateTopic = channelConfiguration.stateTopic; String stateTopic = channelConfiguration.stateTopic;
@ -95,13 +96,13 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
TextValue value = new TextValue(new String[] { channelConfiguration.stateClosed, TextValue value = new TextValue(new String[] { channelConfiguration.stateClosed,
channelConfiguration.stateClosing, channelConfiguration.stateOpen, channelConfiguration.stateClosing, channelConfiguration.stateOpen,
channelConfiguration.stateOpening, channelConfiguration.stateStopped }); channelConfiguration.stateOpening, channelConfiguration.stateStopped });
buildChannel(STATE_CHANNEL_ID, value, "State", componentConfiguration.getUpdateListener()) buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING, value, "State",
.stateTopic(stateTopic).isAdvanced(true).build(); componentConfiguration.getUpdateListener()).stateTopic(stateTopic).isAdvanced(true).build();
} }
if (channelConfiguration.commandTopic != null) { if (channelConfiguration.commandTopic != null) {
hiddenChannels.add(stateChannel = buildChannel(STATE_CHANNEL_ID, new TextValue(), "State", hiddenChannels.add(stateChannel = buildChannel(STATE_CHANNEL_ID, ComponentChannelType.STRING,
componentConfiguration.getUpdateListener()) new TextValue(), "State", componentConfiguration.getUpdateListener())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.build(false)); .build(false));
@ -132,8 +133,8 @@ public class Cover extends AbstractComponent<Cover.ChannelConfiguration> {
channelConfiguration.payloadClose, channelConfiguration.payloadStop, channelConfiguration.stateOpen, channelConfiguration.payloadClose, channelConfiguration.payloadStop, channelConfiguration.stateOpen,
channelConfiguration.stateClosed, inverted, channelConfiguration.setPositionTopic == null); channelConfiguration.stateClosed, inverted, channelConfiguration.setPositionTopic == null);
buildChannel(COVER_CHANNEL_ID, value, "Cover", componentConfiguration.getUpdateListener()) buildChannel(COVER_CHANNEL_ID, ComponentChannelType.ROLLERSHUTTER, value, "Cover",
.stateTopic(rollershutterStateTopic, stateTemplate) componentConfiguration.getUpdateListener()).stateTopic(rollershutterStateTopic, stateTemplate)
.commandTopic(rollershutterCommandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos()) .commandTopic(rollershutterCommandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(command -> { .commandFilter(command -> {
if (stateChannel == null) { 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.ColorValue;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.DecimalType;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
@ -53,14 +54,15 @@ public class DefaultSchemaLight extends Light {
protected @Nullable ComponentChannel rgbChannel; protected @Nullable ComponentChannel rgbChannel;
protected @Nullable ComponentChannel xyChannel; protected @Nullable ComponentChannel xyChannel;
public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) { public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder); super(builder, newStyleChannels);
} }
@Override @Override
protected void buildChannels() { protected void buildChannels() {
ComponentChannel localOnOffChannel; 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) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -69,8 +71,8 @@ public class DefaultSchemaLight extends Light {
@Nullable @Nullable
ComponentChannel localBrightnessChannel = null; ComponentChannel localBrightnessChannel = null;
if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) { if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) {
localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID,
"Brightness", this) ComponentChannelType.DIMMER, brightnessValue, "Brightness", this)
.stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate) .stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate)
.commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -78,20 +80,22 @@ public class DefaultSchemaLight extends Light {
} }
if (channelConfiguration.whiteCommandTopic != null) { 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(), .commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.isAdvanced(true).build(); .isAdvanced(true).build();
} }
if (channelConfiguration.colorModeStateTopic != null) { 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) .stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
.build(); .build();
} }
if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) { 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) .stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
.commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -100,7 +104,8 @@ public class DefaultSchemaLight extends Light {
if (effectValue != null if (effectValue != null
&& (channelConfiguration.effectStateTopic != null || channelConfiguration.effectCommandTopic != 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) .stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
.commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -109,8 +114,8 @@ public class DefaultSchemaLight extends Light {
if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) { if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
hasColorChannel = true; hasColorChannel = true;
hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, new ColorValue(ColorMode.RGB, null, null, 100), hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, ComponentChannelType.COLOR,
"RGB state", this) new ColorValue(ColorMode.RGB, null, null, 100), "RGB state", this)
.stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate) .stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
.commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -119,35 +124,38 @@ public class DefaultSchemaLight extends Light {
if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) { if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) {
hasColorChannel = true; hasColorChannel = true;
hiddenChannels.add(buildChannel(RGBW_CHANNEL_ID, new TextValue(), "RGBW state", this) hiddenChannels
.stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate) .add(buildChannel(RGBW_CHANNEL_ID, ComponentChannelType.STRING, new TextValue(), "RGBW state", this)
.commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(), .stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate)
channelConfiguration.getQos()) .commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(),
.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(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.build(false)); .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) { if (channelConfiguration.hsStateTopic != null || channelConfiguration.hsCommandTopic != null) {
hasColorChannel = true; 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) .stateTopic(channelConfiguration.hsStateTopic, channelConfiguration.hsValueTemplate)
.commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -159,7 +167,7 @@ public class DefaultSchemaLight extends Light {
if (localBrightnessChannel != null) { if (localBrightnessChannel != null) {
hiddenChannels.add(localBrightnessChannel); 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()) .commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
.commandFilter(this::handleColorCommand).build(); .commandFilter(this::handleColorCommand).build();
} else if (localBrightnessChannel != null) { } 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -44,8 +45,8 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
protected @Nullable String payload; protected @Nullable String payload;
} }
public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration) { public DeviceTrigger(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
if (!"trigger".equals(channelConfiguration.automationType)) { if (!"trigger".equals(channelConfiguration.automationType)) {
throw new ConfigurationException("Component:DeviceTrigger must have automation_type 'trigger'"); throw new ConfigurationException("Component:DeviceTrigger must have automation_type 'trigger'");
@ -65,7 +66,8 @@ public class DeviceTrigger extends AbstractComponent<DeviceTrigger.ChannelConfig
value = new TextValue(); 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(); .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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue; 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.config.dto.AbstractChannelConfiguration;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
@ -50,11 +51,12 @@ public class Fan extends AbstractComponent<Fan.ChannelConfiguration> {
protected String payloadOff = "OFF"; protected String payloadOff = "OFF";
} }
public Fan(ComponentFactory.ComponentConfiguration componentConfiguration) { public Fan(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
OnOffValue value = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate) channelConfiguration.getQos(), channelConfiguration.commandTemplate)

View File

@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.HSBType;
@ -73,8 +74,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
TextValue colorModeValue; TextValue colorModeValue;
public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder) { public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder); super(builder, newStyleChannels);
colorModeValue = new TextValue(); colorModeValue = new TextValue();
} }
@ -84,7 +85,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
if (supportedColorModes != null && supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) { if (supportedColorModes != null && supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
colorModeValue = new TextValue( colorModeValue = new TextValue(
supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new)); 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) { if (channelConfiguration.colorMode) {
@ -98,26 +100,27 @@ public class JSONSchemaLight extends AbstractRawSchemaLight {
} }
if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) { if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this) buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command)) this).commandTopic(DUMMY_TOPIC, true, 1)
.build(); .commandFilter(command -> handleColorTempCommand(command)).build();
} }
} }
if (hasColorChannel) { if (hasColorChannel) {
buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this).commandTopic(DUMMY_TOPIC, true, 1) buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
.commandFilter(this::handleCommand).build(); .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} else if (channelConfiguration.brightness) { } else if (channelConfiguration.brightness) {
brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, "Brightness", this) brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build(); "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} else { } else {
onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this) onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build(); this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
} }
if (effectValue != null) { if (effectValue != null) {
buildChannel(EFFECT_CHANNEL_ID, Objects.requireNonNull(effectValue), "Lighting Effect", this) buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
.commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleEffectCommand(command)).build(); "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; 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; String schema = builder.getConfig(ChannelConfiguration.class).schema;
switch (schema) { switch (schema) {
case DEFAULT_SCHEMA: case DEFAULT_SCHEMA:
return new DefaultSchemaLight(builder); return new DefaultSchemaLight(builder, newStyleChannels);
case JSON_SCHEMA: case JSON_SCHEMA:
return new JSONSchemaLight(builder); return new JSONSchemaLight(builder, newStyleChannels);
default: default:
throw new UnsupportedComponentException( throw new UnsupportedComponentException(
"Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!"); "Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!");
} }
} }
protected Light(ComponentFactory.ComponentConfiguration builder) { protected Light(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
super(builder, ChannelConfiguration.class); super(builder, ChannelConfiguration.class, newStyleChannels);
this.channelStateUpdateListener = builder.getUpdateListener(); this.channelStateUpdateListener = builder.getUpdateListener();
@Nullable @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.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.OnOffValue; import org.openhab.binding.mqtt.generic.values.OnOffValue;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
@ -72,8 +73,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
private OnOffValue lockValue; private OnOffValue lockValue;
private TextValue stateValue; private TextValue stateValue;
public Lock(ComponentFactory.ComponentConfiguration componentConfiguration) { public Lock(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
this.optimistic = channelConfiguration.optimistic || channelConfiguration.stateTopic.isBlank(); this.optimistic = channelConfiguration.optimistic || channelConfiguration.stateTopic.isBlank();
@ -82,7 +83,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
channelConfiguration.stateUnlocking, channelConfiguration.stateJammed }, channelConfiguration.stateUnlocking, channelConfiguration.stateJammed },
channelConfiguration.payloadLock, channelConfiguration.payloadUnlock); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
@ -103,7 +105,8 @@ public class Lock extends AbstractComponent<Lock.ChannelConfiguration> {
stateValue = new TextValue(new String[] { channelConfiguration.stateJammed, channelConfiguration.stateLocked, stateValue = new TextValue(new String[] { channelConfiguration.stateJammed, channelConfiguration.stateLocked,
channelConfiguration.stateLocking, channelConfiguration.stateUnlocked, channelConfiguration.stateLocking, channelConfiguration.stateUnlocked,
channelConfiguration.stateUnlocking }, commands); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())

View File

@ -17,6 +17,7 @@ import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.NumberValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.types.util.UnitUtils; import org.openhab.core.types.util.UnitUtils;
@ -69,8 +70,8 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate; protected @Nullable String jsonAttributesTemplate;
} }
public Number(ComponentFactory.ComponentConfiguration componentConfiguration) { public Number(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -82,7 +83,8 @@ public class Number extends AbstractComponent<Number.ChannelConfiguration> {
NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max, NumberValue value = new NumberValue(channelConfiguration.min, channelConfiguration.max,
channelConfiguration.step, UnitUtils.parseUnit(channelConfiguration.unitOfMeasurement)); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate) 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.core.thing.type.AutoUpdatePolicy; import org.openhab.core.thing.type.AutoUpdatePolicy;
@ -44,12 +45,13 @@ public class Scene extends AbstractComponent<Scene.ChannelConfiguration> {
protected String payloadOn = "ON"; protected String payloadOn = "ON";
} }
public Scene(ComponentFactory.ComponentConfiguration componentConfiguration) { public Scene(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
TextValue value = new TextValue(new String[] { channelConfiguration.payloadOn }); 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(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) channelConfiguration.getQos())
.withAutoUpdatePolicy(AutoUpdatePolicy.VETO).build(); .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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.TextValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -54,8 +55,8 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate; protected @Nullable String jsonAttributesTemplate;
} }
public Select(ComponentFactory.ComponentConfiguration componentConfiguration) { public Select(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -66,7 +67,8 @@ public class Select extends AbstractComponent<Select.ChannelConfiguration> {
TextValue value = new TextValue(channelConfiguration.options); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos(), channelConfiguration.commandTemplate) 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.NumberValue;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.generic.values.Value; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener; import org.openhab.binding.mqtt.homeassistant.internal.listener.ExpireUpdateStateListener;
import org.openhab.core.types.util.UnitUtils; import org.openhab.core.types.util.UnitUtils;
@ -67,28 +68,32 @@ public class Sensor extends AbstractComponent<Sensor.ChannelConfiguration> {
protected @Nullable List<String> jsonAttributes; protected @Nullable List<String> jsonAttributes;
} }
public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration) { public Sensor(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
Value value; Value value;
String uom = channelConfiguration.unitOfMeasurement; String uom = channelConfiguration.unitOfMeasurement;
String sc = channelConfiguration.stateClass; String sc = channelConfiguration.stateClass;
ComponentChannelType type;
if (uom != null && !uom.isBlank()) { if (uom != null && !uom.isBlank()) {
value = new NumberValue(null, null, null, UnitUtils.parseUnit(uom)); value = new NumberValue(null, null, null, UnitUtils.parseUnit(uom));
type = ComponentChannelType.NUMBER;
} else if (sc != null && !sc.isBlank()) { } else if (sc != null && !sc.isBlank()) {
// see state_class at https://developers.home-assistant.io/docs/core/entity/sensor#properties // see state_class at https://developers.home-assistant.io/docs/core/entity/sensor#properties
// > If not None, the sensor is assumed to be numerical // > If not None, the sensor is assumed to be numerical
value = new NumberValue(null, null, null, null); value = new NumberValue(null, null, null, null);
type = ComponentChannelType.NUMBER;
} else { } else {
value = new TextValue(); value = new TextValue();
type = ComponentChannelType.STRING;
} }
String icon = channelConfiguration.getIcon(); String icon = channelConfiguration.getIcon();
boolean trigger = TRIGGER_ICONS.matcher(icon).matches(); 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())// .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())//
.trigger(trigger).build(); .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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.values.OnOffValue; 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.config.dto.AbstractChannelConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException; import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
@ -59,8 +60,8 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
protected @Nullable String jsonAttributesTemplate; protected @Nullable String jsonAttributesTemplate;
} }
public Switch(ComponentFactory.ComponentConfiguration componentConfiguration) { public Switch(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels, true);
boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic boolean optimistic = channelConfiguration.optimistic != null ? channelConfiguration.optimistic
: channelConfiguration.stateTopic.isBlank(); : channelConfiguration.stateTopic.isBlank();
@ -72,7 +73,8 @@ public class Switch extends AbstractComponent<Switch.ChannelConfiguration> {
OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff, OnOffValue value = new OnOffValue(channelConfiguration.stateOn, channelConfiguration.stateOff,
channelConfiguration.payloadOn, channelConfiguration.payloadOff); 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()) .stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate())
.commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
channelConfiguration.getQos()) 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.ChannelStateUpdateListener;
import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.generic.values.TextValue;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.types.StringType; 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 ReleaseState state = new ReleaseState();
private @Nullable ReleaseStateListener listener = null; private @Nullable ReleaseStateListener listener = null;
public Update(ComponentFactory.ComponentConfiguration componentConfiguration) { public Update(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
TextValue value = new TextValue(); TextValue value = new TextValue();
String commandTopic = channelConfiguration.commandTopic; String commandTopic = channelConfiguration.commandTopic;
String payloadInstall = channelConfiguration.payloadInstall; 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) { if (channelConfiguration.stateTopic != null) {
builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate()); builder.stateTopic(channelConfiguration.stateTopic, channelConfiguration.getValueTemplate());
} }
@ -162,7 +163,8 @@ public class Update extends AbstractComponent<Update.ChannelConfiguration> imple
if (channelConfiguration.latestVersionTopic != null) { if (channelConfiguration.latestVersionTopic != null) {
value = new TextValue(); 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) .stateTopic(channelConfiguration.latestVersionTopic, channelConfiguration.latestVersionTemplate)
.build(false); .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.TextValue;
import org.openhab.binding.mqtt.generic.values.Value; import org.openhab.binding.mqtt.generic.values.Value;
import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; 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.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -188,8 +189,8 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
* *
* @param componentConfiguration generic componentConfiguration with not parsed JSON config * @param componentConfiguration generic componentConfiguration with not parsed JSON config
*/ */
public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration) { public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration, boolean newStyleChannels) {
super(componentConfiguration, ChannelConfiguration.class); super(componentConfiguration, ChannelConfiguration.class, newStyleChannels);
final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener(); final ChannelStateUpdateListener updateListener = componentConfiguration.getUpdateListener();
final var allowedSupportedFeatures = channelConfiguration.schema == Schema.LEGACY ? LEGACY_SUPPORTED_FEATURES 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); addPayloadToList(deviceSupportedFeatures, FEATURE_START, channelConfiguration.payloadStart, commands);
} }
buildOptionalChannel(COMMAND_CH_ID, new TextValue(commands.toArray(new String[0])), updateListener, null, buildOptionalChannel(COMMAND_CH_ID, ComponentChannelType.STRING, new TextValue(commands.toArray(new String[0])),
channelConfiguration.commandTopic, null, null); updateListener, null, channelConfiguration.commandTopic, null, null);
final var fanSpeedList = channelConfiguration.fanSpeedList; final var fanSpeedList = channelConfiguration.fanSpeedList;
if (deviceSupportedFeatures.contains(FEATURE_FAN_SPEED) && fanSpeedList != null && !fanSpeedList.isEmpty()) { 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])); var fanSpeedValue = new TextValue(fanSpeedList.toArray(new String[0]));
if (channelConfiguration.schema == Schema.LEGACY) { 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.setFanSpeedTopic, channelConfiguration.fanSpeedTemplate,
channelConfiguration.fanSpeedTopic); channelConfiguration.fanSpeedTopic);
} else if (deviceSupportedFeatures.contains(FEATURE_STATUS)) { } 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.setFanSpeedTopic, "{{ value_json.fan_speed }}",
channelConfiguration.stateTopic); channelConfiguration.stateTopic);
} else { } else {
LOGGER.info("Status feature is disabled, unable to get fan speed."); 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); channelConfiguration.setFanSpeedTopic, null, null);
} }
} }
if (deviceSupportedFeatures.contains(FEATURE_SEND_COMMAND)) { if (deviceSupportedFeatures.contains(FEATURE_SEND_COMMAND)) {
buildOptionalChannel(CUSTOM_COMMAND_CH_ID, new TextValue(), updateListener, null, buildOptionalChannel(CUSTOM_COMMAND_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener,
channelConfiguration.sendCommandTopic, null, null); null, channelConfiguration.sendCommandTopic, null, null);
} }
if (channelConfiguration.schema == Schema.LEGACY) { if (channelConfiguration.schema == Schema.LEGACY) {
// I assume, that if these topics defined in config, then we don't need to check features // 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), new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
updateListener, null, null, channelConfiguration.batteryLevelTemplate, updateListener, null, null, channelConfiguration.batteryLevelTemplate,
channelConfiguration.batteryLevelTopic); channelConfiguration.batteryLevelTopic);
buildOptionalChannel(CHARGING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null, buildOptionalChannel(CHARGING_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(TRUE, FALSE),
channelConfiguration.chargingTemplate, channelConfiguration.chargingTopic); updateListener, null, null, channelConfiguration.chargingTemplate,
buildOptionalChannel(CLEANING_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null, channelConfiguration.chargingTopic);
channelConfiguration.cleaningTemplate, channelConfiguration.cleaningTopic); buildOptionalChannel(CLEANING_CH_ID, ComponentChannelType.SWITCH, new OnOffValue(TRUE, FALSE),
buildOptionalChannel(DOCKED_CH_ID, new OnOffValue(TRUE, FALSE), updateListener, null, null, updateListener, null, null, channelConfiguration.cleaningTemplate,
channelConfiguration.dockedTemplate, channelConfiguration.dockedTopic); channelConfiguration.cleaningTopic);
buildOptionalChannel(ERROR_CH_ID, new TextValue(), updateListener, null, null, 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); channelConfiguration.errorTemplate, channelConfiguration.errorTopic);
} else { } else {
if (deviceSupportedFeatures.contains(FEATURE_STATUS)) { if (deviceSupportedFeatures.contains(FEATURE_STATUS)) {
// state key is mandatory // 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, new TextValue(new String[] { STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, STATE_IDLE,
STATE_RETURNING, STATE_ERROR }), STATE_RETURNING, STATE_ERROR }),
updateListener, null, null, "{{ value_json.state }}", channelConfiguration.stateTopic); updateListener, null, null, "{{ value_json.state }}", channelConfiguration.stateTopic);
if (deviceSupportedFeatures.contains(FEATURE_BATTERY)) { 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), new PercentageValue(BigDecimal.ZERO, BigDecimal.valueOf(100), BigDecimal.ONE, null, null),
updateListener, null, null, "{{ value_json.battery_level }}", updateListener, null, null, "{{ value_json.battery_level }}",
channelConfiguration.stateTopic); channelConfiguration.stateTopic);
@ -285,16 +288,16 @@ public class Vacuum extends AbstractComponent<Vacuum.ChannelConfiguration> {
} }
} }
buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, new TextValue(), updateListener, null, null, buildOptionalChannel(JSON_ATTRIBUTES_CH_ID, ComponentChannelType.STRING, new TextValue(), updateListener, null,
channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic); null, channelConfiguration.jsonAttributesTemplate, channelConfiguration.jsonAttributesTopic);
} }
@Nullable @Nullable
private ComponentChannel buildOptionalChannel(String channelId, Value valueState, private ComponentChannel buildOptionalChannel(String channelId, ComponentChannelType channelType, Value valueState,
ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate, ChannelStateUpdateListener channelStateUpdateListener, @Nullable String commandTemplate,
@Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic) { @Nullable String commandTopic, @Nullable String stateTemplate, @Nullable String stateTopic) {
if ((commandTopic != null && !commandTopic.isBlank()) || (stateTopic != null && !stateTopic.isBlank())) { 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()) .stateTopic(stateTopic, stateTemplate, channelConfiguration.getValueTemplate())
.commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(), .commandTopic(commandTopic, channelConfiguration.isRetain(), channelConfiguration.getQos(),
commandTemplate) commandTemplate)

View File

@ -145,7 +145,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
@Override @Override
public Set<ThingTypeUID> getSupportedThingTypes() { 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); .fromString(new String(payload, StandardCharsets.UTF_8), gson);
final String thingID = config.getThingId(haID.objectID); final String thingID = config.getThingId(haID.objectID);
final ThingUID thingUID = new ThingUID(MqttBindingConstants.HOMEASSISTANT_MQTT_THING, connectionBridge,
final ThingTypeUID typeID = new ThingTypeUID(MqttBindingConstants.BINDING_ID, thingID);
MqttBindingConstants.HOMEASSISTANT_MQTT_THING.getId() + "_" + thingID);
final ThingUID thingUID = new ThingUID(typeID, connectionBridge, thingID);
thingIDPerTopic.put(topic, thingUID); thingIDPerTopic.put(topic, thingUID);
@ -241,6 +238,7 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
properties = handlerConfig.appendToProperties(properties); properties = handlerConfig.appendToProperties(properties);
properties = config.appendToProperties(properties); properties = config.appendToProperties(properties);
properties.put("deviceId", thingID); properties.put("deviceId", thingID);
properties.put("newStyleChannels", "true");
// Because we need the new properties map with the updated "components" list // Because we need the new properties map with the updated "components" list
results.put(thingUID.getAsString(), results.put(thingUID.getAsString(),
@ -282,10 +280,6 @@ public class HomeAssistantDiscovery extends AbstractMQTTDiscovery {
results.clear(); results.clear();
componentsPerThingID.clear(); componentsPerThingID.clear();
for (DiscoveryResult result : localResults) { 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); thingDiscovered(result);
} }
} }

View File

@ -13,7 +13,6 @@
package org.openhab.binding.mqtt.homeassistant.internal.handler; package org.openhab.binding.mqtt.homeassistant.internal.handler;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -24,12 +23,12 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler; import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
import org.openhab.binding.mqtt.generic.ChannelState; 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.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.generic.tools.DelayedBatchProcessing; 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.config.core.validation.ConfigValidationException;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; 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.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelDefinition; import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelGroupDefinition;
import org.openhab.core.thing.type.ThingType;
import org.openhab.core.thing.util.ThingHelper;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -86,13 +81,16 @@ import com.google.gson.GsonBuilder;
public class HomeAssistantThingHandler extends AbstractMQTTThingHandler public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> { implements ComponentDiscovered, Consumer<List<AbstractComponent<?>>> {
public static final String AVAILABILITY_CHANNEL = "availability"; public static final String AVAILABILITY_CHANNEL = "availability";
private static final Comparator<Channel> CHANNEL_COMPARATOR_BY_UID = Comparator private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
.comparing(channel -> channel.getUID().toString()); .comparing((AbstractComponent<?> component) -> component.hasGroup())
.thenComparing(AbstractComponent::getName);
private static final URI UPDATABLE_CONFIG_DESCRIPTION_URI = URI.create("thing-type:mqtt:homeassistant-updatable"); private static final URI UPDATABLE_CONFIG_DESCRIPTION_URI = URI.create("thing-type:mqtt:homeassistant-updatable");
private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class); private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
protected final MqttChannelTypeProvider channelTypeProvider; protected final MqttChannelTypeProvider channelTypeProvider;
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry;
public final int attributeReceiveTimeout; public final int attributeReceiveTimeout;
protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing; protected final DelayedBatchProcessing<AbstractComponent<?>> delayedProcessing;
protected final DiscoverComponents discoverComponents; protected final DiscoverComponents discoverComponents;
@ -106,6 +104,7 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
protected final TransformationServiceProvider transformationServiceProvider; protected final TransformationServiceProvider transformationServiceProvider;
private boolean started; private boolean started;
private boolean newStyleChannels;
private @Nullable Update updateComponent; private @Nullable Update updateComponent;
/** /**
@ -118,16 +117,22 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
* @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds. * @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds.
*/ */
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider, public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
TransformationServiceProvider transformationServiceProvider, int subscribeTimeout, TransformationServiceProvider transformationServiceProvider, int subscribeTimeout,
int attributeReceiveTimeout) { int attributeReceiveTimeout) {
super(thing, subscribeTimeout); super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create(); this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.channelTypeProvider = channelTypeProvider; this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
this.transformationServiceProvider = transformationServiceProvider; this.transformationServiceProvider = transformationServiceProvider;
this.attributeReceiveTimeout = attributeReceiveTimeout; this.attributeReceiveTimeout = attributeReceiveTimeout;
this.delayedProcessing = new DelayedBatchProcessing<>(attributeReceiveTimeout, this, scheduler); 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.discoverComponents = new DiscoverComponents(thing.getUID(), scheduler, this, this, gson,
this.transformationServiceProvider); this.transformationServiceProvider, newStyleChannels);
} }
@Override @Override
@ -141,16 +146,12 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} }
discoveryHomeAssistantIDs.addAll(HaID.fromConfig(config)); discoveryHomeAssistantIDs.addAll(HaID.fromConfig(config));
ThingTypeUID typeID = getThing().getThingTypeUID();
for (Channel channel : thing.getChannels()) { for (Channel channel : thing.getChannels()) {
final String groupID = channel.getUID().getGroupId(); final String groupID = channel.getUID().getGroupId();
// Already restored component? // Already restored component?
@Nullable @Nullable
AbstractComponent<?> component = haComponents.get(groupID); 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()); HaID haID = HaID.fromConfig(config.basetopic, channel.getConfiguration());
discoveryHomeAssistantIDs.add(haID); discoveryHomeAssistantIDs.add(haID);
@ -161,29 +162,27 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} else { } else {
try { try {
component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this, component = ComponentFactory.createComponent(thingUID, haID, channelConfigurationJSON, this, this,
scheduler, gson, transformationServiceProvider); scheduler, gson, transformationServiceProvider, newStyleChannels);
final ChannelGroupUID groupUID = component.getGroupUID(); if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
String id = null; typeID = calculateThingTypeUID(component);
if (groupUID != null) {
id = groupUID.getId();
} }
haComponents.put(id, component);
component.addChannelTypes(channelTypeProvider); haComponents.put(component.getGroupId(), component);
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
logger.error("Cannot not restore component {}: {}", thing, e.getMessage()); logger.error("Cannot restore component {}: {}", thing, e.getMessage());
} }
} }
} }
updateThingType(); if (updateThingType(typeID)) {
super.initialize();
super.initialize(); }
} }
@Override @Override
public void dispose() { public void dispose() {
removeStateDescriptions();
// super.dispose() calls stop() // super.dispose() calls stop()
super.dispose(); super.dispose();
haComponents.values().forEach(c -> c.removeChannelTypes(channelTypeProvider));
} }
@Override @Override
@ -234,13 +233,21 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
@Override @Override
public @Nullable ChannelState getChannelState(ChannelUID channelUID) { public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
String groupID = channelUID.getGroupId(); String componentId;
if (channelUID.isInGroup()) {
componentId = channelUID.getGroupId();
} else {
componentId = channelUID.getId();
}
AbstractComponent<?> component; AbstractComponent<?> component;
synchronized (haComponents) { // sync whenever discoverComponents is started synchronized (haComponents) { // sync whenever discoverComponents is started
component = haComponents.get(groupID); component = haComponents.get(componentId);
} }
if (component == null) { if (component == null) {
return null; component = haComponents.get("");
if (component == null) {
return null;
}
} }
ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup()); ComponentChannel componentChannel = component.getChannel(channelUID.getIdWithoutGroup());
if (componentChannel == null) { if (componentChannel == null) {
@ -269,12 +276,12 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
} }
synchronized (haComponents) { // sync whenever discoverComponents is started synchronized (haComponents) { // sync whenever discoverComponents is started
ThingTypeUID typeID = getThing().getThingTypeUID();
for (AbstractComponent<?> discovered : discoveredComponentsList) { for (AbstractComponent<?> discovered : discoveredComponentsList) {
final ChannelGroupUID groupUID = discovered.getGroupUID(); if (typeID.equals(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)) {
String id = null; typeID = calculateThingTypeUID(discovered);
if (groupUID != null) {
id = groupUID.getId();
} }
String id = discovered.getGroupId();
AbstractComponent<?> known = haComponents.get(id); AbstractComponent<?> known = haComponents.get(id);
// Is component already known? // Is component already known?
if (known != null) { 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 // Add component to the component map
haComponents.put(id, discovered); haComponents.put(id, discovered);
// Start component / Subscribe to channel topics // Start component / Subscribe to channel topics
@ -302,47 +307,11 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
updateComponent = (Update) discovered; updateComponent = (Update) discovered;
updateComponent.setReleaseStateUpdateListener(this::releaseStateUpdated); 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 @Override
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) { protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
if (availabilityTopicsSeen.orElse(messageReceived)) { if (availabilityTopicsSeen.orElse(messageReceived)) {
@ -372,28 +341,72 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
super.handleConfigurationUpdate(configurationParameters); super.handleConfigurationUpdate(configurationParameters);
} }
private void updateThingType() { private boolean updateThingType(ThingTypeUID typeID) {
// if this is a dynamic type, then we update the type // if this is a dynamic type, then we update the type
ThingTypeUID typeID = thing.getThingTypeUID();
if (!MqttBindingConstants.HOMEASSISTANT_MQTT_THING.equals(typeID)) { if (!MqttBindingConstants.HOMEASSISTANT_MQTT_THING.equals(typeID)) {
List<ChannelGroupDefinition> groupDefs; var thingTypeBuilder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMEASSISTANT_MQTT_THING);
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();
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) { private void releaseStateUpdated(Update.ReleaseState state) {

View File

@ -7,7 +7,7 @@
<config-description uri="channel-type:mqtt:ha-channel"> <config-description uri="channel-type:mqtt:ha-channel">
<parameter name="component" type="text" readOnly="true" required="true"> <parameter name="component" type="text" readOnly="true" required="true">
<label>Component</label> <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> <default></default>
</parameter> </parameter>
<parameter name="nodeid" type="text" readOnly="true"> <parameter name="nodeid" type="text" readOnly="true">
@ -17,12 +17,12 @@
</parameter> </parameter>
<parameter name="objectid" type="text" readOnly="true" required="true"> <parameter name="objectid" type="text" readOnly="true" required="true">
<label>Object ID</label> <label>Object ID</label>
<description>Object id of the component</description> <description>Object ID of the component</description>
<default></default> <default></default>
</parameter> </parameter>
<parameter name="config" type="text" readOnly="true" required="true"> <parameter name="config" type="text" readOnly="true" required="true">
<label>Json Configuration</label> <label>JSON Configuration</label>
<description>The json configuration string received by the component via MQTT.</description> <description>The JSON configuration string received by the component via MQTT.</description>
<default></default> <default></default>
</parameter> </parameter>
</config-description> </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.basetopic.description = MQTT base prefix
thing-type.config.mqtt.homeassistant.topics.label = MQTT Config Topic 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) 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.label = MQTT Base Prefix
thing-type.config.mqtt.homeassistant-updatable.basetopic.description = 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 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.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; 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.MqttChannelTypeProvider;
import org.openhab.binding.mqtt.generic.TransformationServiceProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
import org.openhab.binding.mqtt.handler.BrokerHandler; 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.MqttBrokerConnection;
import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber; import org.openhab.core.io.transport.mqtt.MqttMessageSubscriber;
import org.openhab.core.test.java.JavaTest; 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.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; 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.ThingUID;
import org.openhab.core.thing.binding.builder.BridgeBuilder; import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder; 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.ThingTypeBuilder;
import org.openhab.core.thing.type.ThingTypeRegistry; import org.openhab.core.thing.type.ThingTypeRegistry;
import org.openhab.transform.jinja.internal.JinjaTransformationService; 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 String BRIDGE_ID = UUID.randomUUID().toString();
public static final ThingUID BRIDGE_UID = new ThingUID(BRIDGE_TYPE_UID, BRIDGE_ID); 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 = "Home Assistant Thing";
public static final String HA_TYPE_LABEL = "Homeassistant"; public static final ThingTypeUID HA_TYPE_UID = new ThingTypeUID(BINDING_ID, "homeassistant_dynamic_type");
public static final ThingTypeUID HA_TYPE_UID = new ThingTypeUID(BINDING_ID, HA_TYPE_ID);
public static final String HA_ID = UUID.randomUUID().toString(); 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({}) MqttBrokerConnection bridgeConnection;
protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry; protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry;
protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider;
protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider; 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 Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build();
protected final BrokerHandler bridgeHandler = spy(new BrokerHandler(bridgeThing)); protected final BrokerHandler bridgeHandler = spy(new BrokerHandler(bridgeThing));
@ -95,13 +103,14 @@ public abstract class AbstractHomeAssistantTests extends JavaTest {
public void beforeEachAbstractHomeAssistantTests() { public void beforeEachAbstractHomeAssistantTests() {
when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID)) when(thingTypeRegistry.getThingType(BRIDGE_TYPE_UID))
.thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build()); .thenReturn(ThingTypeBuilder.instance(BRIDGE_TYPE_UID, BRIDGE_TYPE_LABEL).build());
when(thingTypeRegistry.getThingType(HA_TYPE_UID)) when(thingTypeRegistry.getThingType(MqttBindingConstants.HOMEASSISTANT_MQTT_THING)).thenReturn(HA_THING_TYPE);
.thenReturn(ThingTypeBuilder.instance(HA_TYPE_UID, HA_TYPE_LABEL).build());
when(transformationServiceProvider when(transformationServiceProvider
.getTransformationService(JinjaTransformationProfile.PROFILE_TYPE_UID.getId())) .getTransformationService(JinjaTransformationProfile.PROFILE_TYPE_UID.getId()))
.thenReturn(jinjaTransformationService); .thenReturn(jinjaTransformationService);
channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry)); channelTypeProvider = spy(new MqttChannelTypeProvider(thingTypeRegistry, new VolatileStorageService()));
stateDescriptionProvider = spy(new MqttChannelStateDescriptionProvider());
channelTypeRegistry = spy(new ChannelTypeRegistry());
setupConnection(); setupConnection();

View File

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

View File

@ -65,9 +65,9 @@ public class BinarySensorTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1)); assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("onoffsensor")); 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); OnOffValue.class);
publishMessage("zigbee2mqtt/sensor/state", "{ \"state\": \"ON_\" }"); 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.channels.size(), is(1));
assertThat(component.getName(), is("sensor1")); 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", assertChannel(component, Sensor.SENSOR_CHANNEL_ID, "zigbee2mqtt/sensor/state", "", "sensor1",
NumberValue.class); NumberValue.class);

View File

@ -66,8 +66,8 @@ public class SwitchTests extends AbstractComponentTests {
assertThat(component.channels.size(), is(1)); assertThat(component.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock")); assertThat(component.getName(), is("th1 auto lock"));
assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/auto_lock", "state", assertChannel(component, Switch.SWITCH_CHANNEL_ID, "zigbee2mqtt/th1", "zigbee2mqtt/th1/set/auto_lock",
OnOffValue.class); "th1 auto lock", OnOffValue.class);
publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}"); publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.OFF); 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.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock")); 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\"}"); publishMessage("zigbee2mqtt/th1", "{\"auto_lock\": \"MANUAL\"}");
assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.OFF); 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.channels.size(), is(1));
assertThat(component.getName(), is("th1 auto lock")); 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); OnOffValue.class);
component.getChannel(Switch.SWITCH_CHANNEL_ID).getState().publishValue(OnOffType.OFF); 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.binding.mqtt.homeassistant.internal.component.Switch;
import org.openhab.core.thing.Channel; import org.openhab.core.thing.Channel;
import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.types.StateDescription;
/** /**
* Tests for {@link HomeAssistantThingHandler} * Tests for {@link HomeAssistantThingHandler}
@ -73,8 +74,8 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing); when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, transformationServiceProvider, thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT); channelTypeRegistry, transformationServiceProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection); thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock); thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler; nonSpyThingHandler = thingHandler;
@ -105,9 +106,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class)); verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Climate.class));
thingHandler.delayedProcessing.forceProcessNow(); thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(6)); assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(6));
verify(channelTypeProvider, times(6)).setChannelType(any(), any()); verify(stateDescriptionProvider, times(6)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(1)).setChannelGroupType(any(), any()); verify(channelTypeProvider, times(1)).putChannelGroupType(any());
configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config"; configTopic = "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config";
thingHandler.discoverComponents.processMessage(configTopic, 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)); verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopic)), any(Switch.class));
thingHandler.delayedProcessing.forceProcessNow(); thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(7)); assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
verify(channelTypeProvider, times(7)).setChannelType(any(), any()); verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any()); 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)); verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
thingHandler.delayedProcessing.forceProcessNow(); thingHandler.delayedProcessing.forceProcessNow();
waitForAssert(() -> { 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(); thingHandler.delayedProcessing.forceProcessNow();
verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempOutside)), any(Sensor.class)); verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempOutside)), any(Sensor.class));
waitForAssert(() -> { 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(); thingHandler.delayedProcessing.forceProcessNow();
waitForAssert(() -> { 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)); verify(thingHandler, times(2)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class));
waitForAssert(() -> { 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", "homeassistant/switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt/config",
getResourceAsByteArray("component/configTS0601AutoLock.json")); getResourceAsByteArray("component/configTS0601AutoLock.json"));
thingHandler.delayedProcessing.forceProcessNow(); thingHandler.delayedProcessing.forceProcessNow();
assertThat(haThing.getChannels().size(), CoreMatchers.is(7)); assertThat(nonSpyThingHandler.getThing().getChannels().size(), CoreMatchers.is(7));
verify(channelTypeProvider, times(7)).setChannelType(any(), any()); verify(stateDescriptionProvider, atLeast(7)).setDescription(any(), any(StateDescription.class));
// When dispose // When dispose
thingHandler.dispose(); thingHandler.dispose();
@ -249,9 +250,31 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
MQTT_TOPICS.forEach(t -> { MQTT_TOPICS.forEach(t -> {
verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any()); verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).unsubscribe(eq(t), any());
}); });
}
// Expect channel types removed, 6 for climate and 1 for switch @Test
verify(channelTypeProvider, times(7)).removeChannelType(any()); 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 // Expect channel group types removed, 1 for each component
verify(channelTypeProvider, times(2)).removeChannelGroupType(any()); verify(channelTypeProvider, times(2)).removeChannelGroupType(any());
} }

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.mqtt.homie.internal.handler; package org.openhab.binding.mqtt.homie.internal.handler;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -19,6 +20,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -140,6 +142,8 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
if (config.removetopics) { if (config.removetopics) {
this.removeRetainedTopics(); this.removeRetainedTopics();
} }
channelTypeProvider.removeThingType(thing.getThingTypeUID());
channelTypeProvider.removeChannelGroupTypesForPrefix(thing.getThingTypeUID().getId());
super.handleRemoval(); super.handleRemoval();
} }
@ -169,7 +173,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
} }
delayedProcessing.join(); delayedProcessing.join();
device.stop(); device.stop();
channelTypeProvider.removeThingType(device.thingTypeUID);
super.stop(); super.stop();
} }
@ -216,7 +219,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
@Override @Override
public void nodeRemoved(Node node) { public void nodeRemoved(Node node) {
channelTypeProvider.removeChannelGroupType(node.channelGroupTypeUID);
delayedProcessing.accept(node); delayedProcessing.accept(node);
} }
@ -228,7 +230,6 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
@Override @Override
public void nodeAddedOrChanged(Node node) { public void nodeAddedOrChanged(Node node) {
channelTypeProvider.setChannelGroupType(node.channelGroupTypeUID, node.type());
delayedProcessing.accept(node); delayedProcessing.accept(node);
} }
@ -288,27 +289,42 @@ public class HomieThingHandler extends AbstractMQTTThingHandler implements Devic
private void updateThingType() { private void updateThingType() {
// Make sure any dynamic channel types exist (i.e. ones created for a number channel with a specific dimension) // 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) 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 // if this is a dynamic type, then we update the type
ThingTypeUID typeID = device.thingTypeUID; ThingTypeUID typeID = device.thingTypeUID;
if (!MqttBindingConstants.HOMIE300_MQTT_THING.equals(typeID)) { if (!MqttBindingConstants.HOMIE300_MQTT_THING.equals(typeID)) {
device.nodes.stream() channelTypeProvider.updateChannelGroupTypesForPrefix(thing.getThingTypeUID().getId(), device.nodes.stream()
.forEach(n -> channelTypeProvider.setChannelGroupType(n.channelGroupTypeUID, n.type())); .map(n -> n.type(thing.getThingTypeUID().getId(), channelTypeProvider)).toList());
List<ChannelGroupDefinition> groupDefs = device.nodes().stream().map(Node::getChannelGroupDefinition) List<ChannelGroupDefinition> groupDefs = device.nodes.stream(nodeOrder())
.collect(Collectors.toList()); .map(n -> n.getChannelGroupDefinition(thing.getThingTypeUID().getId())).toList();
var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMIE300_MQTT_THING) var builder = channelTypeProvider.derive(typeID, MqttBindingConstants.HOMIE300_MQTT_THING)
.withChannelGroupDefinitions(groupDefs); .withChannelGroupDefinitions(groupDefs);
ThingType thingType = builder.build();
channelTypeProvider.setThingType(typeID, thingType); channelTypeProvider.putThingType(builder.build());
} }
} }
private void updateChannels() { private void updateChannels() {
List<Channel> channels = device.nodes().stream().flatMap(n -> n.properties.stream()) List<Channel> channels = device.nodes.stream(nodeOrder())
.map(p -> p.getChannel(channelTypeRegistry)).collect(Collectors.toList()); .flatMap(node -> node.properties
.stream(node.propertyOrder(thing.getThingTypeUID().getId(), channelTypeProvider))
.map(p -> p.getChannel(channelTypeRegistry)))
.toList();
updateThing(editThing().withChannels(channels).build()); 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; package org.openhab.binding.mqtt.homie.internal.homie300;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.mapping.AbstractMqttAttributeClass;
import org.openhab.binding.mqtt.generic.tools.ChildMap; import org.openhab.binding.mqtt.generic.tools.ChildMap;
import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants; import org.openhab.binding.mqtt.homie.generic.internal.MqttBindingConstants;
@ -55,7 +57,6 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
// Runtime // Runtime
public final DeviceCallback callback; public final DeviceCallback callback;
protected final ChannelGroupUID channelGroupUID; protected final ChannelGroupUID channelGroupUID;
public final ChannelGroupTypeUID channelGroupTypeUID;
private final String topic; private final String topic;
private boolean initialized = false; private boolean initialized = false;
@ -72,7 +73,6 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
this.topic = topic + "/" + nodeID; this.topic = topic + "/" + nodeID;
this.nodeID = nodeID; this.nodeID = nodeID;
this.callback = callback; this.callback = callback;
channelGroupTypeUID = new ChannelGroupTypeUID(MqttBindingConstants.BINDING_ID, UIDUtils.encode(this.topic));
channelGroupUID = new ChannelGroupUID(thingUID, UIDUtils.encode(nodeID)); channelGroupUID = new ChannelGroupUID(thingUID, UIDUtils.encode(nodeID));
properties = new ChildMap<>(); properties = new ChildMap<>();
} }
@ -117,15 +117,16 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
/** /**
* Return the channel group type for this Node. * Return the channel group type for this Node.
*/ */
public ChannelGroupType type() { public ChannelGroupType type(String prefix, MqttChannelTypeProvider channelTypeProvider) {
final List<ChannelDefinition> channelDefinitions = properties.stream() final List<ChannelDefinition> channelDefinitions = properties.stream(propertyOrder(prefix, channelTypeProvider))
.map(p -> Objects.requireNonNull(p.getChannelDefinition())).collect(Collectors.toList()); .map(p -> Objects.requireNonNull(p.getChannelDefinition())).toList();
return ChannelGroupTypeBuilder.instance(channelGroupTypeUID, attributes.name) return ChannelGroupTypeBuilder.instance(getChannelGroupTypeUID(prefix), attributes.name)
.withChannelDefinitions(channelDefinitions).build(); .withChannelDefinitions(channelDefinitions).build();
} }
public ChannelGroupDefinition getChannelGroupDefinition() { public ChannelGroupDefinition getChannelGroupDefinition(String prefix) {
return new ChannelGroupDefinition(channelGroupUID.getId(), channelGroupTypeUID, attributes.name, null); return new ChannelGroupDefinition(channelGroupUID.getId(), getChannelGroupTypeUID(prefix), attributes.name,
null);
} }
/** /**
@ -220,4 +221,21 @@ public class Node implements AbstractMqttAttributeClass.AttributeChanged {
return topics; 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.config.core.Configuration;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.library.types.StringType; 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.Channel;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
@ -99,7 +100,8 @@ public class HomieThingHandlerTests {
private @NonNullByDefault({}) Thing thing; private @NonNullByDefault({}) Thing thing;
private @NonNullByDefault({}) HomieThingHandler thingHandler; 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 MqttChannelStateDescriptionProvider stateDescriptionProvider = new MqttChannelStateDescriptionProvider();
private final String deviceID = ThingChannelConstants.TEST_HOMIE_THING.getId(); 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(); Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, 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")); 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch; 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.library.types.OnOffType;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType; import org.openhab.core.types.UnDefType;
import org.openhab.core.util.UIDUtils;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
@ -152,17 +150,14 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4); ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(4);
DiscoverComponents discover = spy(new DiscoverComponents(ThingChannelConstants.TEST_HOME_ASSISTANT_THING, 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. // The DiscoverComponents object calls ComponentDiscovered callbacks.
// In the following implementation we add the found component to the `haComponents` map // 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. // and add the types to the channelTypeProvider, like in the real Thing handler.
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
ComponentDiscovered cd = (haID, c) -> { ComponentDiscovered cd = (haID, c) -> {
haComponents.put(c.getGroupUID().getId(), c); haComponents.put(c.getGroupId(), c);
c.addChannelTypes(channelTypeProvider);
channelTypeProvider.setChannelGroupType(Objects.requireNonNull(c.getGroupTypeUID()),
Objects.requireNonNull(c.getType()));
latch.countDown(); latch.countDown();
}; };
@ -181,15 +176,10 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
assertNull(failure); assertNull(failure);
assertThat(haComponents.size(), is(1)); assertThat(haComponents.size(), is(1));
// For the switch component we should have one channel group type and one channel type String channelGroupId = "switch_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId();
// setChannelGroupType is called once above String channelId = Switch.SWITCH_CHANNEL_ID;
verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any());
verify(channelTypeProvider, times(1)).setChannelType(any(), any());
String channelGroupId = UIDUtils State value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache()
.encode("node_" + ThingChannelConstants.TEST_HOME_ASSISTANT_THING.getId() + "_switch");
State value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache()
.getChannelState(); .getChannelState();
assertThat(value, is(UnDefType.UNDEF)); assertThat(value, is(UnDefType.UNDEF));
@ -203,8 +193,7 @@ public class HomeAssistantMQTTImplementationTest extends MqttOSGiTest {
verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any()); verify(channelStateUpdateListener, timeout(4000).times(1)).updateChannelState(any(), any());
// Value should be ON now. // Value should be ON now.
value = haComponents.get(channelGroupId).getChannel(Switch.SWITCH_CHANNEL_ID).getState().getCache() value = haComponents.get(channelGroupId).getChannel(channelGroupId).getState().getCache().getChannelState();
.getChannelState();
assertThat(value, is(OnOffType.ON)); assertThat(value, is(OnOffType.ON));
} }
} }