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.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;
53 * The {@link MycroftHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Gwendal Roulleau - Initial contribution
59 public class MycroftHandler extends BaseThingHandler implements MycroftConnectionListener {
61 private final Logger logger = LoggerFactory.getLogger(MycroftHandler.class);
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<>();
70 /** The reconnect frequency in case of error */
71 private static final int POLL_FREQUENCY_SEC = 30;
72 private int sometimesSendVolumeRequest = 0;
74 public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) {
76 this.webSocketFactory = webSocketFactory;
80 * Stops the API request or websocket reconnect timer
82 private void stopTimer() {
83 ScheduledFuture<?> future = scheduledFuture;
86 scheduledFuture = null;
91 * Starts the websocket connection.
92 * It sometimes also sends a get volume request to check the connection and refresh the volume.
94 private void checkOrstartWebsocket() {
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());
105 sometimesSendVolumeRequest++;
108 connection.start(config.host, config.port);
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());
118 channelCommand.handleCommand(command);
123 public void initialize() {
124 thingDisposing = false;
126 updateStatus(ThingStatus.UNKNOWN);
128 logger.debug("Start initializing Mycroft {}", thing.getUID());
130 String websocketID = ThingWebClientUtil.buildWebClientConsumerName(thing.getUID(), null);
131 this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID));
133 config = getConfigAs(MycroftConfiguration.class);
134 if (config.host.isBlank()) {
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "No host defined");
137 } else if (config.port < 0 || config.port > 0xFFFF) {
138 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139 "Port should be between 0 and 65536");
142 scheduledFuture = scheduler.scheduleWithFixedDelay(this::checkOrstartWebsocket, 0, POLL_FREQUENCY_SEC,
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));
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);
157 registerChannel(new FullMessageChannel(this, messageTypesProperty));
159 checkLinkedChannelsAndRegisterMessageListeners();
162 private void checkLinkedChannelsAndRegisterMessageListeners() {
163 for (Entry<ChannelUID, MycroftChannel<?>> channelEntry : mycroftChannels.entrySet()) {
164 ChannelUID uid = channelEntry.getKey();
165 MycroftChannel<?> channel = channelEntry.getValue();
167 channel.registerListeners();
169 channel.unregisterListeners();
175 public void channelLinked(ChannelUID channelUID) {
176 checkLinkedChannelsAndRegisterMessageListeners();
180 public void channelUnlinked(ChannelUID channelUID) {
181 checkLinkedChannelsAndRegisterMessageListeners();
184 private void registerChannel(MycroftChannel<?> channel) {
185 mycroftChannels.put(channel.getChannelUID(), channel);
188 public void registerMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
189 this.connection.registerListener(messageType, listener);
192 public void unregisterMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
193 this.connection.unregisterListener(messageType, listener);
197 public void connectionEstablished() {
198 logger.debug("Mycroft thing {} is online", thing.getUID());
199 updateStatus(ThingStatus.ONLINE);
203 public void connectionLost(String reason) {
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
208 public void dispose() {
209 thingDisposing = true;
214 public <T extends State> void updateMyChannel(MycroftChannel<T> mycroftChannel, T state) {
215 updateState(mycroftChannel.getChannelUID(), state);
218 public boolean sendMessage(BaseMessage message) {
220 connection.sendMessage(message);
222 } catch (IOException e) {
223 logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
228 public boolean sendMessage(String message) {
230 connection.sendMessage(message);
232 } catch (IOException e) {
233 logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());