From 97ea2703999cc17f680c6238eac139e077903165 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 17 May 2025 06:10:54 -0600 Subject: [PATCH] [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 --- .../internal/MqttThingHandlerFactory.java | 4 +- .../HomeAssistantUpdateThingActions.java | 62 +++++++++++++++++ .../handler/HomeAssistantThingHandler.java | 66 +++++++++---------- .../config/homeassistant-thing-config.xml | 24 ------- .../resources/OH-INF/i18n/mqtt.properties | 14 ++-- .../component/AbstractComponentTests.java | 14 ++-- .../HomeAssistantThingHandlerTests.java | 17 +++-- 7 files changed, 120 insertions(+), 81 deletions(-) create mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/actions/HomeAssistantUpdateThingActions.java diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java index 86f6b8b316..808f272c3a 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/generic/internal/MqttThingHandlerFactory.java @@ -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; } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/actions/HomeAssistantUpdateThingActions.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/actions/HomeAssistantUpdateThingActions.java new file mode 100644 index 0000000000..bdf3c93e4d --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/actions/HomeAssistantUpdateThingActions.java @@ -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(); + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java index 302b39e345..d20ed8c2e0 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java @@ -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> 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 availabilityTopicsSeen) { if (availabilityTopicsSeen.orElse(messageReceived)) { @@ -386,26 +398,6 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler } } - @Override - public void handleConfigurationUpdate(Map 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; } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml index c09fccc5cb..9822a06416 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/config/homeassistant-thing-config.xml @@ -16,28 +16,4 @@ homeassistant - - - - - - - - - List of Home Assistant configuration topics (e.g. button/my-device/restart) - - - - - MQTT base prefix - homeassistant - - - - - Request the device do an OTA update - true - false - - diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties index b7d92813d0..d90840061b 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/resources/OH-INF/i18n/mqtt.properties @@ -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 diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java index 42927f98e4..9c8a5ca600 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java @@ -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 diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java index 039b253496..4449a8f333 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java @@ -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 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;