mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-25 19:36:50 +01:00
Zepp OS: Implement Alexa service protocol
This commit is contained in:
parent
46dfd1040a
commit
3716a031ce
@ -352,4 +352,6 @@ public class DeviceSettingsPreferenceConst {
|
|||||||
public static final String PREF_HOURLY_CHIME_ENABLE = "hourly_chime_enable";
|
public static final String PREF_HOURLY_CHIME_ENABLE = "hourly_chime_enable";
|
||||||
public static final String PREF_HOURLY_CHIME_START = "hourly_chime_start";
|
public static final String PREF_HOURLY_CHIME_START = "hourly_chime_start";
|
||||||
public static final String PREF_HOURLY_CHIME_END = "hourly_chime_end";
|
public static final String PREF_HOURLY_CHIME_END = "hourly_chime_end";
|
||||||
|
|
||||||
|
public static final String PREF_VOICE_SERVICE_LANGUAGE = "voice_service_language";
|
||||||
}
|
}
|
||||||
|
@ -570,6 +570,8 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp
|
|||||||
addPreferenceHandlerFor(PREF_OFFLINE_VOICE_RESPONSE_DURING_SCREEN_LIGHTING);
|
addPreferenceHandlerFor(PREF_OFFLINE_VOICE_RESPONSE_DURING_SCREEN_LIGHTING);
|
||||||
addPreferenceHandlerFor(PREF_OFFLINE_VOICE_LANGUAGE);
|
addPreferenceHandlerFor(PREF_OFFLINE_VOICE_LANGUAGE);
|
||||||
|
|
||||||
|
addPreferenceHandlerFor(PREF_VOICE_SERVICE_LANGUAGE);
|
||||||
|
|
||||||
addPreferenceHandlerFor("lock");
|
addPreferenceHandlerFor("lock");
|
||||||
|
|
||||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||||
|
@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlexaService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||||
@ -336,6 +337,9 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
// Developer
|
// Developer
|
||||||
//
|
//
|
||||||
settings.add(R.xml.devicesettings_header_developer);
|
settings.add(R.xml.devicesettings_header_developer);
|
||||||
|
if (supportsAlexa(device)) {
|
||||||
|
settings.add(R.xml.devicesettings_huami2021_alexa);
|
||||||
|
}
|
||||||
if (supportsWifiHotspot(device)) {
|
if (supportsWifiHotspot(device)) {
|
||||||
settings.add(R.xml.devicesettings_wifi_hotspot);
|
settings.add(R.xml.devicesettings_wifi_hotspot);
|
||||||
}
|
}
|
||||||
@ -445,6 +449,10 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
return ZeppOsShortcutCardsService.isSupported(getPrefs(device));
|
return ZeppOsShortcutCardsService.isSupported(getPrefs(device));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean supportsAlexa(final GBDevice device) {
|
||||||
|
return ZeppOsAlexaService.isSupported(getPrefs(device));
|
||||||
|
}
|
||||||
|
|
||||||
private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) {
|
private boolean supportsConfig(final GBDevice device, final ZeppOsConfigService.ConfigArg config) {
|
||||||
return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config);
|
return ZeppOsConfigService.deviceHasConfig(getPrefs(device), config);
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
|||||||
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE, handler, prefs);
|
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.SHORTCUT_CARDS_SORTABLE, handler, prefs);
|
||||||
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.PREF_WATCHFACE, handler, prefs);
|
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.PREF_WATCHFACE, handler, prefs);
|
||||||
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.MORNING_UPDATES_CATEGORIES_SORTABLE, handler, prefs);
|
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.MORNING_UPDATES_CATEGORIES_SORTABLE, handler, prefs);
|
||||||
|
removeUnsupportedElementsFromListPreference(DeviceSettingsPreferenceConst.PREF_VOICE_SERVICE_LANGUAGE, handler, prefs);
|
||||||
|
|
||||||
for (final ZeppOsConfigService.ConfigArg config : ZeppOsConfigService.ConfigArg.values()) {
|
for (final ZeppOsConfigService.ConfigArg config : ZeppOsConfigService.ConfigArg.values()) {
|
||||||
if (config.getPrefKey() == null) {
|
if (config.getPrefKey() == null) {
|
||||||
@ -364,7 +365,11 @@ public class Huami2021SettingsCustomizer extends HuamiSettingsCustomizer {
|
|||||||
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_START),
|
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_START),
|
||||||
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_STOP),
|
handler.findPreference(DeviceSettingsPreferenceConst.WIFI_HOTSPOT_STOP),
|
||||||
handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_START),
|
handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_START),
|
||||||
handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_STOP)
|
handler.findPreference(DeviceSettingsPreferenceConst.FTP_SERVER_STOP),
|
||||||
|
// TODO: These are temporary for debugging and will be removed
|
||||||
|
handler.findPreference("zepp_os_alexa_btn_trigger"),
|
||||||
|
handler.findPreference("zepp_os_alexa_btn_send_simple"),
|
||||||
|
handler.findPreference("zepp_os_alexa_btn_send_complex")
|
||||||
);
|
);
|
||||||
|
|
||||||
for (final Preference btn : wifiFtpButtons) {
|
for (final Preference btn : wifiFtpButtons) {
|
||||||
|
@ -111,6 +111,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operati
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlexaService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
|
||||||
@ -152,6 +153,7 @@ public abstract class Huami2021Support extends HuamiSupport {
|
|||||||
private final ZeppOsCalendarService calendarService = new ZeppOsCalendarService(this);
|
private final ZeppOsCalendarService calendarService = new ZeppOsCalendarService(this);
|
||||||
private final ZeppOsCannedMessagesService cannedMessagesService = new ZeppOsCannedMessagesService(this);
|
private final ZeppOsCannedMessagesService cannedMessagesService = new ZeppOsCannedMessagesService(this);
|
||||||
private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileUploadService);
|
private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileUploadService);
|
||||||
|
private final ZeppOsAlexaService alexaService = new ZeppOsAlexaService(this);
|
||||||
|
|
||||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
||||||
put(fileUploadService.getEndpoint(), fileUploadService);
|
put(fileUploadService.getEndpoint(), fileUploadService);
|
||||||
@ -168,6 +170,7 @@ public abstract class Huami2021Support extends HuamiSupport {
|
|||||||
put(calendarService.getEndpoint(), calendarService);
|
put(calendarService.getEndpoint(), calendarService);
|
||||||
put(cannedMessagesService.getEndpoint(), cannedMessagesService);
|
put(cannedMessagesService.getEndpoint(), cannedMessagesService);
|
||||||
put(notificationService.getEndpoint(), notificationService);
|
put(notificationService.getEndpoint(), notificationService);
|
||||||
|
put(alexaService.getEndpoint(), alexaService);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
public Huami2021Support() {
|
public Huami2021Support() {
|
||||||
|
@ -0,0 +1,387 @@
|
|||||||
|
/* Copyright (C) 2023 José Rebelo
|
||||||
|
|
||||||
|
This file is part of Gadgetbridge.
|
||||||
|
|
||||||
|
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Gadgetbridge is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;
|
||||||
|
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VOICE_SERVICE_LANGUAGE;
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WATCHFACE;
|
||||||
|
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||||
|
|
||||||
|
public class ZeppOsAlexaService extends AbstractZeppOsService {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAlexaService.class);
|
||||||
|
|
||||||
|
private static final short ENDPOINT = 0x0011;
|
||||||
|
|
||||||
|
private static final byte CMD_START = 0x01;
|
||||||
|
private static final byte CMD_END = 0x02;
|
||||||
|
private static final byte CMD_START_ACK = 0x03;
|
||||||
|
private static final byte CMD_VOICE_DATA = 0x05;
|
||||||
|
private static final byte CMD_TRIGGERED = 0x06;
|
||||||
|
private static final byte CMD_REPLY_COMPLEX = 0x08;
|
||||||
|
private static final byte CMD_REPLY_SIMPLE = 0x09;
|
||||||
|
private static final byte CMD_REPLY_VOICE = 0x0a;
|
||||||
|
private static final byte CMD_REPLY_VOICE_MORE = 0x0b;
|
||||||
|
private static final byte CMD_ERROR = 0x0f;
|
||||||
|
private static final byte CMD_LANGUAGES_REQUEST = 0x10;
|
||||||
|
private static final byte CMD_LANGUAGES_RESPONSE = 0x11;
|
||||||
|
private static final byte CMD_SET_LANGUAGE = 0x12;
|
||||||
|
private static final byte CMD_SET_LANGUAGE_ACK = 0x13;
|
||||||
|
private static final byte CMD_CAPABILITIES_REQUEST = 0x20;
|
||||||
|
private static final byte CMD_CAPABILITIES_RESPONSE = 0x21;
|
||||||
|
|
||||||
|
private static final byte COMPLEX_REPLY_WEATHER = 0x01;
|
||||||
|
private static final byte COMPLEX_REPLY_REMINDER = 0x02;
|
||||||
|
private static final byte COMPLEX_REPLY_RICH_TEXT = 0x06;
|
||||||
|
|
||||||
|
private static final byte ERROR_NO_INTERNET = 0x03;
|
||||||
|
private static final byte ERROR_UNAUTHORIZED = 0x06;
|
||||||
|
|
||||||
|
public static final String PREF_VERSION = "zepp_os_alexa_version";
|
||||||
|
|
||||||
|
final ByteArrayOutputStream voiceBuffer = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
public ZeppOsAlexaService(final Huami2021Support support) {
|
||||||
|
super(support);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public short getEndpoint() {
|
||||||
|
return ENDPOINT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEncrypted() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handlePayload(final byte[] payload) {
|
||||||
|
switch (payload[0]) {
|
||||||
|
case CMD_START:
|
||||||
|
handleStart(payload);
|
||||||
|
break;
|
||||||
|
case CMD_END:
|
||||||
|
handleEnd(payload);
|
||||||
|
break;
|
||||||
|
case CMD_VOICE_DATA:
|
||||||
|
handleVoiceData(payload);
|
||||||
|
break;
|
||||||
|
case CMD_LANGUAGES_RESPONSE:
|
||||||
|
handleLanguagesResponse(payload);
|
||||||
|
break;
|
||||||
|
case CMD_SET_LANGUAGE_ACK:
|
||||||
|
LOG.info("Alexa set language ack, status = {}", payload[1]);
|
||||||
|
break;
|
||||||
|
case CMD_CAPABILITIES_RESPONSE:
|
||||||
|
handleCapabilitiesResponse(payload);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.warn("Unexpected alexa byte {}", String.format("0x%02x", payload[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSendConfiguration(final String config, final Prefs prefs) {
|
||||||
|
switch (config) {
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_VOICE_SERVICE_LANGUAGE:
|
||||||
|
final String alexaLanguage = prefs.getString(DeviceSettingsPreferenceConst.PREF_VOICE_SERVICE_LANGUAGE, null);
|
||||||
|
LOG.info("Setting alexa language to {}", alexaLanguage);
|
||||||
|
setLanguage(alexaLanguage);
|
||||||
|
return true;
|
||||||
|
case "zepp_os_alexa_btn_trigger":
|
||||||
|
GB.toast("Alexa cmd trigger", Toast.LENGTH_SHORT, GB.INFO);
|
||||||
|
sendCmdTriggered();
|
||||||
|
return true;
|
||||||
|
case "zepp_os_alexa_btn_send_simple":
|
||||||
|
GB.toast("Alexa simple reply", Toast.LENGTH_SHORT, GB.INFO);
|
||||||
|
final String simpleText = prefs.getString("zepp_os_alexa_reply_text", null);
|
||||||
|
sendReply(simpleText);
|
||||||
|
return true;
|
||||||
|
case "zepp_os_alexa_btn_send_complex":
|
||||||
|
GB.toast("Alexa complex reply", Toast.LENGTH_SHORT, GB.INFO);
|
||||||
|
final String title = prefs.getString("zepp_os_alexa_reply_title", null);
|
||||||
|
final String subtitle = prefs.getString("zepp_os_alexa_reply_subtitle", null);
|
||||||
|
final String text = prefs.getString("zepp_os_alexa_reply_text", null);
|
||||||
|
sendReply(title, subtitle, text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(final TransactionBuilder builder) {
|
||||||
|
requestCapabilities(builder);
|
||||||
|
requestLanguages(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestCapabilities(final TransactionBuilder builder) {
|
||||||
|
write(builder, CMD_CAPABILITIES_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestLanguages(final TransactionBuilder builder) {
|
||||||
|
write(builder, CMD_LANGUAGES_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReply(final String text) {
|
||||||
|
LOG.debug("Sending alexa simple text reply '{}'", text);
|
||||||
|
|
||||||
|
final byte[] textBytes = StringUtils.ensureNotNull(text).getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.allocate(textBytes.length + 2)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
buf.put(CMD_REPLY_SIMPLE);
|
||||||
|
buf.put(textBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
|
||||||
|
write("send simple text reply", buf.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReply(final String title, final String subtitle, final String text) {
|
||||||
|
LOG.debug("Sending alexa complex text reply '{}', '{}', '{}'", title, subtitle, text);
|
||||||
|
|
||||||
|
final byte[] titleBytes = StringUtils.ensureNotNull(title).getBytes(StandardCharsets.UTF_8);
|
||||||
|
final byte[] subtitleBytes = StringUtils.ensureNotNull(subtitle).getBytes(StandardCharsets.UTF_8);
|
||||||
|
final byte[] textBytes = StringUtils.ensureNotNull(text).getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
final int messageLength = titleBytes.length + subtitleBytes.length + textBytes.length + 3;
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.allocate(1 + 2 + 4 + messageLength)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
buf.put(CMD_REPLY_COMPLEX);
|
||||||
|
buf.putShort(COMPLEX_REPLY_RICH_TEXT);
|
||||||
|
buf.putInt(messageLength);
|
||||||
|
buf.put(titleBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
buf.put(subtitleBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
buf.put(textBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
|
||||||
|
write("send complex text reply", buf.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReply(final WeatherSpec weather) {
|
||||||
|
// TODO finish this
|
||||||
|
if (true) {
|
||||||
|
LOG.warn("Reply with weather not fully implemented");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
baos.write(0xfd); // ?
|
||||||
|
baos.write(0x03); // ?
|
||||||
|
baos.write(0x00); // ?
|
||||||
|
baos.write(0x00); // ?
|
||||||
|
|
||||||
|
baos.write(BLETypeConversions.fromUint32(weather.timestamp));
|
||||||
|
|
||||||
|
baos.write(StringUtils.ensureNotNull(weather.location).getBytes(StandardCharsets.UTF_8));
|
||||||
|
baos.write(0);
|
||||||
|
|
||||||
|
// FIXME long date string
|
||||||
|
baos.write(0);
|
||||||
|
|
||||||
|
baos.write(StringUtils.ensureNotNull(weather.currentCondition).getBytes(StandardCharsets.UTF_8));
|
||||||
|
baos.write(0);
|
||||||
|
|
||||||
|
// FIXME Second line for the condition
|
||||||
|
baos.write(0);
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
|
||||||
|
baos.write(weather.forecasts.size());
|
||||||
|
for (final WeatherSpec.Forecast forecast : weather.forecasts) {
|
||||||
|
// FIXME
|
||||||
|
}
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to encode weather payload", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.allocate(1 + 2 + 4 + baos.size())
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
buf.put(CMD_REPLY_COMPLEX);
|
||||||
|
buf.putShort(COMPLEX_REPLY_WEATHER);
|
||||||
|
buf.putInt(baos.size());
|
||||||
|
buf.put(baos.toByteArray());
|
||||||
|
|
||||||
|
write("send weather reply", buf.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReplyReminder() {
|
||||||
|
// TODO implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendReplyAlarm() {
|
||||||
|
// TODO implement
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendVoiceReply(final List<byte[]> voiceFrames) {
|
||||||
|
try {
|
||||||
|
final TransactionBuilder builder = getSupport().performInitialized("send voice reply");
|
||||||
|
|
||||||
|
for (final byte[] voiceFrame : voiceFrames) {
|
||||||
|
// TODO encode
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.queue(getSupport().getQueue());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LOG.error("Failed to send voice reply", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLanguage(final String language) {
|
||||||
|
if (language == null) {
|
||||||
|
LOG.warn("Alexa language is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] languageBytes = language.replace("_", "-").getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.allocate(languageBytes.length + 2)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
buf.put(CMD_SET_LANGUAGE);
|
||||||
|
buf.put(languageBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
|
||||||
|
write("set alexa language", buf.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendError(final byte errorCode, final String errorMessage) {
|
||||||
|
final byte[] messageBytes = StringUtils.ensureNotNull(errorMessage).getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
final ByteBuffer buf = ByteBuffer.allocate(messageBytes.length + 3)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
buf.put(CMD_ERROR);
|
||||||
|
buf.put(errorCode);
|
||||||
|
buf.put(messageBytes);
|
||||||
|
buf.put((byte) 0);
|
||||||
|
|
||||||
|
write("send alexa error", buf.array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendStartAck() {
|
||||||
|
write("send alexa start ack", new byte[]{CMD_START_ACK, 0x00});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendCmdTriggered() {
|
||||||
|
write("alexa cmd triggered", CMD_TRIGGERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendVoiceMore() {
|
||||||
|
write("alexa request more voice", CMD_REPLY_VOICE_MORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCapabilitiesResponse(final byte[] payload) {
|
||||||
|
final int version = payload[1] & 0xFF;
|
||||||
|
if (version != 3) {
|
||||||
|
LOG.warn("Unsupported alexa service version {}", version);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final byte var1 = payload[2];
|
||||||
|
if (var1 != 1) {
|
||||||
|
LOG.warn("Unexpected value for var1 '{}'", var1);
|
||||||
|
}
|
||||||
|
final byte var2 = payload[3];
|
||||||
|
if (var1 != 1) {
|
||||||
|
LOG.warn("Unexpected value for var2 '{}'", var2);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version));
|
||||||
|
|
||||||
|
LOG.info("Alexa version={}, var1={}, var2={}", version, var1, var2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStart(final byte[] payload) {
|
||||||
|
final byte var1 = payload[1];
|
||||||
|
final byte var2 = payload[2];
|
||||||
|
final byte var3 = payload[3];
|
||||||
|
final byte var4 = payload[4];
|
||||||
|
final String params = StringUtils.untilNullTerminator(payload, 5);
|
||||||
|
|
||||||
|
LOG.info("Alexa starting: var1={}, var2={}, var3={}, var4={}, params={}", var1, var2, var3, var4, params);
|
||||||
|
|
||||||
|
sendStartAck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleEnd(final byte[] payload) {
|
||||||
|
voiceBuffer.reset();
|
||||||
|
// TODO do something else?
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleVoiceData(final byte[] payload) {
|
||||||
|
LOG.info("Got {} bytes of voice data", payload.length);
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleLanguagesResponse(final byte[] payload) {
|
||||||
|
int pos = 2;
|
||||||
|
final String currentLanguage = StringUtils.untilNullTerminator(payload, pos);
|
||||||
|
pos = pos + currentLanguage.length() + 1;
|
||||||
|
|
||||||
|
final int numLanguages = payload[pos++] & 0xFF;
|
||||||
|
final List<String> allLanguages = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < numLanguages; i++) {
|
||||||
|
final String language = StringUtils.untilNullTerminator(payload, pos);
|
||||||
|
allLanguages.add(language);
|
||||||
|
pos = pos + language.length() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info("Got alexa language = {}, supported languages = {}", currentLanguage, allLanguages);
|
||||||
|
|
||||||
|
final GBDeviceEventUpdatePreferences evt = new GBDeviceEventUpdatePreferences()
|
||||||
|
.withPreference(PREF_VOICE_SERVICE_LANGUAGE, currentLanguage.replace("-", "_"))
|
||||||
|
.withPreference(Huami2021Coordinator.getPrefPossibleValuesKey(PREF_VOICE_SERVICE_LANGUAGE), String.join(",", allLanguages).replace("-", "_"));
|
||||||
|
getSupport().evaluateGBDeviceEvent(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSupported(final Prefs devicePrefs) {
|
||||||
|
return devicePrefs.getInt(PREF_VERSION, 0) == 3;
|
||||||
|
}
|
||||||
|
}
|
@ -2116,4 +2116,9 @@
|
|||||||
<string name="contact_phone_number">Phone number</string>
|
<string name="contact_phone_number">Phone number</string>
|
||||||
<string name="contact_missing_name">Contact name is empty</string>
|
<string name="contact_missing_name">Contact name is empty</string>
|
||||||
<string name="contact_missing_number">Contact number is empty</string>
|
<string name="contact_missing_number">Contact number is empty</string>
|
||||||
|
<string name="voice_service_package_title">Voice service package</string>
|
||||||
|
<string name="voice_service_package_summary">Application that contains the service handling voice commands</string>
|
||||||
|
<string name="voice_service_class_title">Voice service class</string>
|
||||||
|
<string name="voice_service_class_summary">Full service path handling voice commands</string>
|
||||||
|
<string name="voice_service">Voice Service</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -173,14 +173,14 @@
|
|||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="voice_service_package"
|
android:key="voice_service_package"
|
||||||
android:title="Voice service package"
|
android:title="@string/voice_service_package_title"
|
||||||
android:summary="Application that contains the service handling voice commands"
|
android:summary="@string/voice_service_package_summary"
|
||||||
app:useSimpleSummaryProvider="true"/>
|
app:useSimpleSummaryProvider="true"/>
|
||||||
|
|
||||||
<EditTextPreference
|
<EditTextPreference
|
||||||
android:key="voice_service_class"
|
android:key="voice_service_class"
|
||||||
android:title="Voice service full path"
|
android:title="@string/voice_service_class_title"
|
||||||
android:summary="Full service path handling voice commands"
|
android:summary="@string/voice_service_class_summary"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
|
82
app/src/main/res/xml/devicesettings_huami2021_alexa.xml
Normal file
82
app/src/main/res/xml/devicesettings_huami2021_alexa.xml
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceScreen
|
||||||
|
android:icon="@drawable/ic_voice"
|
||||||
|
android:key="pref_screen_alexa"
|
||||||
|
android:title="@string/menuitem_alexa">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="auto"
|
||||||
|
android:entries="@array/pref_language_all"
|
||||||
|
android:entryValues="@array/pref_language_all_values"
|
||||||
|
android:icon="@drawable/ic_language"
|
||||||
|
android:key="voice_service_language"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="@string/pref_title_language" />
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="pref_header_voice_service"
|
||||||
|
android:title="@string/voice_service">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="voice_service_package"
|
||||||
|
android:summary="@string/voice_service_package_summary"
|
||||||
|
android:title="@string/voice_service_package_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:key="voice_service_class"
|
||||||
|
android:summary="@string/voice_service_class_summary"
|
||||||
|
android:title="@string/voice_service_class_title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
<!-- Keeping these without a translation intentionally, they should be temporary -->
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="pref_header_voice_service_debug"
|
||||||
|
android:title="@string/action_debug">
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:defaultValue=""
|
||||||
|
android:key="zepp_os_alexa_reply_title"
|
||||||
|
android:title="Reply Title"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:defaultValue=""
|
||||||
|
android:key="zepp_os_alexa_reply_subtitle"
|
||||||
|
android:title="Reply Subtitle"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<EditTextPreference
|
||||||
|
android:defaultValue=""
|
||||||
|
android:key="zepp_os_alexa_reply_text"
|
||||||
|
android:title="Reply Text"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="zepp_os_alexa_ask_more_input"
|
||||||
|
android:summary="Continue listening after sending the reply"
|
||||||
|
android:title="Ask for more input" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="zepp_os_alexa_btn_trigger"
|
||||||
|
android:summary="Send a command trigger to the watch (stops sending voice data)"
|
||||||
|
android:title="Send command trigger" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="zepp_os_alexa_btn_send_simple"
|
||||||
|
android:summary="Send a simple reply to the watch (just text)"
|
||||||
|
android:title="Send simple reply" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="zepp_os_alexa_btn_send_complex"
|
||||||
|
android:summary="Send a complex reply to the watch (title, subtitle, text)"
|
||||||
|
android:title="Send complex reply" />
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
|
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user