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