]> git.basschouten.com Git - openhab-addons.git/blob
23e6c67ee276f5d77b91a1f383ee58c96a10c451
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.mqtt.generic.AbstractMQTTThingHandler;
26 import org.openhab.binding.mqtt.generic.ChannelConfig;
27 import org.openhab.binding.mqtt.generic.ChannelState;
28 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
29 import org.openhab.binding.mqtt.generic.MqttChannelStateDescriptionProvider;
30 import org.openhab.binding.mqtt.generic.TransformationServiceProvider;
31 import org.openhab.binding.mqtt.generic.utils.FutureCollector;
32 import org.openhab.binding.mqtt.generic.values.Value;
33 import org.openhab.binding.mqtt.generic.values.ValueFactory;
34 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
35 import org.openhab.core.thing.Channel;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.type.ChannelTypeUID;
41 import org.openhab.core.types.StateDescription;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * This handler manages manual created Things with manually added channels to link to MQTT topics.
47  *
48  * @author David Graeff - Initial contribution
49  */
50 @NonNullByDefault
51 public class GenericMQTTThingHandler extends AbstractMQTTThingHandler implements ChannelStateUpdateListener {
52     private final Logger logger = LoggerFactory.getLogger(GenericMQTTThingHandler.class);
53     final Map<ChannelUID, ChannelState> channelStateByChannelUID = new HashMap<>();
54     protected final MqttChannelStateDescriptionProvider stateDescProvider;
55     protected final TransformationServiceProvider transformationServiceProvider;
56
57     /**
58      * Creates a new Thing handler for generic MQTT channels.
59      *
60      * @param thing The thing of this handler
61      * @param stateDescProvider A channel state provider
62      * @param transformationServiceProvider The transformation service provider
63      * @param subscribeTimeout The subscribe timeout
64      */
65     public GenericMQTTThingHandler(Thing thing, MqttChannelStateDescriptionProvider stateDescProvider,
66             TransformationServiceProvider transformationServiceProvider, int subscribeTimeout) {
67         super(thing, subscribeTimeout);
68         this.stateDescProvider = stateDescProvider;
69         this.transformationServiceProvider = transformationServiceProvider;
70     }
71
72     @Override
73     public @Nullable ChannelState getChannelState(ChannelUID channelUID) {
74         return channelStateByChannelUID.get(channelUID);
75     }
76
77     /**
78      * Subscribe on all channel static topics on all {@link ChannelState}s.
79      * If subscribing on all channels worked, the thing is put ONLINE, else OFFLINE.
80      *
81      * @param connection A started broker connection
82      */
83     @Override
84     protected CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection) {
85         // availability topics are also started asynchronously, so no problem here
86         clearAllAvailabilityTopics();
87         initializeAvailabilityTopicsFromConfig();
88         return channelStateByChannelUID.values().stream().map(c -> c.start(connection, scheduler, 0))
89                 .collect(FutureCollector.allOf()).thenRun(this::calculateThingStatus);
90     }
91
92     @Override
93     protected void stop() {
94         channelStateByChannelUID.values().forEach(c -> c.getCache().resetState());
95         super.stop();
96     }
97
98     @Override
99     public void dispose() {
100         // Remove all state descriptions of this handler
101         channelStateByChannelUID.forEach((uid, state) -> stateDescProvider.remove(uid));
102         super.dispose();
103         // there is a design flaw, we can't clean up our stuff because it is needed by the super-class on disposal for
104         // unsubscribing
105         channelStateByChannelUID.clear();
106     }
107
108     @Override
109     public CompletableFuture<Void> unsubscribeAll() {
110         return CompletableFuture.allOf(
111                 channelStateByChannelUID.values().stream().map(ChannelState::stop).toArray(CompletableFuture[]::new));
112     }
113
114     /**
115      * For every Thing channel there exists a corresponding {@link ChannelState}. It consists of the MQTT state
116      * and MQTT command topic, the ChannelUID and a value state.
117      *
118      * @param channelConfig The channel configuration that contains MQTT state and command topic and multiple other
119      *            configurations.
120      * @param channelUID The channel UID
121      * @param valueState The channel value state
122      * @return
123      */
124     protected ChannelState createChannelState(ChannelConfig channelConfig, ChannelUID channelUID, Value valueState) {
125         ChannelState state = new ChannelState(channelConfig, channelUID, valueState, this);
126
127         // Incoming value transformations
128         state.addTransformation(channelConfig.transformationPattern, transformationServiceProvider);
129         // Outgoing value transformations
130         state.addTransformationOut(channelConfig.transformationPatternOut, transformationServiceProvider);
131
132         return state;
133     }
134
135     @Override
136     public void initialize() {
137         initializeAvailabilityTopicsFromConfig();
138
139         List<ChannelUID> configErrors = new ArrayList<>();
140         for (Channel channel : thing.getChannels()) {
141             final ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
142             if (channelTypeUID == null) {
143                 logger.warn("Channel {} has no type", channel.getLabel());
144                 continue;
145             }
146             final ChannelConfig channelConfig = channel.getConfiguration().as(ChannelConfig.class);
147             try {
148                 Value value = ValueFactory.createValueState(channelConfig, channelTypeUID.getId());
149                 ChannelState channelState = createChannelState(channelConfig, channel.getUID(), value);
150                 channelStateByChannelUID.put(channel.getUID(), channelState);
151                 StateDescription description = value.createStateDescription(channelConfig.commandTopic.isBlank())
152                         .build().toStateDescription();
153                 if (description != null) {
154                     stateDescProvider.setDescription(channel.getUID(), description);
155                 }
156             } catch (IllegalArgumentException e) {
157                 logger.warn("Configuration error for channel '{}'", channel.getUID(), e);
158                 configErrors.add(channel.getUID());
159             }
160         }
161
162         // If some channels could not start up, put the entire thing offline and display the channels
163         // in question to the user.
164         if (!configErrors.isEmpty()) {
165             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Remove and recreate: "
166                     + configErrors.stream().map(ChannelUID::getAsString).collect(Collectors.joining(",")));
167             return;
168         }
169         super.initialize();
170     }
171
172     @Override
173     protected void updateThingStatus(boolean messageReceived, Optional<Boolean> availibilityTopicsSeen) {
174         if (availibilityTopicsSeen.orElse(true)) {
175             updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
176         } else {
177             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE);
178         }
179     }
180
181     private void initializeAvailabilityTopicsFromConfig() {
182         GenericThingConfiguration config = getConfigAs(GenericThingConfiguration.class);
183
184         String availabilityTopic = config.availabilityTopic;
185
186         if (availabilityTopic != null) {
187             addAvailabilityTopic(availabilityTopic, config.payloadAvailable, config.payloadNotAvailable,
188                     config.transformationPattern, transformationServiceProvider);
189         } else {
190             clearAllAvailabilityTopics();
191         }
192     }
193 }