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 m_pendingForAll = new ArrayList(); // 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 m_intents = new HashMap(); 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(); } }