]> git.basschouten.com Git - openhab-addons.git/blob
7b00fa03186325287dbd2661576da748c459a20a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.mycroft.internal;
14
15 import java.io.IOException;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Map.Entry;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.mycroft.internal.api.MessageType;
25 import org.openhab.binding.mycroft.internal.api.MycroftConnection;
26 import org.openhab.binding.mycroft.internal.api.MycroftConnectionListener;
27 import org.openhab.binding.mycroft.internal.api.MycroftMessageListener;
28 import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
29 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
30 import org.openhab.binding.mycroft.internal.channels.AudioPlayerChannel;
31 import org.openhab.binding.mycroft.internal.channels.ChannelCommandHandler;
32 import org.openhab.binding.mycroft.internal.channels.FullMessageChannel;
33 import org.openhab.binding.mycroft.internal.channels.ListenChannel;
34 import org.openhab.binding.mycroft.internal.channels.MuteChannel;
35 import org.openhab.binding.mycroft.internal.channels.MycroftChannel;
36 import org.openhab.binding.mycroft.internal.channels.SpeakChannel;
37 import org.openhab.binding.mycroft.internal.channels.UtteranceChannel;
38 import org.openhab.binding.mycroft.internal.channels.VolumeChannel;
39 import org.openhab.core.io.net.http.WebSocketFactory;
40 import org.openhab.core.thing.Channel;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.util.ThingWebClientUtil;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.State;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * The {@link MycroftHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Gwendal Roulleau - Initial contribution
57  */
58 @NonNullByDefault
59 public class MycroftHandler extends BaseThingHandler implements MycroftConnectionListener {
60
61     private final Logger logger = LoggerFactory.getLogger(MycroftHandler.class);
62
63     private final WebSocketFactory webSocketFactory;
64     private @NonNullByDefault({}) MycroftConnection connection;
65     private @Nullable ScheduledFuture<?> scheduledFuture;
66     private MycroftConfiguration config = new MycroftConfiguration();
67     private boolean thingDisposing = false;
68     protected Map<ChannelUID, MycroftChannel<?>> mycroftChannels = new HashMap<>();
69
70     /** The reconnect frequency in case of error */
71     private static final int POLL_FREQUENCY_SEC = 30;
72     private int sometimesSendVolumeRequest = 0;
73
74     public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) {
75         super(thing);
76         this.webSocketFactory = webSocketFactory;
77     }
78
79     /**
80      * Stops the API request or websocket reconnect timer
81      */
82     private void stopTimer() {
83         ScheduledFuture<?> future = scheduledFuture;
84         if (future != null) {
85             future.cancel(false);
86             scheduledFuture = null;
87         }
88     }
89
90     /**
91      * Starts the websocket connection.
92      * It sometimes also sends a get volume request to check the connection and refresh the volume.
93      */
94     private void checkOrstartWebsocket() {
95         if (thingDisposing) {
96             return;
97         }
98         if (connection.isConnected()) {
99             // sometimes test the connection by sending a real message
100             // AND refreshing volume in the same step
101             if (sometimesSendVolumeRequest >= 3) { // arbitrary one on three times
102                 sometimesSendVolumeRequest = 0;
103                 sendMessage(new MessageVolumeGet());
104             } else {
105                 sometimesSendVolumeRequest++;
106             }
107         } else {
108             connection.start(config.host, config.port);
109         }
110     }
111
112     @Override
113     public void handleCommand(ChannelUID channelUID, Command command) {
114         ChannelCommandHandler channelCommand = mycroftChannels.get(channelUID);
115         if (channelCommand == null) {
116             logger.error("Command {} for channel {} cannot be handled", command.toString(), channelUID.toString());
117         } else {
118             channelCommand.handleCommand(command);
119         }
120     }
121
122     @Override
123     public void initialize() {
124         thingDisposing = false;
125
126         updateStatus(ThingStatus.UNKNOWN);
127
128         logger.debug("Start initializing Mycroft {}", thing.getUID());
129
130         String websocketID = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
131         this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID));
132
133         config = getConfigAs(MycroftConfiguration.class);
134         if (config.host.isBlank()) {
135             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "No host defined");
136             return;
137         } else if (config.port < 0 || config.port > 0xFFFF) {
138             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139                     "Port should be between 0 and 65536");
140             return;
141         }
142         scheduledFuture = scheduler.scheduleWithFixedDelay(this::checkOrstartWebsocket, 0, POLL_FREQUENCY_SEC,
143                 TimeUnit.SECONDS);
144
145         registerChannel(new ListenChannel(this));
146         registerChannel(new VolumeChannel(this));
147         registerChannel(new MuteChannel(this, config.volume_restoration_level));
148         registerChannel(new SpeakChannel(this));
149         registerChannel(new AudioPlayerChannel(this));
150         registerChannel(new UtteranceChannel(this));
151
152         final Channel fullMessageChannel = getThing().getChannel(MycroftBindingConstants.FULL_MESSAGE_CHANNEL);
153         @SuppressWarnings("null") // cannot be null
154         String messageTypesProperty = (String) fullMessageChannel.getConfiguration()
155                 .get(MycroftBindingConstants.FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY);
156
157         registerChannel(new FullMessageChannel(this, messageTypesProperty));
158
159         checkLinkedChannelsAndRegisterMessageListeners();
160     }
161
162     private void checkLinkedChannelsAndRegisterMessageListeners() {
163         for (Entry<ChannelUID, MycroftChannel<?>> channelEntry : mycroftChannels.entrySet()) {
164             ChannelUID uid = channelEntry.getKey();
165             MycroftChannel<?> channel = channelEntry.getValue();
166             if (isLinked(uid)) {
167                 channel.registerListeners();
168             } else {
169                 channel.unregisterListeners();
170             }
171         }
172     }
173
174     @Override
175     public void channelLinked(ChannelUID channelUID) {
176         checkLinkedChannelsAndRegisterMessageListeners();
177     }
178
179     @Override
180     public void channelUnlinked(ChannelUID channelUID) {
181         checkLinkedChannelsAndRegisterMessageListeners();
182     }
183
184     private void registerChannel(MycroftChannel<?> channel) {
185         mycroftChannels.put(channel.getChannelUID(), channel);
186     }
187
188     public void registerMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
189         this.connection.registerListener(messageType, listener);
190     }
191
192     public void unregisterMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
193         this.connection.unregisterListener(messageType, listener);
194     }
195
196     @Override
197     public void connectionEstablished() {
198         logger.debug("Mycroft thing {} is online", thing.getUID());
199         updateStatus(ThingStatus.ONLINE);
200     }
201
202     @Override
203     public void connectionLost(String reason) {
204         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
205     }
206
207     @Override
208     public void dispose() {
209         thingDisposing = true;
210         stopTimer();
211         connection.close();
212     }
213
214     public <T extends State> void updateMyChannel(MycroftChannel<T> mycroftChannel, T state) {
215         updateState(mycroftChannel.getChannelUID(), state);
216     }
217
218     public boolean sendMessage(BaseMessage message) {
219         try {
220             connection.sendMessage(message);
221             return true;
222         } catch (IOException e) {
223             logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
224             return false;
225         }
226     }
227
228     public boolean sendMessage(String message) {
229         try {
230             connection.sendMessage(message);
231             return true;
232         } catch (IOException e) {
233             logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
234             return false;
235         }
236     }
237 }