Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration with Google Home and support for Dummable Light #85

Merged
merged 1 commit into from
Oct 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 161 additions & 60 deletions ESP8266HueEmulator/LightService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include <aJSON.h> // Replace avm/pgmspace.h with pgmspace.h there and set #define PRINT_BUFFER_LEN 4096 ################# IMPORTANT
#include <assert.h>

#if PRINT_BUFFER_LEN < 2048
#if PRINT_BUFFER_LEN < 4096
# error aJson print buffer length PRINT_BUFFER_LEN must be increased to at least 4096
#endif

Expand Down Expand Up @@ -190,14 +190,17 @@ class WcFnRequestHandler : public RequestHandler {

LightServiceClass LightService;

LightHandler *lightHandlers[MAX_LIGHT_HANDLERS]; // interfaces exposed to the outside world
LightHandler *lightHandlers[MAX_LIGHT_HANDLERS] = {}; // interfaces exposed to the outside world

LightServiceClass::LightServiceClass() { }

bool LightServiceClass::setLightHandler(int index, LightHandler *handler) {
if (index >= currentNumLights || index < 0) return false;
lightHandlers[index] = handler;
return true;
}


bool LightServiceClass::setLightsAvailable(int lights) {
if (lights <= MAX_LIGHT_HANDLERS) {
currentNumLights = lights;
Expand Down Expand Up @@ -238,10 +241,10 @@ static const char* _ssdp_response_template =
"EXT:\r\n"
"CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL
"LOCATION: http://%s:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
"SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, %s/%s\r\n" // _modelName, _modelNumber
"hue-bridgeid: %s\r\n"
"ST: %s\r\n" // _deviceType
"USN: uuid:%s\r\n" // _uuid
"USN: uuid:%s::upnp:rootdevice\r\n" // _uuid::_deviceType
"\r\n";

static const char* _ssdp_notify_template =
Expand All @@ -250,11 +253,31 @@ static const char* _ssdp_notify_template =
"NTS: ssdp:alive\r\n"
"CACHE-CONTROL: max-age=%u\r\n" // SSDP_INTERVAL
"LOCATION: http://%s:%u/%s\r\n" // WiFi.localIP(), _port, _schemaURL
"SERVER: Arduino/1.0 UPNP/1.1 %s/%s\r\n" // _modelName, _modelNumber
"SERVER: FreeRTOS/6.0.5, UPnP/1.0, %s/%s\r\n" // _modelName, _modelNumber
"hue-bridgeid: %s\r\n"
"NT: %s\r\n" // _deviceType
"USN: uuid:%s\r\n" // _uuid
"USN: uuid:%s::upnp:rootdevice\r\n" // _uuid
"\r\n";

static const char* _ssdp_xml_template = "<?xml version=\"1.0\" ?>"
"<root xmlns=\"urn:schemas-upnp-org:device-1-0\">"
"<specVersion><major>1</major><minor>0</minor></specVersion>"
"<URLBase>http://{ip}:80/</URLBase>"
"<device>"
"<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>"
"<friendlyName>Philips hue ( {ip} )</friendlyName>"
"<manufacturer>Royal Philips Electronics</manufacturer>"
"<manufacturerURL>http://www.philips.com</manufacturerURL>"
"<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>"
"<modelName>Philips hue bridge 2012</modelName>"
"<modelNumber>929000226503</modelNumber>"
"<modelURL>http://www.meethue.com</modelURL>"
"<serialNumber>{mac}</serialNumber>"
"<UDN>uuid:2f402f80-da50-11e1-9b23-{mac}</UDN>"
"<presentationURL>index.html</presentationURL>"
//"<iconList><icon><mimetype>image/png</mimetype><height>48</height><width>48</width><depth>24</depth><url>hue_logo_0.png</url></icon><icon><mimetype>image/png</mimetype><height>120</height><width>120</width><depth>24</depth><url>hue_logo_3.png</url></icon></iconList>"
"</device>"
"</root>";

int ssdpMsgFormatCallback(SSDPClass *ssdp, char *buffer, int buff_len,
bool isNotify, int interval, char *modelName,
Expand Down Expand Up @@ -352,9 +375,17 @@ void on(HandlerFunction fn, const String &wcUri, HTTPMethod method, char wildcar
}

void descriptionFn() {
String str = "<root><specVersion><major>1</major><minor>0</minor></specVersion><URLBase>http://" + ipString + ":80/</URLBase><device><deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType><friendlyName>Philips hue (" + ipString + ")</friendlyName><manufacturer>Royal Philips Electronics</manufacturer><manufacturerURL>http://www.philips.com</manufacturerURL><modelDescription>Philips hue Personal Wireless Lighting</modelDescription><modelName>Philips hue bridge 2012</modelName><modelNumber>929000226503</modelNumber><modelURL>http://www.meethue.com</modelURL><serialNumber>"+macString+"</serialNumber><UDN>uuid:2f402f80-da50-11e1-9b23-"+macString+"</UDN><presentationURL>index.html</presentationURL><iconList><icon><mimetype>image/png</mimetype><height>48</height><width>48</width><depth>24</depth><url>hue_logo_0.png</url></icon><icon><mimetype>image/png</mimetype><height>120</height><width>120</width><depth>24</depth><url>hue_logo_3.png</url></icon></iconList></device></root>";
HTTP->send(200, "text/plain", str);
Serial.println(str);
String response = _ssdp_xml_template;

String escapedMac = macString;
escapedMac.replace(":", "");
escapedMac.toLowerCase();

response.replace("{mac}", escapedMac);
response.replace("{ip}", ipString);

HTTP->send(200, "text/xml", response);
Serial.println(response);
}

void unimpFn(WcFnRequestHandler *handler, String requestUri, HTTPMethod method) {
Expand Down Expand Up @@ -571,7 +602,7 @@ void groupsIdFn(WcFnRequestHandler *handler, String requestUri, HTTPMethod metho
aJson.addStringToObject(object, "name", "0");
aJsonObject *lightsArray = aJson.createArray();
aJson.addItemToObject(object, "lights", lightsArray);
for (int i = 0; i < MAX_LIGHT_HANDLERS; i++) {
for (int i = 0; i < LightService.getLightsAvailable(); i++) {
if (!lightHandlers[i]) {
continue;
}
Expand Down Expand Up @@ -666,10 +697,12 @@ void lightsIdFn(WcFnRequestHandler *whandler, String requestUri, HTTPMethod meth
}

void lightsIdStateFn(WcFnRequestHandler *whandler, String requestUri, HTTPMethod method) {
int numberOfTheLight = atoi(whandler->getWildCard(1).c_str()) - 1;
int numberOfTheLight = max(0, atoi(whandler->getWildCard(1).c_str()) - 1);
LightHandler *handler = LightService.getLightHandler(numberOfTheLight);
if (!handler) {
sendError(3, requestUri, "Requested light not available");
char buff[100] = {};
sprintf(buff, "Requested light not available for number %d", numberOfTheLight);
sendError(3, requestUri, buff);
return;
}

Expand Down Expand Up @@ -749,36 +782,40 @@ void LightServiceClass::begin(ESP8266WebServer *svr) {

HTTP->begin();

String serial = macString;
serial.toLowerCase();

Serial.println("Starting SSDP...");
SSDP.begin();
SSDP.setSchemaURL((char*)"description.xml");
SSDP.setSchemaURL("description.xml");
SSDP.setHTTPPort(80);
SSDP.setName((char*)"Philips hue clone");
SSDP.setSerialNumber(macString.c_str());
SSDP.setURL((char*)"index.html");
SSDP.setModelName((char*)"IpBridge");
SSDP.setModelNumber((char*)"0.1");
SSDP.setModelURL((char*)"http://www.meethue.com");
SSDP.setManufacturer((char*)"Royal Philips Electronics");
SSDP.setManufacturerURL((char*)"http://www.philips.com");
SSDP.setDeviceType((char*)"upnp:rootdevice");
SSDP.setMessageFormatCallback(ssdpMsgFormatCallback);
SSDP.setName("Philips hue clone");
SSDP.setSerialNumber(serial.c_str());
SSDP.setURL("index.html");
SSDP.setModelName("IpBridge");
SSDP.setModelNumber("0.1");
SSDP.setModelURL("http://www.meethue.com");
SSDP.setManufacturer("Royal Philips Electronics");
SSDP.setManufacturerURL("http://www.philips.com");

//SSDP.setDeviceType((char*)"upnp:rootdevice");
SSDP.setDeviceType("urn:schemas-upnp-org:device:basic:1");
//SSDP.setMessageFormatCallback(ssdpMsgFormatCallback);
SSDP.begin();
Serial.println("SSDP Started");
}

void LightServiceClass::update() {
HTTP->handleClient();
}

void sendJson(aJsonObject *root)
{
void sendJson(aJsonObject *root) {
// Take aJsonObject and print it to Serial and to WiFi
// From https://github.com/pubnub/msp430f5529/blob/master/msp430f5529.ino
char *msgStr = aJson.print(root);
aJson.deleteItem(root);
Serial.println(millis());
Serial.println(msgStr);
HTTP->send(200, "text/plain", msgStr);
HTTP->send(200, "application/json", msgStr);
free(msgStr);
}

Expand All @@ -795,23 +832,69 @@ void sendJson(aJsonObject *root)
// The code is based on this brilliant note: https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/commit/f41091cf671e13fe8c32fcced12604cd31cceaf3

rgbcolor getXYtoRGB(float x, float y, int brightness_raw) {
float brightness = ((float)brightness_raw) / 255.0f;
float bright_y = brightness / y;
float X = x * bright_y;
float Z = (1 - x - y) * bright_y;

// convert to RGB (0.0-1.0) color space
float R = X * 1.4628067 - brightness * 0.1840623 - Z * 0.2743606;
float G = -X * 0.5217933 + brightness * 1.4472381 + Z * 0.0677227;
float B = X * 0.0349342 - brightness * 0.0968930 + Z * 1.2884099;

// apply inverse 2.2 gamma
float inv_gamma = 1.0 / 2.4;
float linear_delta = 0.055;
float linear_interval = 1 + linear_delta;
float r = R <= 0.0031308 ? 12.92 * R : (linear_interval) * pow(R, inv_gamma) - linear_delta;
float g = G <= 0.0031308 ? 12.92 * G : (linear_interval) * pow(G, inv_gamma) - linear_delta;
float b = B <= 0.0031308 ? 12.92 * B : (linear_interval) * pow(B, inv_gamma) - linear_delta;
float Y = brightness_raw / 250.0f;

float z = 1.0f - x - y;
float X = (Y / y) * x;
float Z = (Y / y) * z;

// sRGB D65 conversion
float r = X * 1.656492f - Y * 0.354851f - Z * 0.255038f;
float g = -X * 0.707196f + Y * 1.655397f + Z * 0.036152f;
float b = X * 0.051713f - Y * 0.121364f + Z * 1.011530f;

if (r > b && r > g && r > 1.0f) {
// red is too big
g = g / r;
b = b / r;
r = 1.0f;
}
else if (g > b && g > r && g > 1.0f) {
// green is too big
r = r / g;
b = b / g;
g = 1.0f;
}
else if (b > r && b > g && b > 1.0f) {
// blue is too big
r = r / b;
g = g / b;
b = 1.0f;
}

// Apply gamma correction
r = r <= 0.0031308f ? 12.92f * r : (1.0f + 0.055f) * pow(r, (1.0f / 2.4f)) - 0.055f;
g = g <= 0.0031308f ? 12.92f * g : (1.0f + 0.055f) * pow(g, (1.0f / 2.4f)) - 0.055f;
b = b <= 0.0031308f ? 12.92f * b : (1.0f + 0.055f) * pow(b, (1.0f / 2.4f)) - 0.055f;

if (r > b && r > g) {
// red is biggest
if (r > 1.0f) {
g = g / r;
b = b / r;
r = 1.0f;
}
}
else if (g > b && g > r) {
// green is biggest
if (g > 1.0f) {
r = r / g;
b = b / g;
g = 1.0f;
}
}
else if (b > r && b > g) {
// blue is biggest
if (b > 1.0f) {
r = r / b;
g = g / b;
b = 1.0f;
}
}

r = r < 0 ? 0 : r;
g = g < 0 ? 0 : g;
b = b < 0 ? 0 : b;

return rgbcolor(r * COLOR_SATURATION,
g * COLOR_SATURATION,
Expand Down Expand Up @@ -903,7 +986,7 @@ bool parseHueLightInfo(HueLightInfo currentInfo, aJsonObject *parsedRoot, HueLig
newInfo->effect = EFFECT_NONE;
}
}

// pull alert
aJsonObject* alertState = aJson.getObjectItem(parsedRoot, "alert");
if (alertState) {
Expand Down Expand Up @@ -955,27 +1038,46 @@ bool parseHueLightInfo(HueLightInfo currentInfo, aJsonObject *parsedRoot, HueLig
}

void addLightJson(aJsonObject* root, int numberOfTheLight, LightHandler *lightHandler) {
if (!lightHandler) return;
String lightName = "" + (String) (numberOfTheLight + 1);
if (!lightHandler)
return;


String lightNumber = (String) (numberOfTheLight + 1);
String lightName = lightHandler->getFriendlyName(numberOfTheLight);
aJsonObject *light;
aJson.addItemToObject(root, lightName.c_str(), light = aJson.createObject());
aJson.addStringToObject(light, "type", "Extended color light"); // type of lamp (all "Extended colour light" for now)
aJson.addStringToObject(light, "name", ("Hue LightStrips " + (String) (numberOfTheLight + 1)).c_str()); // // the name as set through the web UI or app
aJson.addStringToObject(light, "uniqueid", ("AA:BB:CC:DD:EE:FF:00:11-" + (String) (numberOfTheLight + 1)).c_str());
aJson.addItemToObject(root, lightNumber.c_str(), light = aJson.createObject());

HueLightInfo info = lightHandler->getInfo(numberOfTheLight);
if (info.bulbType == HueBulbType::DIMMABLE_LIGHT) {
aJson.addStringToObject(light, "type", "Dimmable light");
} else {
aJson.addStringToObject(light, "type", "Extended color light");
}

aJson.addStringToObject(light, "manufacturername", "OpenSource"); // type of lamp (all "Extended colour light" for now)
aJson.addStringToObject(light, "swversion", "0.1");
aJson.addStringToObject(light, "name", lightName.c_str()); // // the name as set through the web UI or app
aJson.addStringToObject(light, "uniqueid", (macString + "-" + (String) (numberOfTheLight + 1)).c_str());
aJson.addStringToObject(light, "modelid", "LST001"); // the model number

aJsonObject *state;
aJson.addItemToObject(light, "state", state = aJson.createObject());
HueLightInfo info = lightHandler->getInfo(numberOfTheLight);
aJson.addBooleanToObject(state, "on", info.on);
aJson.addNumberToObject(state, "hue", info.hue); // hs mode: the hue (expressed in ~deg*182.04)

aJson.addNumberToObject(state, "bri", info.brightness); // brightness between 0-254 (NB 0 is not off!)
aJson.addNumberToObject(state, "sat", info.saturation); // hs mode: saturation between 0-254
double numbers[2] = {0.0, 0.0};
aJson.addItemToObject(state, "xy", aJson.createFloatArray(numbers, 2)); // xy mode: CIE 1931 color co-ordinates
aJson.addNumberToObject(state, "ct", 500); // ct mode: color temp (expressed in mireds range 154-500)

if (info.bulbType == HueBulbType::EXTENDED_COLOR_LIGHT) {
double numbers[2] = {0.0, 0.0};
aJson.addItemToObject(state, "xy", aJson.createFloatArray(numbers, 2)); // xy mode: CIE 1931 color co-ordinates
aJson.addStringToObject(state, "colormode", "hs"); // the current color mode
aJson.addStringToObject(state, "effect", info.effect == EFFECT_COLORLOOP ? "colorloop" : "none");
aJson.addNumberToObject(state, "ct", 500); // ct mode: color temp (expressed in mireds range 154-500)

aJson.addNumberToObject(state, "hue", info.hue); // hs mode: the hue (expressed in ~deg*182.04)
aJson.addNumberToObject(state, "sat", info.saturation); // hs mode: saturation between 0-254
}

aJson.addStringToObject(state, "alert", "none"); // 'select' flash the lamp once, 'lselect' repeat flash for 30s
aJson.addStringToObject(state, "effect", info.effect == EFFECT_COLORLOOP ? "colorloop" : "none");
aJson.addStringToObject(state, "colormode", "hs"); // the current color mode
aJson.addBooleanToObject(state, "reachable", true); // lamp can be seen by the hub
}

Expand Down Expand Up @@ -1237,4 +1339,3 @@ String methodToString(int method) {
default: return "unknown";
}
}

17 changes: 16 additions & 1 deletion ESP8266HueEmulator/LightService.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#include <Arduino.h>

enum HueColorType {
TYPE_HUE_SAT, TYPE_CT, TYPE_XY
};
Expand All @@ -10,17 +12,24 @@ enum HueEffect {
EFFECT_NONE, EFFECT_COLORLOOP
};

enum class HueBulbType {
EXTENDED_COLOR_LIGHT, DIMMABLE_LIGHT
};


struct HueLightInfo {
bool on = false;
int brightness = 0;
HueColorType type = TYPE_HUE_SAT;
HueBulbType bulbType = HueBulbType::EXTENDED_COLOR_LIGHT;
int hue = 0, saturation = 0;
HueAlert alert = ALERT_NONE;
HueEffect effect = EFFECT_NONE;
unsigned int transitionTime = 800; // by default there is a transition time to the new state of 400 milliseconds
};

class aJsonObject;
class String;
bool parseHueLightInfo(HueLightInfo currentInfo, aJsonObject *parsedRoot, HueLightInfo *newInfo);

class LightHandler {
Expand All @@ -31,15 +40,21 @@ class LightHandler {
HueLightInfo info;
return info;
}
virtual String getFriendlyName(int lightNumber) const {
return "Hue LightStrips " + ((String) (lightNumber + 1));
}
};

// Max number of exposed lights is directly related to aJSON PRINT_BUFFER_LEN, 14 for 4096
#define MAX_LIGHT_HANDLERS 6
#define MAX_LIGHT_HANDLERS 2
#define COLOR_SATURATION 255.0f

class ESP8266WebServer;
class LightServiceClass {
const char* _friendlyName;

public:
LightServiceClass();
LightHandler *getLightHandler(int numberOfTheLight);
bool setLightsAvailable(int numLights);
int getLightsAvailable();
Expand Down
Loading