]> git.basschouten.com Git - openhab-addons.git/blob
d9ab437f4f41d4339a9c6095d1fa591b134ccd6a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.generic.internal.handler;
14
15 import java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Optional;
20 import java.util.concurrent.CompletableFuture;
21 import java.util.stream.Collectors;
22
23 import javax.measure.Unit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
28 import org.openhab.binding.mqtt.generic.ChannelConfig;
29 import org.openhab.binding.mqtt.generic.ChannelState;
30 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
31 import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
32 import org.openhab.binding.mqtt.generic.internal.MqttBindingConstants;
33 import org.openhab.binding.mqtt.generic.utils.FutureCollector;
34 import org.openhab.binding.mqtt.generic.values.Value;
35 import org.openhab.binding.mqtt.generic.values.ValueFactory;
36 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
37 import org.openhab.core.thing.Channel;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.ThingHandlerCallback;
43 import org.openhab.core.thing.binding.builder.ChannelBuilder;
44 import org.openhab.core.thing.binding.builder.ThingBuilder;
45 import org.openhab.core.thing.type.ChannelTypeUID;
46 import org.openhab.core.types.StateDescription;
47 import org.openhab.core.types.util.UnitUtils;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * This handler manages manual created Things with manually added channels to link to MQTT topics.
53  *
54  * @author David Graeff - Initial contribution
55  */
56 @NonNullByDefault
57 public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements ChannelStateUpdateListener {
58     private final Logger logger = LoggerFactory.getLogger(GenericMQTTThingHandler.class);
59     final Map<ChannelUID, ChannelState> channelStateByChannelUID = new HashMap<>();
60     protected final MqttChannelStateDescriptionProvider stateDescProvider;
61
62     /**
63      * Creates a new Thing handler for generic MQTT channels.
64      *
65      * @param thing The thing of this handler
66      * @param stateDescProvider A channel state provider
67      * @param subscribeTimeout The subscribe timeout
68      */
69     public GenericMQTTThingHandler(Thing thing, MqttChannelStateDescriptionProvider stateDescProvider,
70             int subscribeTimeout) {
71         super(thing, subscribeTimeout);
72         this.stateDescProvider = stateDescProvider;
73     }
74
75     @Override
76     public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
77         return channelStateByChannelUID.get(channelUID);
78     }
79
80     /**
81      * Subscribe on all channel static topics on all {@link ChannelState}s.
82      * If subscribing on all channels worked, the thing is put ONLINE, else OFFLINE.
83      *
84      * @param connection A started broker connection
85      */
86     @Override
87     protected CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection) {
88         // availability topics are also started asynchronously, so no problem here
89         clearAllAvailabilityTopics();
90         initializeAvailabilityTopicsFromConfig();
91         return channelStateByChannelUID.values().stream().map(c -> c.start(connection, scheduler, 0))
92                 .collect(FutureCollector.allOf()).thenRun(() -> calculateAndUpdateThingStatus(false));
93     }
94
95     @Override
96     protected void stop() {
97         channelStateByChannelUID.values().forEach(c -> c.getCache().resetState());
98         super.stop();
99     }
100
101     @Override
102     public void dispose() {
103         // Remove all state descriptions of this handler
104         channelStateByChannelUID.forEach((uid, state) -> stateDescProvider.remove(uid));
105         super.dispose();
106         // there is a design flaw, we can't clean up our stuff because it is needed by the super-class on disposal for
107         // unsubscribing
108         channelStateByChannelUID.clear();
109     }
110
111     @Override
112     public CompletableFuture<Void> unsubscribeAll() {
113         return CompletableFuture.allOf(
114                 channelStateByChannelUID.values().stream().map(ChannelState::stop).toArray(CompletableFuture[]::new));
115     }
116
117     /**
118      * For every Thing channel there exists a corresponding {@link ChannelState}. It consists of the MQTT state
119      * and MQTT command topic, the ChannelUID and a value state.
120      *
121      * @param channelConfig The channel configuration that contains MQTT state and command topic and multiple other
122      *            configurations.
123      * @param channelUID The channel UID
124      * @param valueState The channel value state
125      * @return
126      */
127     protected ChannelState createChannelState(ChannelConfig channelConfig, ChannelUID channelUID, Value valueState) {
128         return new ChannelState(channelConfig, channelUID, valueState, this);
129     }
130
131     @Override
132     public void initialize() {
133         initializeAvailabilityTopicsFromConfig();
134
135         ThingHandlerCallback callback = getCallback();
136         if (callback == null) {
137             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Framework failure: callback must not be null");
138             return;
139         }
140
141         ThingBuilder thingBuilder = editThing();
142         boolean modified = false;
143
144         List<ChannelUID> configErrors = new ArrayList<>();
145         for (Channel channel : thing.getChannels()) {
146             final ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
147             if (channelTypeUID == null) {
148                 logger.warn("Channel {} has no type", channel.getLabel());
149                 continue;
150             }
151             final ChannelConfig channelConfig = channel.getConfiguration().as(ChannelConfig.class);
152
153             if (channelTypeUID
154                     .equals(new ChannelTypeUID(MqttBindingConstants.BINDING_ID, MqttBindingConstants.NUMBER))) {
155                 Unit<?> unit = UnitUtils.parseUnit(channelConfig.unit);
156                 String dimension = unit == null ? null : UnitUtils.getDimensionName(unit);
157                 String expectedItemType = dimension == null ? "Number" : "Number:" + dimension; // unknown dimension ->
158                 // Number
159                 String actualItemType = channel.getAcceptedItemType();
160                 if (!expectedItemType.equals(actualItemType)) {
161                     ChannelBuilder channelBuilder = callback.createChannelBuilder(channel.getUID(), channelTypeUID)
162                             .withAcceptedItemType(expectedItemType).withConfiguration(channel.getConfiguration());
163                     String label = channel.getLabel();
164                     if (label != null) {
165                         channelBuilder.withLabel(label);
166                     }
167                     String description = channel.getDescription();
168                     if (description != null) {
169                         channelBuilder.withDescription(description);
170                     }
171                     thingBuilder.withoutChannel(channel.getUID());
172                     thingBuilder.withChannel(channelBuilder.build());
173                     modified = true;
174                 }
175             }
176
177             try {
178                 Value value = ValueFactory.createValueState(channelConfig, channelTypeUID.getId());
179                 ChannelState channelState = createChannelState(channelConfig, channel.getUID(), value);
180                 channelStateByChannelUID.put(channel.getUID(), channelState);
181                 StateDescription description = value.createStateDescription(channelConfig.commandTopic.isBlank())
182                         .build().toStateDescription();
183                 if (description != null) {
184                     stateDescProvider.setDescription(channel.getUID(), description);
185                 }
186             } catch (IllegalArgumentException e) {
187                 logger.warn("Configuration error for channel '{}'", channel.getUID(), e);
188                 configErrors.add(channel.getUID());
189             }
190         }
191
192         if (modified) {
193             updateThing(thingBuilder.build());
194         }
195
196         // If some channels could not start up, put the entire thing offline and display the channels
197         // in question to the user.
198         if (!configErrors.isEmpty()) {
199             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Remove and recreate: "
200                     + configErrors.stream().map(ChannelUID::getAsString).collect(Collectors.joining(",")));
201             return;
202         }
203         super.initialize();
204     }
205
206     @Override
207     protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availibilityTopicsSeen) {
208         if (availibilityTopicsSeen.orElse(true)) {
209             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
210         } else {
211             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
212         }
213     }
214
215     private void initializeAvailabilityTopicsFromConfig() {
216         GenericThingConfiguration config = getConfigAs(GenericThingConfiguration.class);
217
218         String availabilityTopic = config.availabilityTopic;
219
220         if (availabilityTopic != null) {
221             addAvailabilityTopic(availabilityTopic, config.payloadAvailable, config.payloadNotAvailable,
222                     config.transformationPattern);
223         } else {
224             clearAllAvailabilityTopics();
225         }
226     }
227 }