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) {
71 if (message.type == MessageType.mycroft_volume_get_response) {
72 float volumeGet = ((MessageVolumeGetResponse) message).data.percent;
73 updateAndSaveMyState(normalizeVolume(volumeGet));
74 } else if (message.type == MessageType.mycroft_volume_set) {
75 float volumeSet = ((MessageVolumeSet) message).data.percent;
76 updateAndSaveMyState(normalizeVolume(volumeSet));
77 } else if (message.type == MessageType.mycroft_volume_duck) {
78 updateAndSaveMyState(new PercentType(0));
79 } else if (message.type == MessageType.mycroft_volume_unduck) {
80 updateAndSaveMyState(lastNonZeroVolume);
81 } else if (message.type == MessageType.mycroft_volume_increase) {
82 updateAndSaveMyState(normalizeVolume(lastVolume.intValue() + 10));
83 } else if (message.type == MessageType.mycroft_volume_decrease) {
84 updateAndSaveMyState(normalizeVolume(lastVolume.intValue() - 10));
88 protected final void updateAndSaveMyState(State state) {
89 if (state instanceof PercentType volume) {
90 this.lastVolume = volume;
91 if (volume.intValue() > 0) {
92 this.lastNonZeroVolume = volume;
95 super.updateMyState(state);
99 * Protection method for volume with
100 * potentially wrong value.
102 * @param volume The requested volume, on a scale from 0 to 100.
103 * Could be out of bond, then it will be corrected.
104 * @return A safe volume in PercentType between 0 and 100
106 private PercentType normalizeVolume(int volume) {
108 return PercentType.HUNDRED;
109 } else if (volume <= 0) {
110 return PercentType.ZERO;
112 return new PercentType(volume);
117 * Protection method for volume with
118 * potentially wrong value.
120 * @param volume The requested volume, on a scale from 0 to 1.
121 * @return A safe volume in PercentType between 0 and 100
123 private PercentType normalizeVolume(float volume) {
125 return PercentType.HUNDRED;
126 } else if (volume <= 0) {
127 return PercentType.ZERO;
129 return new PercentType(Math.round(volume * 100));
133 public float toMycroftVolume(PercentType percentType) {
134 return Float.valueOf(percentType.intValue());
137 public PercentType computeNewVolume(int valueAdded) {
138 return new PercentType(lastVolume.intValue() + valueAdded);
141 private boolean sendSetMessage(float volume) {
142 String messageToSend = VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME", Float.toString(volume));
143 return handler.sendMessage(messageToSend);
147 public void handleCommand(Command command) {
148 if (command instanceof OnOffType) {
149 if (command == OnOffType.ON) {
150 if (sendSetMessage(toMycroftVolume(lastNonZeroVolume))) {
151 updateAndSaveMyState(lastNonZeroVolume);
154 if (command == OnOffType.OFF) {
155 if (sendSetMessage(0)) {
156 updateAndSaveMyState(PercentType.ZERO);
159 } else if (command instanceof IncreaseDecreaseType) {
160 if (command == IncreaseDecreaseType.INCREASE) {
161 if (handler.sendMessage(new MessageVolumeIncrease())) {
162 updateAndSaveMyState(computeNewVolume(10));
165 if (command == IncreaseDecreaseType.DECREASE) {
166 handler.sendMessage(new MessageVolumeDecrease());
167 updateAndSaveMyState(computeNewVolume(-10));
169 } else if (command instanceof PercentType volume) {
170 sendSetMessage(toMycroftVolume(volume));
171 updateAndSaveMyState(volume);
172 } else if (command instanceof RefreshType) {
173 handler.sendMessage(new MessageVolumeGet());