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.channels;
15 import java.util.Arrays;
16 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
20 import org.openhab.binding.mycroft.internal.MycroftHandler;
21 import org.openhab.binding.mycroft.internal.api.MessageType;
22 import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
23 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDecrease;
24 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
25 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGetResponse;
26 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeIncrease;
27 import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet;
28 import org.openhab.core.library.types.IncreaseDecreaseType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.PercentType;
31 import org.openhab.core.types.Command;
32 import org.openhab.core.types.RefreshType;
33 import org.openhab.core.types.State;
36 * The channel responsible for handling the volume of the Mycroft speaker
37 * QUITE FUNCTIONAL but with workaround
38 * (see https://community.mycroft.ai/t/openhab-plugin-development-audio-volume-message-types-missing/10576
39 * and https://github.com/MycroftAI/skill-volume/issues/53)
41 * @author Gwendal Roulleau - Initial contribution
44 public class VolumeChannel extends MycroftChannel<State> {
47 * As the MessageVolumeSet is, contrary to the documentation, not listened to by Mycroft,
48 * we use a workaround and send a message simulating an intent detection
50 public static final String VOLUME_SETTER_MESSAGE = "{\"type\": \"mycroft-volume.mycroftai:SetVolume\", \"data\": {\"intent_type\": \"mycroft-volume.mycroftai:SetVolume\", \"mycroft_volume_mycroftaiVolume\": \"volume\", \"mycroft_volume_mycroftaiLevel\": \"$$VOLUME\", \"mycroft_volume_mycroftaiTo\": \"to\", \"target\": null, \"confidence\": 0.6000000000000001, \"__tags__\": [{\"match\": \"volume\", \"key\": \"volume\", \"start_token\": 1, \"entities\": [{\"key\": \"volume\", \"match\": \"volume\", \"data\": [[\"volume\", \"mycroft_volume_mycroftaiVolume\"]], \"confidence\": 1.0}], \"end_token\": 1, \"from_context\": false}, {\"match\": \"$$VOLUME\", \"key\": \"$$VOLUME\", \"start_token\": 3, \"entities\": [{\"key\": \"$$VOLUME\", \"match\": \"$$VOLUME\", \"data\": [[\"$$VOLUME\", \"mycroft_volume_mycroftaiLevel\"]], \"confidence\": 1.0}], \"end_token\": 3, \"from_context\": false}, {\"match\": \"to\", \"key\": \"to\", \"start_token\": 2, \"entities\": [{\"key\": \"to\", \"match\": \"to\", \"data\": [[\"to\", \"mycroft_volume_mycroftaiTo\"]], \"confidence\": 1.0}], \"end_token\": 2, \"from_context\": false}], \"utterance\": \"set volume to $$VOLUME\", \"utterances\": [\"set volume to X\"]}, \"context\": {\"client_name\": \"mycroft_cli\", \"source\": [\"skills\"], \"destination\": \"debug_cli\"}}";
52 private PercentType lastVolume = new PercentType(50);
53 private PercentType lastNonZeroVolume = new PercentType(50);
55 public VolumeChannel(MycroftHandler handler) {
56 super(handler, MycroftBindingConstants.VOLUME_CHANNEL);
60 public List<MessageType> getMessageToListenTo() {
61 // we don't listen to mute/unmute message because duck/unduck seems sufficient
62 // and we don't want to change state twice for the same event
63 // but it should be tested on mark I, as volume is handled differently
64 return Arrays.asList(MessageType.mycroft_volume_get_response, MessageType.mycroft_volume_set,
65 MessageType.mycroft_volume_increase, MessageType.mycroft_volume_decrease,
66 MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck);
70 public void messageReceived(BaseMessage message) {
72 if (message.type == MessageType.mycroft_volume_get_response) {
73 float volumeGet = ((MessageVolumeGetResponse) message).data.percent;
74 updateAndSaveMyState(normalizeVolume(volumeGet));
75 } else if (message.type == MessageType.mycroft_volume_set) {
76 float volumeSet = ((MessageVolumeSet) message).data.percent;
77 updateAndSaveMyState(normalizeVolume(volumeSet));
78 } else if (message.type == MessageType.mycroft_volume_duck) {
79 updateAndSaveMyState(new PercentType(0));
80 } else if (message.type == MessageType.mycroft_volume_unduck) {
81 updateAndSaveMyState(lastNonZeroVolume);
82 } else if (message.type == MessageType.mycroft_volume_increase) {
83 updateAndSaveMyState(normalizeVolume(lastVolume.intValue() + 10));
84 } else if (message.type == MessageType.mycroft_volume_decrease) {
85 updateAndSaveMyState(normalizeVolume(lastVolume.intValue() - 10));
89 protected final void updateAndSaveMyState(State state) {
90 if (state instanceof PercentType) {
91 this.lastVolume = ((PercentType) state);
92 if (((PercentType) state).intValue() > 0) {
93 this.lastNonZeroVolume = ((PercentType) state);
96 super.updateMyState(state);
100 * Protection method for volume with
101 * potentially wrong value.
103 * @param volume The requested volume, on a scale from 0 to 100.
104 * Could be out of bond, then it will be corrected.
105 * @return A safe volume in PercentType between 0 and 100
107 private PercentType normalizeVolume(int volume) {
109 return PercentType.HUNDRED;
110 } else if (volume <= 0) {
111 return PercentType.ZERO;
113 return new PercentType(volume);
118 * Protection method for volume with
119 * potentially wrong value.
121 * @param volume The requested volume, on a scale from 0 to 1.
122 * @return A safe volume in PercentType between 0 and 100
124 private PercentType normalizeVolume(float volume) {
126 return PercentType.HUNDRED;
127 } else if (volume <= 0) {
128 return PercentType.ZERO;
130 return new PercentType(Math.round(volume * 100));
134 public float toMycroftVolume(PercentType percentType) {
135 return Float.valueOf(percentType.intValue());
138 public PercentType computeNewVolume(int valueAdded) {
139 return new PercentType(lastVolume.intValue() + valueAdded);
142 private boolean sendSetMessage(float volume) {
143 String messageToSend = VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME", Float.valueOf(volume).toString());
144 return handler.sendMessage(messageToSend);
148 public void handleCommand(Command command) {
149 if (command instanceof OnOffType) {
150 if (command == OnOffType.ON) {
151 if (sendSetMessage(toMycroftVolume(lastNonZeroVolume))) {
152 updateAndSaveMyState(lastNonZeroVolume);
155 if (command == OnOffType.OFF) {
156 if (sendSetMessage(0)) {
157 updateAndSaveMyState(PercentType.ZERO);
160 } else if (command instanceof IncreaseDecreaseType) {
161 if (command == IncreaseDecreaseType.INCREASE) {
162 if (handler.sendMessage(new MessageVolumeIncrease())) {
163 updateAndSaveMyState(computeNewVolume(10));
166 if (command == IncreaseDecreaseType.DECREASE) {
167 handler.sendMessage(new MessageVolumeDecrease());
168 updateAndSaveMyState(computeNewVolume(-10));
170 } else if (command instanceof PercentType) {
171 sendSetMessage(toMycroftVolume((PercentType) command));
172 updateAndSaveMyState((PercentType) command);
173 } else if (command instanceof RefreshType) {
174 handler.sendMessage(new MessageVolumeGet());