-
Notifications
You must be signed in to change notification settings - Fork 6
/
YeelightDS.cpp
195 lines (162 loc) · 5.45 KB
/
YeelightDS.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/* Yeelight Smart Switch App for ESP8266
* Yeelight communication library definition
* (c) DNS 2018-2021
*/
#include "YeelightDS.h" // Yeelight support
#include <ESP8266WiFi.h> // Wi-Fi support
using namespace ds;
// Yeelight protocol; see https://www.yeelight.com/en_US/developer
// Some black magic to avoid defining multicast address twice
#define _comma ,
#define _ssdp_multicast_addr(dot) 239 dot 255 dot 255 dot 250
#define _ssdp_multicast_addr_comma _ssdp_multicast_addr(_comma)
#define _ssdp_multicast_addr_str __XSTRING(_ssdp_multicast_addr(.))
#define _ssdp_port 1982
#define _ssdp_port_str __STRING(_ssdp_port)
static const char *YL_MSG_DISCOVER PROGMEM =
"M-SEARCH * HTTP/1.1\r\n"
"HOST: " _ssdp_multicast_addr_str ":" _ssdp_port_str "\r\n"
"MAN: \"ssdp:discover\"\r\n"
"ST: wifi_bulb";
static const char *YL_MSG_TOGGLE PROGMEM =
"{\"id\":1,"
"\"method\":\"toggle\","
"\"params\":[]"
"}\r\n";
/////////////////////// YBulb ///////////////////////
WiFiClient YBulb::client; // Wi-Fi client (shared across all bulbs)
const char *YBulb::ID_UNKNOWN PROGMEM = "0x000000000UNKNOWN"; // Unknown ID literal
// Constructor (bulb ID, bulb IP, bulb port)
YBulb::YBulb(const String& yid, const IPAddress& yip, const uint16_t yport) :
id(yid), ip(yip), port(yport), power(false), active(false) {
// Reduce connection timeout for inactive bulbs
if (client.getTimeout() != TIMEOUT)
client.setTimeout(TIMEOUT);
}
// Return shortened bulb ID
//// Experience shows that Yeelight IDs are long zero-padded strings, so we can save some space
String YBulb::getShortID() const {
return id.substring(11);
}
// Turn the bulb on. Returns true on success
bool YBulb::turnOn() {
return power ? true /* already on */ : flip();
}
// Turn the bulb off. Returns true on success
bool YBulb::turnOff() {
return power ? flip() : true /* already off */;
}
// Toggle bulb power state. Returns true on success
bool YBulb::flip() {
if (client.connect(ip, port)) {
client.print(FPSTR(YL_MSG_TOGGLE));
client.stop();
power = !power;
return true;
} else
return false;
}
// Print bulb info in HTML
//// Name | ID (shortened) | IP Address | Model | Power
void YBulb::printHTML(String& str) const {
str += F("<td>");
str += name;
str += F("</td><td>");
str += getShortID();
str += F("</td><td>");
str += ip.toString();
str += F("</td><td>");
str += model;
str += F("</td><td>");
str += getPowerStr();
str += F("</td>");
}
// Print bulb status in HTML
void YBulb::printStatusHTML(String& str) const {
str += F("<tr>");
printHTML(str);
str += F("</tr>\n");
}
// Print bulb configuration controls in HTML
//// Same as status but prepended with a selector
void YBulb::printConfHTML(String& str, uint8_t num) const {
str += F("<tr><td>");
str += F("<input type=\"checkbox\" name=\"bulb\" value=\"");
str += num;
str += '"';
if (active)
str += F(" checked=\"checked\"");
str += F("/></td>");
printHTML(str);
str += F("</tr>\n");
}
//////////////////// YDiscovery /////////////////////
const IPAddress YDiscovery::SSDP_MULTICAST_ADDR(_ssdp_multicast_addr_comma);
const uint16_t YDiscovery::SSDP_PORT = _ssdp_port;
// Send discovery request
bool YDiscovery::send() {
const String discovery_msg(FPSTR(YL_MSG_DISCOVER)); // Preload the message from flash, as WiFiUDP cannot work with flash directly
t0 = millis();
auto ret = true;
// Send broadcast message
udp.stop();
ret = udp.beginPacketMulticast(SSDP_MULTICAST_ADDR, SSDP_PORT, WiFi.localIP(), 32);
if (ret) {
ret = udp.write(discovery_msg.c_str(), discovery_msg.length());
if (ret) {
ret = udp.endPacket();
if (ret) {
// Switch to listening for the replies on the same port
const auto udp_port = udp.localPort();
udp.stop();
ret = udp.begin(udp_port);
}
}
}
return ret;
}
// Receive discovery reply
YBulb *YDiscovery::receive() {
YBulb *new_bulb = nullptr;
while (isInProgress() && !new_bulb) {
if (!udp.parsePacket())
continue;
const auto len = udp.read(reply_buffer, sizeof(reply_buffer) - 1);
if (len <= 0)
continue;
reply_buffer[len] = '\0'; // Null-terminate
String reply(reply_buffer);
IPAddress host;
uint16_t port = 0;
while (true) {
const auto idx = reply.indexOf(F("\r\n"));
if (idx == -1)
break;
auto line = reply.substring(0, idx);
reply.remove(0, idx + 2);
if (line.startsWith(F("Location: yeelight://"))) {
line.remove(0, line.indexOf('/') + 2);
host.fromString(line.substring(0, line.indexOf(':')));
port = line.substring(line.indexOf(':') + 1).toInt();
} else
if (line.startsWith(F("id: "))) {
const auto id = line.substring(4);
if (!id.isEmpty() && host && port)
new_bulb = new YBulb(id, host, port);
} else
if (line.startsWith(F("model: ")) && new_bulb)
new_bulb->setModel(line.substring(7));
else
if (line.startsWith(F("name: ")) && new_bulb)
new_bulb->setName(line.substring(6)); // Currently, Yeelights always seem to return an empty name here :(
else
if (line.startsWith(F("power: ")) && new_bulb)
new_bulb->setPower(line.substring(7));
}
if (!new_bulb)
yield(); // Discovery process is lengthy; allow for background processing
}
return new_bulb;
}
// Declare a singleton-like instance
YDiscovery YDISCOVERY; // Global discovery handler