Last active
December 27, 2018 22:05
-
-
Save Stuckeymax/78fb6ff2722e0bc06ca3ad7803c2b19e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Somfy motorized drapery controller | |
#include <ESP8266WiFi.h> | |
#include <ESP8266WebServer.h> | |
#include <WiFiUdp.h> | |
#include <functional> | |
#define LEDBLU 2 // Controls blue LED on WiFi module | |
#define LEDRED 16 // Controls red LED on NodeMCU board | |
#define LEDGRN 4 // Added to include 3rd function (MY) control and added green LED at D2/GPIO4 of NodeMCU v1.0 ESP8266 board | |
// Schematic is pin D2 to 470 ohm resistor -/\/\/\- to green LED cathode -|<- anode to 3V3 pin; green LED is on when D2 false | |
// Connect ESP8266 to Somfy transmitter ground; ESP8266 D0 to /\ button, D2 to MY button, D4 to \/ button; may need to reverse /\ \/ depending on personal preference | |
//Wired as shown above, /\ will close, \/ will open. You'll want that the other way for roller shades. | |
void prepareIds(); | |
boolean connectWifi(); | |
boolean connectUDP(); | |
void startHttpServer(); | |
void turnOnRelay(); | |
void turnOffRelay(); | |
void sendRelayState(); | |
const char* ssid = "yourSSID"; | |
const char* password = "yourWiFipassword"; | |
String friendlyName = "Drapes"; // Alexa device name | |
unsigned int localPort = 1900; // local port to listen on | |
WiFiUDP UDP; | |
boolean udpConnected = false; | |
IPAddress ipMulti(239, 255, 255, 250); | |
unsigned int portMulti = 1900; // local port to listen on | |
ESP8266WebServer HTTP(80); | |
boolean wifiConnected = false; | |
boolean relayState = false; | |
boolean myState = false; | |
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; //buffer to hold incoming packet, | |
String serial; | |
String persistent_uuid; | |
//const int relayPin = 2; //Stuxadd commented out, replaced by #define above, D1 changed to just 2 (blue LED; 0 is red | |
boolean cannotConnectToWifi = false; | |
void setup() { | |
Serial.begin(115200); | |
// Setup Relay | |
pinMode(LEDBLU, OUTPUT); | |
pinMode(LEDRED, OUTPUT); | |
pinMode(LEDGRN, OUTPUT); | |
digitalWrite(LEDBLU, LOW); | |
digitalWrite(LEDRED, LOW); | |
digitalWrite(LEDGRN, LOW); | |
delay(1000); | |
digitalWrite(LEDBLU, HIGH); | |
digitalWrite(LEDRED, HIGH); | |
digitalWrite(LEDGRN, HIGH); | |
delay(1000); | |
digitalWrite(LEDBLU, LOW); | |
digitalWrite(LEDRED, LOW); | |
digitalWrite(LEDGRN, LOW); | |
delay(1000); | |
digitalWrite(LEDBLU,HIGH); | |
digitalWrite(LEDRED, HIGH); | |
digitalWrite(LEDGRN, HIGH); | |
prepareIds(); | |
// Initialise wifi connection | |
wifiConnected = connectWifi(); | |
// only proceed if wifi connection successful | |
if(wifiConnected){ | |
Serial.println("Ask Alexa to discover devices"); | |
udpConnected = connectUDP(); | |
if (udpConnected){ | |
// initialise pins if needed | |
startHttpServer(); | |
} | |
} | |
} | |
void loop() { | |
HTTP.handleClient(); | |
delay(1); | |
// if there's data available, read a packet | |
// check if the WiFi and UDP connections were successful | |
if(wifiConnected){ | |
if(udpConnected){ | |
// if there’s data available, read a packet | |
int packetSize = UDP.parsePacket(); | |
if(packetSize) { | |
//Serial.println(""); | |
//Serial.print("Received packet of size "); | |
//Serial.println(packetSize); | |
//Serial.print("From "); | |
IPAddress remote = UDP.remoteIP(); | |
for (int i =0; i < 4; i++) { | |
Serial.print(remote[i], DEC); | |
if (i < 3) { | |
Serial.print("."); | |
} | |
} | |
Serial.print(", port "); | |
Serial.println(UDP.remotePort()); | |
int len = UDP.read(packetBuffer, 255); | |
if (len > 0) { | |
packetBuffer[len] = 0; | |
} | |
String request = packetBuffer; | |
//Serial.println("Request:"); | |
//Serial.println(request); | |
if(request.indexOf('M-SEARCH') > 0) { | |
if(request.indexOf("urn:Belkin:device:**") > 0) { | |
Serial.println("Responding to search request ..."); | |
respondToSearch(); | |
} | |
} | |
} | |
delay(10); | |
} | |
} else { | |
// Turn on/off to indicate cannot connect .. | |
} | |
} | |
void prepareIds() { | |
uint32_t chipId = ESP.getChipId(); | |
char uuid[64]; | |
sprintf_P(uuid, PSTR("38323636-4558-4dda-9188-cda0e6%02x%02x%02x"), | |
(uint16_t) ((chipId >> 16) & 0xff), | |
(uint16_t) ((chipId >> 8) & 0xff), | |
(uint16_t) chipId & 0xff); | |
serial = String(uuid); | |
persistent_uuid = "Socket-1_0-" + serial; | |
} | |
void respondToSearch() { | |
Serial.println(""); | |
Serial.print("Sending response to "); | |
Serial.println(UDP.remoteIP()); | |
Serial.print("Port : "); | |
Serial.println(UDP.remotePort()); | |
IPAddress localIP = WiFi.localIP(); | |
char s[16]; | |
sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); | |
String response = | |
"HTTP/1.1 200 OK\r\n" | |
"CACHE-CONTROL: max-age=86400\r\n" | |
"DATE: Fri, 15 Apr 2016 04:56:29 GMT\r\n" | |
"EXT:\r\n" | |
"LOCATION: http://" + String(s) + ":80/setup.xml\r\n" | |
"OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\r\n" | |
"01-NLS: b9200ebb-736d-4b93-bf03-835149d13983\r\n" | |
"SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" | |
"ST: urn:Belkin:device:**\r\n" | |
"USN: uuid:" + persistent_uuid + "::urn:Belkin:device:**\r\n" | |
"X-User-Agent: redsonic\r\n\r\n"; | |
UDP.beginPacket(UDP.remoteIP(), UDP.remotePort()); | |
UDP.write(response.c_str()); | |
UDP.endPacket(); | |
Serial.println("Response sent !"); | |
} | |
void startHttpServer() { | |
HTTP.on("/index.html", HTTP_GET, [](){ | |
Serial.println("Got Request index.html ...\n"); | |
HTTP.send(200, "text/plain", "Hello World!"); | |
}); | |
HTTP.on("/upnp/control/basicevent1", HTTP_POST, []() { | |
Serial.println("########## Responding to /upnp/control/basicevent1 ... ##########"); | |
//for (int x=0; x <= HTTP.args(); x++) { | |
// Serial.println(HTTP.arg(x)); | |
//} | |
String request = HTTP.arg(0); | |
Serial.print("request:"); | |
Serial.println(request); | |
if(request.indexOf("SetBinaryState") >= 0) { | |
if(request.indexOf("<BinaryState>1</BinaryState>") >= 0) { | |
Serial.println("Got Turn on request"); | |
turnOnRelay(); | |
} | |
if(request.indexOf("<BinaryState>0</BinaryState>") >= 0) { | |
Serial.println("Got Turn off request"); | |
turnOffRelay(); | |
} | |
} | |
if(request.indexOf("GetBinaryState") >= 0) { | |
Serial.println("Got binary state request"); | |
sendRelayState(); | |
} | |
HTTP.send(200, "text/plain", ""); | |
}); | |
HTTP.on("/eventservice.xml", HTTP_GET, [](){ | |
Serial.println(" ########## Responding to eventservice.xml ... ########\n"); | |
String eventservice_xml = "<scpd xmlns=\"urn:Belkin:service-1-0\">" | |
"<actionList>" | |
"<action>" | |
"<name>SetBinaryState</name>" | |
"<argumentList>" | |
"<argument>" | |
"<retval/>" | |
"<name>BinaryState</name>" | |
"<relatedStateVariable>BinaryState</relatedStateVariable>" | |
"<direction>in</direction>" | |
"</argument>" | |
"</argumentList>" | |
"</action>" | |
"<action>" | |
"<name>GetBinaryState</name>" | |
"<argumentList>" | |
"<argument>" | |
"<retval/>" | |
"<name>BinaryState</name>" | |
"<relatedStateVariable>BinaryState</relatedStateVariable>" | |
"<direction>out</direction>" | |
"</argument>" | |
"</argumentList>" | |
"</action>" | |
"</actionList>" | |
"<serviceStateTable>" | |
"<stateVariable sendEvents=\"yes\">" | |
"<name>BinaryState</name>" | |
"<dataType>Boolean</dataType>" | |
"<defaultValue>0</defaultValue>" | |
"</stateVariable>" | |
"<stateVariable sendEvents=\"yes\">" | |
"<name>level</name>" | |
"<dataType>string</dataType>" | |
"<defaultValue>0</defaultValue>" | |
"</stateVariable>" | |
"</serviceStateTable>" | |
"</scpd>\r\n" | |
"\r\n"; | |
HTTP.send(200, "text/plain", eventservice_xml.c_str()); | |
}); | |
HTTP.on("/setup.xml", HTTP_GET, [](){ | |
Serial.println(" ########## Responding to setup.xml ... ########\n"); | |
IPAddress localIP = WiFi.localIP(); | |
char s[16]; | |
sprintf(s, "%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); | |
String setup_xml = "<?xml version=\"1.0\"?>" | |
"<root>" | |
"<device>" | |
"<deviceType>urn:Belkin:device:controllee:1</deviceType>" | |
"<friendlyName>"+ friendlyName +"</friendlyName>" | |
"<manufacturer>Belkin International Inc.</manufacturer>" | |
"<modelName>Socket</modelName>" | |
"<modelNumber>3.1415</modelNumber>" | |
"<modelDescription>Belkin Plugin Socket 1.0</modelDescription>\r\n" | |
"<UDN>uuid:"+ persistent_uuid +"</UDN>" | |
"<serialNumber>221517K0101769</serialNumber>" | |
"<binaryState>0</binaryState>" | |
"<serviceList>" | |
"<service>" | |
"<serviceType>urn:Belkin:service:basicevent:1</serviceType>" | |
"<serviceId>urn:Belkin:serviceId:basicevent1</serviceId>" | |
"<controlURL>/upnp/control/basicevent1</controlURL>" | |
"<eventSubURL>/upnp/event/basicevent1</eventSubURL>" | |
"<SCPDURL>/eventservice.xml</SCPDURL>" | |
"</service>" | |
"</serviceList>" | |
"</device>" | |
"</root>\r\n" | |
"\r\n"; | |
HTTP.send(200, "text/xml", setup_xml.c_str()); | |
Serial.print("Sending :"); | |
Serial.println(setup_xml); | |
}); | |
HTTP.begin(); | |
Serial.println("HTTP Server started .."); | |
} | |
// connect to wifi – returns true if successful or false if not | |
boolean connectWifi(){ | |
boolean state = true; | |
int i = 0; | |
WiFi.mode(WIFI_STA); | |
WiFi.begin(ssid, password); | |
Serial.println(""); | |
Serial.println("Connecting to WiFi"); | |
// Wait for connection | |
Serial.print("Connecting ..."); | |
while (WiFi.status() != WL_CONNECTED) { | |
delay(500); | |
Serial.print("."); | |
if (i > 10){ | |
state = false; | |
break; | |
} | |
i++; | |
} | |
if (state){ | |
Serial.println(""); | |
Serial.print("Connected to "); | |
Serial.println(ssid); | |
Serial.print("IP address: "); | |
Serial.println(WiFi.localIP()); | |
} | |
else { | |
Serial.println(""); | |
Serial.println("Connection failed."); | |
} | |
return state; | |
} | |
boolean connectUDP(){ | |
boolean state = false; | |
Serial.println(""); | |
Serial.println("Connecting to UDP"); | |
if(UDP.beginMulticast(WiFi.localIP(), ipMulti, portMulti)) { | |
Serial.println("Connection successful"); | |
state = true; | |
} | |
else{ | |
Serial.println("Connection failed"); | |
} | |
return state; | |
} | |
//Lines 366 to 425 modified to convert ON/OFF commands to three separate 0.5-sec button pushes | |
void turnOnRelay() { | |
if((!relayState) | (myState)){ | |
digitalWrite(LEDBLU, LOW); // turn on LED on | |
delay(500); | |
digitalWrite(LEDBLU, HIGH); // then turn it right back off, but set relayState true | |
relayState = true; | |
myState = false; | |
} | |
else{ | |
digitalWrite(LEDGRN, LOW); // turn green LED on (my) | |
delay(500); | |
digitalWrite(LEDGRN, HIGH); // then turn it right back off, but set relayState false (open) | |
myState = true; | |
} | |
Serial.println("$$$$$Exiting turnOnRelay after if statement, relayState should be true, but is " + String(relayState) + " $$$$$"); | |
String body = | |
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>\r\n" | |
"<u:SetBinaryStateResponse xmlns:u=\"urn:Belkin:service:basicevent:1\">\r\n" | |
"<BinaryState>1</BinaryState>\r\n" | |
"</u:SetBinaryStateResponse>\r\n" | |
"</s:Body> </s:Envelope>"; | |
HTTP.send(200, "text/xml", body.c_str()); | |
Serial.print("Sending :"); | |
Serial.println(body); | |
} | |
void turnOffRelay() { | |
if(relayState | myState){ | |
digitalWrite(LEDRED, LOW); // turn on relay with voltage HIGH | |
delay(500); | |
digitalWrite(LEDRED, HIGH); // then turn it right back off, but leave relay state unchanged | |
relayState = false; | |
myState = false; | |
} | |
else{ | |
digitalWrite(LEDGRN, LOW); // turn green LED on | |
delay(500); | |
digitalWrite(LEDGRN, HIGH); // then turn it right back off, but keep relayState true | |
myState = true; | |
} | |
String body = | |
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>\r\n" | |
"<u:SetBinaryStateResponse xmlns:u=\"urn:Belkin:service:basicevent:1\">\r\n" | |
"<BinaryState>0</BinaryState>\r\n" | |
"</u:SetBinaryStateResponse>\r\n" | |
"</s:Body> </s:Envelope>"; | |
HTTP.send(200, "text/xml", body.c_str()); | |
Serial.print("Sending :"); | |
Serial.println(body); | |
} | |
void sendRelayState() { | |
String body = | |
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>\r\n" | |
"<u:GetBinaryStateResponse xmlns:u=\"urn:Belkin:service:basicevent:1\">\r\n" | |
"<BinaryState>"; | |
body += (relayState ? "1" : "0"); | |
body += "</BinaryState>\r\n" | |
"</u:GetBinaryStateResponse>\r\n" | |
"</s:Body> </s:Envelope>\r\n"; | |
HTTP.send(200, "text/xml", body.c_str()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Somfy Add-On Motorized Window Treatment Version
This sketch is modified from the irdevkit.ino code published by kakopappa. Kakopappa did the heavy lifting here, not me; kudos to kakopappa. It takes pseudo-Philips hard, ON/OFF control and adapts it to create 1/2-second button pushes on any of three buttons on an RTS Somfy 3-button handheld motorized window controller. Connection instructions are included in the sketch comments. Change line 21 if you want a different device name (default is “drapes;” set in your SSID and password and you are good to go. Soldering required, or a good bed-of-nails test jig for your Somfy remote. Hardware details: I am running a NodeMCU v1.0 board. Requires ESP8266WiFi.h, ESP8266Webserver.h, WiFiUdp.h and , whatever that last is. Just reading the include-file listing here.
A Boolean variable myState is added to accommodate the 3rd button along with some if/else/OR logic in the turnOffRelay() and turnOnRelay() functions. Once initialized by a “turn on drapes” command followed by a “turn off drapes” command, the custom-opening-distance “MY” button is “voice-pressed” by ordering draperies OFF when they’re already off (closed; think light) or ordering draperies ON when they’re already on (open).
You have to turn the draperies ON or OFF, because OPEN or CLOSED is not supported for this faux-Philips device, which is a fake Philips lighting controller listening on port 1900, I think. Alexa will tell you as much. This code includes device-state feedback to Alexa so that when you open the iPhone app, the correct state is always displayed if open or closed (on or off, respectively). No state recognition for being in the MY partially open state, of course.
Here’s how it achieves 3 button pushes from a 2-state Alexa device:
Drapes go to the full-open position
Drapes go to the closed position
Drapes go to the pre-programmed MY position.
Drapes go to the pre-programmed MY position, as in 3.
In other words, tell it to go where it isn’t, it goes there. Tell it to go where it already is, it goes to the intermediate position. Yes, this is a little clunky, but a $40 Somfy remote is a lot cheaper than a full-blown Somfy MyLink Ethernet bridge unit ($250 on a good day). Plus hacked beats ready-made any day:)
Using this to control Somfy motorized window treatments requires a Somfy 3-button handheld controller dedicated to the project. Planned improvements include:
Philips Note: If you have an Alexa-recognized Philips lighting Internet bridge unit on the same network as this device, you'll need to power off the Philips Hub/Bridge unit while you are performing device discovery. Otherwise, Alexa will never recognize your ESP8266 device. Power up your ESP8266, and once it’s recognized, you can power up the Philips Hub, and both it and your new, added ESP8266 device will be recognized and working fine forever.
Motor Note: Somfy motorized products have a self-preservation feature to ensure that motors live a long, uncooked life. When you first get this new toy working and you kick back, ordering Alexa to turn on the drapes, turn off the drapes, turn off the drapes, turn on the drapes… eventually your drapes or roller shades or what have you will stop responding. No worry. Give the motor a few minutes to cool down, and everything will start working again like it never took a breather. This open/closed/open/closed business never happens in real life, so don’t fix it; it’s not broken.
There are probably other uses for this two-to-three conversion. Please report any adaptations you find useful.
Enjoy!
Stuckeymax