[mqtt.homeassistant] Use a thing action for updates, instead of a config action (#18524)

* [mqtt.homeassistant] Use a thing action for updates, instead of a config action

Signed-off-by: Cody Cutrer <cody@cutrer.us>
This commit is contained in:
Cody Cutrer
2025-05-17 14:10:54 +02:00
committed by GitHub
parent 3607758dea
commit 97ea270399
7 changed files with 120 additions and 81 deletions
@@ -81,8 +81,8 @@ public class MqttThingHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (supportsThingType(thingTypeUID)) {
return new HomeAssistantThingHandler(thing, typeProvider, stateDescriptionProvider, channelTypeRegistry,
jinjava, unitProvider, 10000, 2000);
return new HomeAssistantThingHandler(thing, this, typeProvider, stateDescriptionProvider,
channelTypeRegistry, jinjava, unitProvider, 10000, 2000);
}
return null;
}
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2025 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.actions;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mqtt.homeassistant.internal.handler.HomeAssistantThingHandler;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ServiceScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This is the automation engine action handler service for the update action.
*
* @author Cody Cutrer - Initial contribution
*/
@Component(scope = ServiceScope.PROTOTYPE, service = HomeAssistantUpdateThingActions.class)
@ThingActionsScope(name = "mqtt")
@NonNullByDefault
public class HomeAssistantUpdateThingActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(HomeAssistantUpdateThingActions.class);
private @Nullable HomeAssistantThingHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (HomeAssistantThingHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@RuleAction(label = "@text/updateActionLabel", description = "@text/updateActionDesc")
public void update() {
HomeAssistantThingHandler handler = this.handler;
if (handler == null) {
logger.warn("Home Assistant Update Action Service ThingHandler is null!");
return;
}
handler.doUpdate();
}
public static void update(ThingActions actions) {
((HomeAssistantUpdateThingActions) actions).update();
}
}
@@ -12,7 +12,6 @@
*/
package org.openhab.binding.mqtt.homeassistant.internal.handler;
import java.net.URI;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@@ -40,13 +39,13 @@ import org.openhab.binding.mqtt.homeassistant.internal.DiscoverComponents.Compon
import org.openhab.binding.mqtt.homeassistant.internal.HaID;
import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration;
import org.openhab.binding.mqtt.homeassistant.internal.HomeAssistantChannelLinkageChecker;
import org.openhab.binding.mqtt.homeassistant.internal.actions.HomeAssistantUpdateThingActions;
import org.openhab.binding.mqtt.homeassistant.internal.component.AbstractComponent;
import org.openhab.binding.mqtt.homeassistant.internal.component.ComponentFactory;
import org.openhab.binding.mqtt.homeassistant.internal.component.Update;
import org.openhab.binding.mqtt.homeassistant.internal.config.ChannelConfigurationTypeAdapterFactory;
import org.openhab.binding.mqtt.homeassistant.internal.exception.ConfigurationException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
import org.openhab.core.thing.Channel;
@@ -56,6 +55,7 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.slf4j.Logger;
@@ -88,10 +88,10 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
private static final Comparator<AbstractComponent<?>> COMPONENT_COMPARATOR = Comparator
.comparing((AbstractComponent<?> component) -> component.hasGroup())
.thenComparing(AbstractComponent::getName);
private static final URI UPDATABLE_CONFIG_DESCRIPTION_URI = URI.create("thing-type:mqtt:homeassistant-updatable");
private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class);
protected final BaseThingHandlerFactory thingHandlerFactory;
protected final MqttChannelTypeProvider channelTypeProvider;
protected final MqttChannelStateDescriptionProvider stateDescriptionProvider;
protected final ChannelTypeRegistry channelTypeRegistry;
@@ -122,11 +122,13 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
* @param subscribeTimeout Timeout for the entire tree parsing and subscription. In milliseconds.
* @param attributeReceiveTimeout The timeout per attribute field subscription. In milliseconds.
*/
public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
public HomeAssistantThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
MqttChannelTypeProvider channelTypeProvider, MqttChannelStateDescriptionProvider stateDescriptionProvider,
ChannelTypeRegistry channelTypeRegistry, Jinjava jinjava, UnitProvider unitProvider, int subscribeTimeout,
int attributeReceiveTimeout) {
super(thing, subscribeTimeout);
this.gson = new GsonBuilder().registerTypeAdapterFactory(new ChannelConfigurationTypeAdapterFactory()).create();
this.thingHandlerFactory = thingHandlerFactory;
this.channelTypeProvider = channelTypeProvider;
this.stateDescriptionProvider = stateDescriptionProvider;
this.channelTypeRegistry = channelTypeRegistry;
@@ -344,11 +346,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
return null;
});
}
if (discovered instanceof Update) {
updateComponent = (Update) discovered;
updateComponent.setReleaseStateUpdateListener(this::releaseStateUpdated);
}
}
updateThingType(typeID);
}
@@ -369,6 +366,10 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
haComponents.remove(known.getComponentId());
haComponentsByHaId.remove(removed);
componentActuallyRemoved = true;
if (known.equals(updateComponent)) {
updateComponent = null;
}
}
}
if (componentActuallyRemoved) {
@@ -377,6 +378,17 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
}
public void doUpdate() {
Update updateComponent = this.updateComponent;
if (updateComponent == null) {
logger.warn(
"Received update command for Home Assistant device {}, but it does not have an update component.",
getThing().getUID());
} else {
updateComponent.doUpdate();
}
}
@Override
protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availabilityTopicsSeen) {
if (availabilityTopicsSeen.orElse(messageReceived)) {
@@ -386,26 +398,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
}
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters)
throws ConfigValidationException {
if (configurationParameters.containsKey("doUpdate")) {
configurationParameters = new HashMap<>(configurationParameters);
Object value = configurationParameters.remove("doUpdate");
if (value instanceof Boolean doUpdate && doUpdate) {
Update updateComponent = this.updateComponent;
if (updateComponent == null) {
logger.warn(
"Received update command for Home Assistant device {}, but it does not have an update component.",
getThing().getUID());
} else {
updateComponent.doUpdate();
}
}
}
super.handleConfigurationUpdate(configurationParameters);
}
private boolean updateThingType(ThingTypeUID typeID) {
// if this is a dynamic type, then we update the type
if (!MqttBindingConstants.HOMEASSISTANT_MQTT_THING.equals(typeID)) {
@@ -434,10 +426,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
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());
@@ -511,6 +499,14 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler
haComponents.put(component.getComponentId(), component);
haComponentsByUniqueId.put(component.getUniqueId(), component);
haComponentsByHaId.put(component.getHaID(), component);
if (component instanceof Update updateComponent) {
this.updateComponent = updateComponent;
updateComponent.setReleaseStateUpdateListener(this::releaseStateUpdated);
if (updateComponent.isUpdatable()) {
thingHandlerFactory.registerService(this, HomeAssistantUpdateThingActions.class);
}
}
return true;
}
@@ -16,28 +16,4 @@
<default>homeassistant</default>
</parameter>
</config-description>
<config-description uri="thing-type:mqtt:homeassistant-updatable">
<parameter-group name="actions">
<label>Actions</label>
</parameter-group>
<parameter name="topics" type="text" required="true" multiple="true">
<label>MQTT Config Topic</label>
<description>List of Home Assistant configuration topics (e.g. button/my-device/restart)</description>
</parameter>
<parameter name="basetopic" type="text" required="true">
<label>MQTT Base Prefix</label>
<description>MQTT base prefix</description>
<default>homeassistant</default>
</parameter>
<parameter name="doUpdate" type="boolean" groupName="actions">
<label>Update</label>
<description>Request the device do an OTA update</description>
<advanced>true</advanced>
<default>false</default>
</parameter>
</config-description>
</config-description:config-descriptions>
@@ -45,15 +45,6 @@ channel-type.config.mqtt.ha-channel.nodeid.description = Optional node name of t
channel-type.config.mqtt.ha-channel.objectid.label = Object ID
channel-type.config.mqtt.ha-channel.objectid.description = Object ID of the component
# thing types config
thing-type.config.mqtt.homeassistant-updatable.basetopic.label = MQTT Base Prefix
thing-type.config.mqtt.homeassistant-updatable.basetopic.description = MQTT base prefix
thing-type.config.mqtt.homeassistant-updatable.topics.label = MQTT Config Topic
thing-type.config.mqtt.homeassistant-updatable.topics.description = List of Home Assistant configuration topics (e.g. button/my-device/restart)
thing-type.config.mqtt.homeassistant-updatable.doUpdate.label = Update
thing-type.config.mqtt.homeassistant-updatable.doUpdate.description = Request the device do an OTA update
# binding config
binding.config.mqtt.homeassistant-status.label = Publish Online Status
@@ -129,3 +120,8 @@ state.water-heater.mode.gas = Gas
state.water-heater.mode.heat-pump = Heat Pump
state.water-heater.mode.high-demand = High Demand
state.water-heater.mode.performance = Performance
# thing actions
updateActionLabel = Update
updateActionDesc = Trigger the device to perform an update
@@ -47,6 +47,7 @@ import org.openhab.core.library.types.HSBType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.type.AutoUpdatePolicy;
import org.openhab.core.thing.type.ChannelKind;
@@ -67,6 +68,7 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private static final int ATTRIBUTE_RECEIVE_TIMEOUT = 2000;
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
protected @Mock @NonNullByDefault({}) BaseThingHandlerFactory thingHandlerFactory;
private @NonNullByDefault({}) LatchThingHandler thingHandler;
protected @Mock @NonNullByDefault({}) UnitProvider unitProvider;
@@ -85,8 +87,9 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
when(callbackMock.getBridge(eq(BRIDGE_UID))).thenReturn(bridgeThing);
thingHandler = new LatchThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new LatchThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry, unitProvider, SUBSCRIBE_TIMEOUT,
ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
thingHandler = spy(thingHandler);
@@ -361,11 +364,12 @@ public abstract class AbstractComponentTests extends AbstractHomeAssistantTests
private @Nullable CountDownLatch latch;
private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent;
public LatchThingHandler(Thing thing, MqttChannelTypeProvider channelTypeProvider,
public LatchThingHandler(Thing thing, BaseThingHandlerFactory thingHandlerFactory,
MqttChannelTypeProvider channelTypeProvider,
MqttChannelStateDescriptionProvider stateDescriptionProvider, ChannelTypeRegistry channelTypeRegistry,
UnitProvider unitProvider, int subscribeTimeout, int attributeReceiveTimeout) {
super(thing, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry, new Jinjava(),
unitProvider, subscribeTimeout, attributeReceiveTimeout);
super(thing, thingHandlerFactory, channelTypeProvider, stateDescriptionProvider, channelTypeRegistry,
new Jinjava(), unitProvider, subscribeTimeout, attributeReceiveTimeout);
}
@Override
@@ -42,6 +42,7 @@ import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
@@ -70,6 +71,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
private static final List<String> MQTT_TOPICS = CONFIG_TOPICS.stream()
.map(AbstractHomeAssistantTests::configTopicToMqtt).collect(Collectors.toList());
private @Mock @NonNullByDefault({}) BaseThingHandlerFactory thingHandlerFactory;
private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock;
private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler;
private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler;
@@ -88,8 +90,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
}
protected void setupThingHandler() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@@ -357,8 +360,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
@Test
public void testDuplicateChannelId() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;
@@ -413,8 +417,9 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests {
@Test
public void testDuplicateChannelIdComplex() {
thingHandler = new HomeAssistantThingHandler(haThing, channelTypeProvider, stateDescriptionProvider,
channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler = new HomeAssistantThingHandler(haThing, thingHandlerFactory, channelTypeProvider,
stateDescriptionProvider, channelTypeRegistry, new Jinjava(), unitProvider, SUBSCRIBE_TIMEOUT,
ATTRIBUTE_RECEIVE_TIMEOUT);
thingHandler.setConnection(bridgeConnection);
thingHandler.setCallback(callbackMock);
nonSpyThingHandler = thingHandler;