nymea-app/androidservice/java/io/guh/nymeaapp/NymeaAppControlService.java

290 lines
13 KiB
Java

package io.guh.nymeaapp;
import android.util.Log;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.ComponentName;
import android.app.PendingIntent;
import android.net.Uri;
import android.content.Context;
import android.service.controls.ControlsProviderService;
import android.service.controls.actions.*;
import android.service.controls.Control;
import android.service.controls.DeviceTypes;
import android.service.controls.templates.*;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import java.util.concurrent.Flow.Publisher;
import java.util.function.Consumer;
import java.util.List;
import java.util.ArrayList;
import java.util.UUID;
import java.util.HashMap;
import io.reactivex.Flowable;
import io.reactivex.processors.ReplayProcessor;
import org.reactivestreams.FlowAdapters;
import org.json.*;
// Android device controls service
// This service is instantiated by the android device controls on demand. It will
// connect to the NymeaAppService and interact with nymea through that.
public class NymeaAppControlService extends ControlsProviderService {
private String TAG = "nymea-app: NymeaAppControlService";
private NymeaAppServiceConnection m_serviceConnection;
// For publishing all available
private ReplayProcessor m_publisherForAll;
private ArrayList<UUID> m_pendingForAll = new ArrayList<UUID>(); // pending nymea ids to query
private ReplayProcessor m_updatePublisher;
private List m_activeControlIds;
private void ensureServiceConnection() {
if (m_serviceConnection == null) {
m_serviceConnection = new NymeaAppServiceConnection(getBaseContext()) {
@Override public void onConnectedChanged(boolean connected) {
Log.d(TAG, "Connected to NymeaAppService. Known hosts: " + m_serviceConnection.getHosts().size());
if (connected && m_publisherForAll != null) {
Log.d(TAG, "Processing all");
processAll();
}
}
@Override public void onReadyChanged(UUID nymeaId, boolean ready) {
Log.d(TAG, "Nymea instance " + nymeaId.toString() + " ready state changed: " + Boolean.toString(ready));
if (ready) {
process(nymeaId);
}
}
@Override public void onUpdate(UUID nymeaId, UUID thingId) {
if (m_updatePublisher != null && m_activeControlIds.contains(thingId.toString())) {
// Log.d(TAG, "Updating publisher for thing: " + thingId);
m_updatePublisher.onNext(thingToControl(nymeaId, thingId));
// m_updatePublisher.onComplete();
}
}
};
}
Intent serviceIntent = new Intent(this, NymeaAppService.class);
bindService(serviceIntent, m_serviceConnection, Context.BIND_AUTO_CREATE);
}
private void processAll() {
ensureServiceConnection();
if (m_serviceConnection.connected()) {
// Need to add all the pending before processing
if (m_publisherForAll != null) {
for (UUID nymeaId: m_serviceConnection.getHosts().keySet()) {
m_pendingForAll.add(nymeaId);
}
}
for (UUID nymeaId: m_serviceConnection.getHosts().keySet()) {
if (m_serviceConnection.getHosts().get(nymeaId).isReady) {
process(nymeaId);
}
}
} else {
Log.d(TAG, "Not connected to NymeaAppService yet...");
}
}
private void process(UUID nymeaId) {
Log.d(TAG, "Processing...");
ensureServiceConnection();
if (!m_serviceConnection.connected()) {
Log.d(TAG, "NymeaAppService not connected to nymea instance " + nymeaId + " yet.");
return;
}
if (!m_serviceConnection.getHosts().keySet().contains(nymeaId)) {
Log.d(TAG, "Service connection is not ready yet...");
return;
}
for (Thing thing : m_serviceConnection.getHosts().get(nymeaId).things.values()) {
Log.d(TAG, "Processing thing: " + thing.name);
if (m_publisherForAll != null) {
Log.d(TAG, "Adding stateless");
m_publisherForAll.onNext(thingToControl(nymeaId, thing.id));
}
if (m_updatePublisher != null) {
if (m_activeControlIds.contains(thing.id.toString())) {
Log.d(TAG, "Adding stateful");
m_updatePublisher.onNext(thingToControl(nymeaId, thing.id));
}
}
}
if (m_pendingForAll.contains(nymeaId)) {
m_pendingForAll.remove(nymeaId);
}
// The publisher for all needs to be completed when done
if (m_publisherForAll != null && m_pendingForAll.isEmpty()) {
Log.d(TAG, "Completing all publisher");
m_publisherForAll.onComplete();
}
Log.d(TAG, "Done processing");
// We never close the update publisher as we need that one to send updates
}
@Override
public Publisher createPublisherForAllAvailable() {
Log.d(TAG, "Creating publishers for all");
m_publisherForAll = ReplayProcessor.create();
processAll();
return FlowAdapters.toFlowPublisher(m_publisherForAll);
}
@Override
public Publisher createPublisherFor(List controlIds) {
Log.d(TAG, "Creating publishers for " + Integer.toString(controlIds.size()));
m_updatePublisher = ReplayProcessor.create();
m_activeControlIds = controlIds;
processAll();
return FlowAdapters.toFlowPublisher(m_updatePublisher);
}
@Override
public void performControlAction(String controlId, ControlAction action, Consumer consumer) {
Log.d(TAG, "Performing control action: " + controlId);
UUID nymeaId = m_serviceConnection.hostForThing(UUID.fromString(controlId));
if (nymeaId == null) {
Log.d(TAG, "Nymea host not found for thing id: " + controlId);
consumer.accept(ControlAction.RESPONSE_FAIL);
return;
}
Thing thing = m_serviceConnection.getThing(UUID.fromString(controlId));
if (thing == null) {
Log.d(TAG, "Thing not found for id: " + controlId);
consumer.accept(ControlAction.RESPONSE_FAIL);
return;
}
UUID actionTypeId;
String param;
if (thing.interfaces.contains("dimmablelight") && action instanceof FloatAction) {
actionTypeId = thing.stateByName("brightness").typeId;
FloatAction fAction = (FloatAction) action;
param = String.valueOf(Math.round(fAction.getNewValue()));
} else if (thing.interfaces.contains("power") && action instanceof BooleanAction) {
actionTypeId = thing.stateByName("power").typeId;
BooleanAction bAction = (BooleanAction) action;
param = bAction.getNewState() == true ? "true" : "false";
} else if (thing.interfaces.contains("closable") && action instanceof BooleanAction) {
BooleanAction bAction = (BooleanAction) action;
if (bAction.getNewState()) {
Log.d(TAG, "executing open");
actionTypeId = thing.actionByName("open").typeId;
} else {
Log.d(TAG, "executing close");
actionTypeId = thing.actionByName("close").typeId;
}
param = "";
} else if (thing.interfaces.contains("volumecontroller") && thing.stateByName("volume") != null) {
actionTypeId = thing.stateByName("volume").typeId;
FloatAction fAction = (FloatAction) action;
param = String.valueOf(Math.round(fAction.getNewValue()));
} else {
Log.d(TAG, "Unhandled action for: " + thing.name);
consumer.accept(ControlAction.RESPONSE_FAIL);
return;
}
m_serviceConnection.executeAction(nymeaId, thing.id, actionTypeId, param);
consumer.accept(ControlAction.RESPONSE_OK);
}
private HashMap<UUID, Integer> m_intents = new HashMap<UUID, Integer>();
private Control thingToControl(UUID nymeaId, UUID thingId) {
// Log.d(TAG, "Creating control for thing: " + thing.name + " id: " + thing.id);
NymeaHost nymeaHost = m_serviceConnection.getHosts().get(nymeaId);
Thing thing = nymeaHost.things.get(thingId);
// NOTE: intentId 1 doesn't work for some reason I don't understand yet...
// so let's make sure we never add "1" to it by always added 100
int intentId = m_intents.size() + 100;
PendingIntent pi;
if (m_intents.containsKey(thing.id)) {
intentId = m_intents.get(thing.id);
} else {
m_intents.put(thing.id, intentId);
}
Context context = getBaseContext();
Intent intent = new Intent(context, NymeaAppControlsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
intent.putExtra("nymeaId", nymeaId.toString());
intent.putExtra("thingId", thing.id.toString());
pi = PendingIntent.getActivity(context, intentId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Log.d(TAG, "Created pendingintent for " + thing.name + " with id " + intentId + " and extra " + thing.id);
Control.StatefulBuilder builder = new Control.StatefulBuilder(thing.id.toString(), pi)
.setTitle(thing.name)
.setSubtitle(thing.className)
.setStructure(nymeaHost.name);
if (thing.interfaces.contains("impulsebasedgaragedoor")) {
builder.setDeviceType(DeviceTypes.TYPE_GARAGE);
builder.setControlTemplate(new StatelessTemplate(thing.id.toString()));
} else if (thing.interfaces.contains("statefulgaragedoor")) {
builder.setDeviceType(DeviceTypes.TYPE_GARAGE);
State stateState = thing.stateByName("state");
ControlButton controlButton = new ControlButton(stateState.value.equals("open"), stateState.displayName);
builder.setControlTemplate(new ToggleTemplate(thing.id.toString(), controlButton));
// } else if (thing.interfaces.contains("extendedstatefulgaragedoor")) {
// builder.setDeviceTyoe(DeviceTypes.TYPE_GARAGE);
} else if (thing.interfaces.contains("light")) {
builder.setDeviceType(DeviceTypes.TYPE_LIGHT);
State powerState = thing.stateByName("power");
ControlButton controlButton = new ControlButton(powerState.value.equals("true"), powerState.displayName);
if (thing.interfaces.contains("dimmablelight")) {
State brightnessState = thing.stateByName("brightness");
RangeTemplate rangeTemplate = new RangeTemplate(thing.id.toString(), 0, 100, Float.parseFloat(brightnessState.value), 1, brightnessState.displayName);
builder.setControlTemplate(new ToggleRangeTemplate(thing.id.toString(), controlButton, rangeTemplate));
} else {
builder.setControlTemplate(new ToggleTemplate(thing.id.toString(), controlButton));
}
} else if (thing.interfaces.contains("powersocket")) {
builder.setDeviceType(DeviceTypes.TYPE_OUTLET);
State powerState = thing.stateByName("power");
ControlButton controlButton = new ControlButton(powerState.value.equals("true"), powerState.displayName);
builder.setControlTemplate(new ToggleTemplate(thing.id.toString(), controlButton));
} else if (thing.interfaces.contains("mediaplayer")) {
if (thing.stateByName("playerType").value == "video") {
builder.setDeviceType(DeviceTypes.TYPE_TV);
} else {
// FIXME: There doesn't seem to be a speaker DeviceType!?!
builder.setDeviceType(DeviceTypes.TYPE_TV);
}
if (thing.interfaces.contains("volumecontroller")) {
State volumeState = thing.stateByName("volume");
if (volumeState != null) {
RangeTemplate rangeTemplate = new RangeTemplate(thing.id.toString(), 0, 100, Float.parseFloat(volumeState.value), 1, volumeState.displayName);
builder.setControlTemplate(rangeTemplate);
}
}
} else {
builder.setDeviceType(DeviceTypes.TYPE_GENERIC_ON_OFF);
}
builder.setStatus(Control.STATUS_OK);
// Log.d(TAG, "Created control for thing: " + thing.name + " id: " + thing.id);
return builder.build();
}
}