package com.kuxhausen.huemore.net.hue;
import com.google.gson.Gson;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.os.SystemClock;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.Volley;
import com.kuxhausen.huemore.R;
import com.kuxhausen.huemore.net.Connection;
import com.kuxhausen.huemore.net.DeviceManager;
import com.kuxhausen.huemore.net.NetworkBulb;
import com.kuxhausen.huemore.net.NetworkBulb.ConnectivityState;
import com.kuxhausen.huemore.net.hue.api.BulbAttributes;
import com.kuxhausen.huemore.net.hue.api.BulbAttributesSuccessListener.OnBulbAttributesReturnedListener;
import com.kuxhausen.huemore.net.hue.api.BulbListSuccessListener.OnBulbListReturnedListener;
import com.kuxhausen.huemore.net.hue.api.ConnectionMonitor;
import com.kuxhausen.huemore.net.hue.api.NetworkMethods;
import com.kuxhausen.huemore.persistence.Definitions.NetBulbColumns;
import com.kuxhausen.huemore.persistence.Definitions.NetConnectionColumns;
import com.kuxhausen.huemore.state.BulbState;
import com.kuxhausen.huemore.utils.DeferredLog;
import com.kuxhausen.huemore.utils.RateLimiter;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import alt.android.os.CountDownTimer;
public class HubConnection implements Connection, OnBulbAttributesReturnedListener,
ConnectionMonitor, OnBulbListReturnedListener {
private static final String[] columns = {NetConnectionColumns._ID,
NetConnectionColumns.TYPE_COLUMN,
NetConnectionColumns.NAME_COLUMN,
NetConnectionColumns.DEVICE_ID_COLUMN,
NetConnectionColumns.JSON_COLUMN};
private static final String[] bulbColumns = {NetBulbColumns._ID,
NetBulbColumns.CONNECTION_DATABASE_ID,
NetBulbColumns.TYPE_COLUMN,
NetBulbColumns.NAME_COLUMN,
NetBulbColumns.DEVICE_ID_COLUMN,
NetBulbColumns.JSON_COLUMN};
private static final Integer TYPE = NetBulbColumns.NetBulbType.PHILIPS_HUE;
private static final Gson gson = new Gson();
//In milis
private final static long TRANSMIT_TIMEOUT_TIME = 10000;
// In SystemClock.elapsedRealtime();
private Long mDesiredLastChanged;
private Long mBaseId;
private String mName, mDeviceId;
HubData mData;
private Context mContext;
private ArrayList<HueBulb> mBulbList;
private ArrayList<Route> myRoutes;
public ChangeLoopManager mLoopManager;
private DeviceManager mDeviceManager;
private RequestQueue volleyRQ;
public HubConnection(Context c, Long baseId, String name, String deviceId, HubData data,
DeviceManager dm) {
mContext = c;
mBaseId = baseId;
mName = name;
mDeviceId = deviceId;
mData = data;
mBulbList = new ArrayList<HueBulb>();
mLoopManager = new ChangeLoopManager();
myRoutes = new ArrayList<Route>();
String selection =
NetBulbColumns.TYPE_COLUMN + " = ? AND " + NetBulbColumns.CONNECTION_DATABASE_ID + " = ?";
String[] selectionArgs = {"" + TYPE, "" + mBaseId};
Cursor cursor =
c.getContentResolver().query(NetBulbColumns.URI, bulbColumns, selection, selectionArgs,
null);
cursor.moveToPosition(-1);// not the same as move to first!
while (cursor.moveToNext()) {
Long bulbBaseId = cursor.getLong(0);
String bulbName = cursor.getString(3);
String bulbDeviceId = cursor.getString(4);
HueBulbData bulbData = gson.fromJson(cursor.getString(5), HueBulbData.class);
mBulbList.add(new HueBulb(c, bulbBaseId, bulbName, bulbDeviceId, bulbData, this));
}
cursor.close();
// junk?
mDeviceManager = dm;
volleyRQ = Volley.newRequestQueue(mContext);
// initalized state
this.mDeviceManager.onStateChanged();
}
@Override
public void initializeConnection(Context c) {
myRoutes.clear();
if (mData != null && mData.localHubAddress != null) {
myRoutes.add(new Route(mData.localHubAddress, ConnectivityState.Unknown));
}
if (mData != null && mData.portForwardedAddress != null) {
myRoutes.add(new Route(mData.portForwardedAddress, ConnectivityState.Unknown));
}
getLooper().queueGetList();
for (HueBulb b : this.mBulbList) {
getLooper().queueGetState(b);
}
}
public List<Route> getBestRoutes() {
ConnectivityState bestSoFar = ConnectivityState.Unreachable;
ArrayList<Route> result = new ArrayList<Route>();
for (Route route : myRoutes) {
if (route.state == bestSoFar && route.state != ConnectivityState.Connected) {
result.add(route);
} else if (route.isMoreConnectedThan(bestSoFar)) {
result.clear();
result.add(route);
}
}
return result;
}
public DeviceManager getDeviceManager() {
return mDeviceManager;
}
@Override
public void onDestroy() {
mLoopManager.onDestroy();
volleyRQ.cancelAll("");
mBulbList = null;
}
public static ArrayList<HubConnection> loadHubConnections(Context c, DeviceManager dm) {
ArrayList<HubConnection> hubs = new ArrayList<HubConnection>();
String[] selectionArgs = {"" + NetBulbColumns.NetBulbType.PHILIPS_HUE};
Cursor cursor =
c.getContentResolver().query(NetConnectionColumns.URI, columns,
NetConnectionColumns.TYPE_COLUMN + " = ?", selectionArgs,
null);
cursor.moveToPosition(-1);// not the same as move to first!
while (cursor.moveToNext()) {
Long baseId = cursor.getLong(0);
String name = cursor.getString(2);
String deviceId = cursor.getString(3);
HubData data = gson.fromJson(cursor.getString(4), HubData.class);
hubs.add(new HubConnection(c, baseId, name, deviceId, data, dm));
}
cursor.close();
// initialize all connections
for (HubConnection h : hubs) {
h.initializeConnection(c);
}
return hubs;
}
@Override
public ArrayList<NetworkBulb> getBulbs() {
if (mBulbList != null) {
ArrayList<NetworkBulb> result = new ArrayList<NetworkBulb>(mBulbList.size());
result.addAll(mBulbList);
return result;
} else {
//TODO fix root cause
//this case should never occur, but it does
return new ArrayList<NetworkBulb>();
}
}
@Override
public void setHubConnectionState(Route r, ConnectivityState newState) {
if (r.state != newState) {
r.state = newState;
mDeviceManager.onConnectionChanged();
}
}
public RequestQueue getRequestQueue() {
return volleyRQ;
}
@Override
public void onAttributesReturned(BulbAttributes result, String bulbHueId) {
for (HueBulb bulb : this.mBulbList) {
if (bulb.getHubBulbNumber().equals(bulbHueId)) {
//found the bulb who's attributes were returned
bulb.attributesReturned(result);
}
}
}
@Override
public void onListReturned(BulbAttributes[] result) {
outer:
for (int i = 0; i < result.length; i++) {
BulbAttributes fromHue = result[i];
for (int j = 0; j < mBulbList.size(); j++) {
HueBulb fromMemory = mBulbList.get(j);
// check to see if this bulb is already in our database
if (fromMemory.getHubBulbNumber().equals(fromHue.number)) {
if (!fromMemory.getName().equals(fromHue.name)) {
// A known bulb's name has changed
ContentValues cv = new ContentValues();
cv.put(NetBulbColumns.NAME_COLUMN, fromHue.name);
String[] selectionArgs = {"" + fromHue.number};
mContext.getContentResolver().update(NetBulbColumns.URI, cv,
NetBulbColumns.DEVICE_ID_COLUMN + " = ?",
selectionArgs);
}
if (!fromMemory.getData().matches(fromHue.getHueBulbData())) {
// A known bulb's data attributes have changed
ContentValues cv = new ContentValues();
cv.put(NetBulbColumns.JSON_COLUMN, gson.toJson(fromHue.getHueBulbData()));
String[] selectionArgs = {"" + fromHue.number};
mContext.getContentResolver().update(NetBulbColumns.URI, cv,
NetBulbColumns.DEVICE_ID_COLUMN + " = ?",
selectionArgs);
}
continue outer;
}
}
// if we reach this point, must not already be in memory, so add to database and memory
String bulbName = fromHue.name;
String bulbDeviceId = fromHue.number + "";
ContentValues cv = new ContentValues();
cv.put(NetBulbColumns.NAME_COLUMN, bulbName);
cv.put(NetBulbColumns.DEVICE_ID_COLUMN, bulbDeviceId);
cv.put(NetBulbColumns.CONNECTION_DATABASE_ID, mBaseId);
cv.put(NetBulbColumns.JSON_COLUMN, gson.toJson(fromHue.getHueBulbData()));
cv.put(NetBulbColumns.TYPE_COLUMN, NetBulbColumns.NetBulbType.PHILIPS_HUE);
cv.put(NetBulbColumns.CURRENT_MAX_BRIGHTNESS, 100);
long bulbBaseId =
Long.parseLong(mContext.getContentResolver().insert(NetBulbColumns.URI, cv)
.getLastPathSegment());
mBulbList.add(new HueBulb(mContext, bulbBaseId, bulbName, bulbDeviceId,
fromHue.getHueBulbData(), this));
}
// manually force reload the list of known bulbs
// Note test this by trying to play moods on newly connected connections
this.mDeviceManager.onBulbsListChanged();
}
public ConnectivityState getConnectivityState() {
if (this.getBestRoutes().isEmpty()) {
return ConnectivityState.Unreachable;
} else {
return this.getBestRoutes().get(0).state;
}
}
public void reportStateChangeFailure(PendingStateChange mRequest) {
mRequest.hubBulb.lastSendInitiatedTime = null;
this.mLoopManager.queueSendState(mRequest.hubBulb);
}
public void reportStateChangeSucess(PendingStateChange request) {
HueBulb affected = request.hubBulb;
affected.confirm(request.sentState);
// if more changes should be sent, do so
if (affected.hasPendingTransmission()) {
this.mLoopManager.queueSendState(affected);
}
// notify changes
this.mDeviceManager.onStateChanged();
}
@Override
public String mainDescription() {
// TODO Auto-generated method stub
// return "placeholder";
return this.getConnectivityState().name();
}
@Override
public String subDescription() {
return this.mContext.getResources().getString(R.string.device_hue);
}
public void updateDesiredLastChanged() {
mDesiredLastChanged = SystemClock.elapsedRealtime();
}
@Override
public boolean hasPendingWork() {
if (mDesiredLastChanged != null
&& (mDesiredLastChanged + this.TRANSMIT_TIMEOUT_TIME) > SystemClock.elapsedRealtime()) {
boolean hasPendingWork = false;
for (HueBulb hb : mBulbList) {
if (hb.hasOngoingTransmission()) {
hasPendingWork = true;
}
}
return hasPendingWork;
}
return false;
}
public HubData getHubData() {
return mData;
}
/**
* saves new HubData and reinitializes connections
*/
public void updateConfiguration(HubData newPaths) {
mData = newPaths;
ContentValues cv = new ContentValues();
cv.put(NetConnectionColumns.JSON_COLUMN, gson.toJson(mData));
String selector = NetConnectionColumns._ID + "=?";
String[] selectionArgs = {"" + mBaseId};
mContext.getContentResolver().update(NetConnectionColumns.URI, cv, selector, selectionArgs);
initializeConnection(mContext);
}
@Override
public void delete() {
this.onDestroy();
String selector = NetConnectionColumns._ID + "=?";
String[] selectionArgs = {"" + mBaseId};
mContext.getContentResolver().delete(NetConnectionColumns.URI, selector, selectionArgs);
}
public ChangeLoopManager getLooper() {
return mLoopManager;
}
public class ChangeLoopManager {
// How many state changes can be sent per second.
private final static int TRANSMITS_PER_SECOND = 24;
// How often we should poll the hub for the latest state
private final static long POLL_STATE_INTERVAL_MS = 4000L;
private LinkedHashSet<HueBulb> mOutgoingStateQueue = new LinkedHashSet<HueBulb>();
private LinkedHashSet<HueBulb> mIncomingStateQueue = new LinkedHashSet<HueBulb>();
private boolean requestList = false;
/**
* When the last time all of bulbs were queued to poll for their actual state
* In SystemClock.elapsedRealtime();
*/
private long mLastPollTimeMs = 0L;
private CountDownTimer countDownTimer;
private RateLimiter mRateLimiter;
private HueBulb mPendingOutgoingState;
public ChangeLoopManager() {
mRateLimiter = new RateLimiter(1000L, TRANSMITS_PER_SECOND);
}
protected void onDestroy() {
if (countDownTimer != null) {
countDownTimer.cancel();
countDownTimer = null;
}
}
public void queueSendState(HueBulb changed) {
mOutgoingStateQueue.add(changed);
ensureLooping();
}
public void queueGetState(HueBulb querry) {
mIncomingStateQueue.add(querry);
ensureLooping();
}
public void queueGetList() {
requestList = true;
ensureLooping();
}
private void ensureLooping() {
if (countDownTimer == null) {
countDownTimer = new CountDownTimer(Integer.MAX_VALUE, 1000L / TRANSMITS_PER_SECOND) {
@Override
public void onFinish() {
}
@Override
public void onTick(long millisUntilFinished) {
if (requestList) {
for (Route route : getBestRoutes()) {
NetworkMethods
.getBulbList(route, mData.hashedUsername, mContext, getRequestQueue(),
HubConnection.this, HubConnection.this);
DeferredLog.d("net.hue.connection.onTi", "get bulb list");
}
requestList = false;
} else if (mPendingOutgoingState != null || mOutgoingStateQueue.size() > 0) {
if(mPendingOutgoingState == null) {
mPendingOutgoingState = mOutgoingStateQueue.iterator().next();
mOutgoingStateQueue.remove(mPendingOutgoingState);
}
BulbState toSend = mPendingOutgoingState.getSendState();
if (toSend != null && !toSend.isEmpty()
&& mPendingOutgoingState.lastSendInitiatedTime == null) {
long sentTime = SystemClock.elapsedRealtime();
int capacity = HueUtils.countZibBeeCommandsRequired(toSend);
if(!mRateLimiter.hasCapacity(sentTime, capacity)) {
return;
}
mRateLimiter.consumeCapacity(sentTime, capacity);
PendingStateChange
stateChange =
new PendingStateChange(toSend, mPendingOutgoingState);
for (Route route : getBestRoutes()) {
NetworkMethods.transmitPendingState(route, mData.hashedUsername, mContext,
getRequestQueue(), HubConnection.this,
stateChange);
DeferredLog.d("net.hue.connection.onTi", "transmit pending state %d, %s",
stateChange.hubBulb.getBaseId(), stateChange.sentState);
}
mPendingOutgoingState.lastSendInitiatedTime = sentTime;
mPendingOutgoingState = null;
}
} else if (mIncomingStateQueue.size() > 0) {
HueBulb toQuerry = mIncomingStateQueue.iterator().next();
mIncomingStateQueue.remove(toQuerry);
for (Route route : getBestRoutes()) {
NetworkMethods
.getBulbAttributes(route, mData.hashedUsername, mContext,
getRequestQueue(),
HubConnection.this, HubConnection.this,
toQuerry.getHubBulbNumber());
DeferredLog.d("net.hue.connection.onTi",
"get bulb attributes %s", toQuerry.getBaseId());
}
} else if (SystemClock.elapsedRealtime() > mLastPollTimeMs + POLL_STATE_INTERVAL_MS
&& mIncomingStateQueue.size() < mBulbList.size()) {
// If we have nothing else to do, and we haven't polled all the bulbs recently
// and the poll queue is not backlogged, add all bulbs to the poll queue
mIncomingStateQueue.addAll(mBulbList);
mLastPollTimeMs = SystemClock.elapsedRealtime();
}
}
};
}
countDownTimer.start();
}
}
}