2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mycroft.internal;
15 import java.io.IOException;
16 import java.util.HashMap;
18 import java.util.Map.Entry;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
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;
52 * The {@link MycroftHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Gwendal Roulleau - Initial contribution
58 public class MycroftHandler extends BaseThingHandler implements MycroftConnectionListener {
60 private final Logger logger = LoggerFactory.getLogger(MycroftHandler.class);
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<>();
69 /** The reconnect frequency in case of error */
70 private static final int POLL_FREQUENCY_SEC = 30;
71 private int sometimesSendVolumeRequest = 0;
73 public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) {
75 this.webSocketFactory = webSocketFactory;
79 * Stops the API request or websocket reconnect timer
81 private void stopTimer() {
82 ScheduledFuture<?> future = scheduledFuture;
85 scheduledFuture = null;
90 * Starts the websocket connection.
91 * It sometimes also sends a get volume request to check the connection and refresh the volume.
93 private void checkOrstartWebsocket() {
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());
104 sometimesSendVolumeRequest++;
107 connection.start(config.host, config.port);
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());
117 channelCommand.handleCommand(command);
122 public void initialize() {
123 thingDisposing = false;
125 updateStatus(ThingStatus.UNKNOWN);
127 logger.debug("Start initializing Mycroft {}", thing.getUID());
129 String websocketID = thing.getUID().getAsString().replace(':', '-');
130 if (websocketID.length() < 4) {
131 websocketID = "mycroft-" + websocketID;
133 if (websocketID.length() > 20) {
134 websocketID = websocketID.substring(websocketID.length() - 20);
136 this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID));
138 config = getConfigAs(MycroftConfiguration.class);
139 if (config.host.isBlank()) {
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "No host defined");
142 } else if (config.port < 0 || config.port > 0xFFFF) {
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144 "Port should be between 0 and 65536");
147 scheduledFuture = scheduler.scheduleWithFixedDelay(this::checkOrstartWebsocket, 0, POLL_FREQUENCY_SEC,
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));
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);
162 registerChannel(new FullMessageChannel(this, messageTypesProperty));
164 checkLinkedChannelsAndRegisterMessageListeners();
167 private void checkLinkedChannelsAndRegisterMessageListeners() {
168 for (Entry<ChannelUID, MycroftChannel<?>> channelEntry : mycroftChannels.entrySet()) {
169 ChannelUID uid = channelEntry.getKey();
170 MycroftChannel<?> channel = channelEntry.getValue();
172 channel.registerListeners();
174 channel.unregisterListeners();
180 public void channelLinked(ChannelUID channelUID) {
181 checkLinkedChannelsAndRegisterMessageListeners();
185 public void channelUnlinked(ChannelUID channelUID) {
186 checkLinkedChannelsAndRegisterMessageListeners();
189 private void registerChannel(MycroftChannel<?> channel) {
190 mycroftChannels.put(channel.getChannelUID(), channel);
193 public void registerMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
194 this.connection.registerListener(messageType, listener);
197 public void unregisterMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
198 this.connection.unregisterListener(messageType, listener);
202 public void connectionEstablished() {
203 logger.debug("Mycroft thing {} is online", thing.getUID());
204 updateStatus(ThingStatus.ONLINE);
208 public void connectionLost(String reason) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
213 public void dispose() {
214 thingDisposing = true;
219 public <T extends State> void updateMyChannel(MycroftChannel<T> mycroftChannel, T state) {
220 updateState(mycroftChannel.getChannelUID(), state);
223 public boolean sendMessage(BaseMessage message) {
225 connection.sendMessage(message);
227 } catch (IOException e) {
228 logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
233 public boolean sendMessage(String message) {
235 connection.sendMessage(message);
237 } catch (IOException e) {
238 logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());