forked from arendst/Tasmota
-
Notifications
You must be signed in to change notification settings - Fork 20
Expand file tree
/
Copy pathdaikin_control.tc
More file actions
276 lines (250 loc) · 12.7 KB
/
Copy pathdaikin_control.tc
File metadata and controls
276 lines (250 loc) · 12.7 KB
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// daikin_control.tc — dedicated controller for Daikin WiFi A/C units
// (BRP069/airbase local HTTP API — the same units the home bridge reads).
//
// READS the full operational state of each unit and lets you START/STOP and set
// mode + target temperature. Everything is plain HTTP GET, so it needs no extra
// firmware support — just TinyC's httpGet().
//
// Status : live rows on the device main web page (WebCall).
// Web UI : per-unit Mode dropdown + Setpoint slider + Apply button, right on
// the main page (WebCall). Pick mode/temp, hit Apply — EverySecond
// queues it, TaskLoop sends it. Nothing blocks the web server.
// Control : GET /dk?u=<n>&pow=<0|1>&mode=<m>&stemp=<c> (for scripts/HA/curl)
// e.g. /dk?u=0&pow=1&mode=4&stemp=22 turn unit 0 to Heat 22 C
// /dk?u=0&pow=0 stop unit 0
// /dk?u=1&stemp=21 unit 1 keep mode, set 21 C
// Omitted fields keep the unit's current value. Returns JSON.
// Scriptable from a browser, curl, Home Assistant, Node-RED, rules.
//
// mode: 2=Dry 3=Cool 4=Heat 6=Fan-only 0/1/7=Auto
//
// Daikin quirk: set_control_info REQUIRES pow+mode+stemp+shum+f_rate+f_dir on
// every call — a partial write garbles the rest. So control is read-modify-write:
// read get_control_info, keep shum/f_rate/f_dir, override the rest, write it back.
#define NUNITS 2
// ── Your units — edit IPs/names ─────────────────────────────────────────────
char ip0[] = "192.168.188.24"; char nm0[] = "Wohnzimmer";
char ip1[] = "192.168.188.43"; char nm1[] = "Schlafzimmer";
// ── Cached operational state, one entry per unit ────────────────────────────
float d_htemp[NUNITS]; // inside temperature C
float d_otemp[NUNITS]; // outside temperature C
float d_hhum[NUNITS]; // inside humidity % (<=0 = not reported)
float d_stemp[NUNITS]; // target setpoint C
int d_pow[NUNITS]; // 0 = off, 1 = on
int d_mode[NUNITS]; // operation mode (see legend above)
int d_cmp[NUNITS]; // compressor frequency Hz (0 = idle, >0 = running)
int d_ok[NUNITS]; // last poll succeeded
char buf[256]; // shared httpGet response (>64 -> heap)
char url[160]; // shared URL builder (>64 -> heap)
// ── Pending-command queue ───────────────────────────────────────────────────
// The /dk web handler runs on the web/main task; TaskLoop's polling runs on the
// VM task. Two blocking httpGet()s overlapping from different tasks corrupt the
// HTTP client's heap (delayed crash/hang). So WebOn never does httpGet itself —
// it just records the request here, and TaskLoop (the sole VM task) executes it.
// All httpGet traffic is therefore serialized onto one task.
int req_pending; // 0 = idle, 1 = a command is waiting
int req_unit; // target unit index
int req_pow; // -1 = keep current
int req_mode; // -1 = keep current
float req_stemp; // <0 = keep current
// ── Web-UI controls (one Mode pulldown + Setpoint slider + Apply button/unit) ─
// These are `watch`ed so the firmware records UI writes (slider drag / dropdown
// pick / button click arrive as out-of-band ?sv= writes). EverySecond reacts to
// the Apply press only, so dragging the slider never spams the A/C — the chosen
// values just sit in c_state*/c_temp* until Apply is pressed.
// Mode dropdown index: 0=Off 1=Cool 2=Heat 3=Dry 4=Fan 5=Auto
watch int c_state0; watch int c_temp0; watch int c_apply0;
watch int c_state1; watch int c_temp1; watch int c_apply1;
int ui_synced; // 1 after widgets seeded from the first successful poll
// Find "key" in src and atof the value that follows. key MUST be a real char[]
// (caller strcpy's the literal in first) — passing a bare string literal through
// a char[] param can mis-resolve in TinyC. Returns -999 if the key is absent.
float dfield(char src[], char key[]) {
char v[16]; int p;
p = strFind(src, key);
if (p < 0) { return -999.0; }
strSub(v, src, p + strlen(key), 12); // numeric value; atof stops at ','
return atof(v);
}
void mode_name(char dst[], int m) {
if (m == 2) { strcpy(dst, "Dry"); }
else if (m == 3) { strcpy(dst, "Cool"); }
else if (m == 4) { strcpy(dst, "Heat"); }
else if (m == 6) { strcpy(dst, "Fan"); }
else { strcpy(dst, "Auto"); }
}
// Dropdown index (0=Off 1=Cool 2=Heat 3=Dry 4=Fan 5=Auto) → current A/C state.
int state_from(int pow, int m) {
if (pow == 0) { return 0; }
if (m == 3) { return 1; }
if (m == 4) { return 2; }
if (m == 2) { return 3; }
if (m == 6) { return 4; }
return 5; // 0/1/7 → Auto
}
// Translate a dropdown index + slider temp into a queued req_* command.
void queue_unit(int u, int stateidx, int tempv) {
int pw; int md;
pw = 1; md = -1;
if (stateidx == 0) { pw = 0; md = -1; } // Off (keep mode)
else if (stateidx == 1) { md = 3; } // Cool
else if (stateidx == 2) { md = 4; } // Heat
else if (stateidx == 3) { md = 2; } // Dry
else if (stateidx == 4) { md = 6; } // Fan
else { md = 1; } // Auto
req_unit = u; req_pow = pw; req_mode = md; req_stemp = (float)tempv;
req_pending = 1; // flag LAST
}
// Poll one unit: get_sensor_info (temps/humidity/compressor) + get_control_info
// (power/mode/setpoint). Updates the cached state for index u.
void daikin_read(char ip[], int u) {
char k[12]; float f;
// no httpGet before WiFi is up — corrupts the heap + boot-loops an autoexec slot
if (!tasm_wifi) { return; }
d_ok[u] = 0;
sprintf(url, "http://%s/aircon/get_sensor_info", ip);
buf[0] = 0;
if (httpGet(url, buf) > 0) {
strcpy(k, "htemp="); f = dfield(buf, k); if (f > -90.0) { d_htemp[u] = f; }
strcpy(k, "otemp="); f = dfield(buf, k); if (f > -90.0) { d_otemp[u] = f; }
strcpy(k, "hhum="); f = dfield(buf, k); d_hhum[u] = f; // '-' -> ~0
strcpy(k, "cmpfreq="); f = dfield(buf, k); if (f >= 0.0) { d_cmp[u] = (int)f; }
d_ok[u] = 1;
}
sprintf(url, "http://%s/aircon/get_control_info", ip);
buf[0] = 0;
if (httpGet(url, buf) > 0) {
strcpy(k, "pow="); f = dfield(buf, k); if (f >= 0.0) { d_pow[u] = (int)f; }
strcpy(k, "mode="); f = dfield(buf, k); if (f >= 0.0) { d_mode[u] = (int)f; }
strcpy(k, "stemp="); f = dfield(buf, k); if (f > 0.0) { d_stemp[u] = f; }
}
}
// Start/stop + set mode + setpoint (read-modify-write so shum/f_rate/f_dir are
// preserved exactly, as the Daikin requires).
void daikin_set(char ip[], int pow, int mode, float stemp) {
int shum; int fdir; char frate[8]; char tmp[12]; int p;
// no httpGet before WiFi is up — corrupts the heap + boot-loops an autoexec slot
if (!tasm_wifi) { return; }
shum = 0; fdir = 0; strcpy(frate, "A");
sprintf(url, "http://%s/aircon/get_control_info", ip);
buf[0] = 0;
if (httpGet(url, buf) > 0) {
p = strFind(buf, "shum="); if (p >= 0) { strSub(tmp, buf, p + 5, 8); shum = (int)atof(tmp); }
p = strFind(buf, "f_dir="); if (p >= 0) { strSub(tmp, buf, p + 6, 8); fdir = (int)atof(tmp); }
p = strFind(buf, "f_rate="); if (p >= 0) { strSub(tmp, buf, p + 7, 8); strToken(frate, tmp, ',', 0); }
}
sprintf(url, "http://%s/aircon/set_control_info?pow=%d&mode=%d&stemp=%.1f&shum=%d&f_rate=%s&f_dir=%d",
ip, pow, mode, stemp, shum, frate, fdir);
httpGet(url, buf);
addLog(url);
}
// Emit one unit's live status row (actual A/C state from the last poll).
void status_row(char nm[], int u) {
char row[160]; char mn[8];
mode_name(mn, d_mode[u]);
if (d_pow[u]) {
sprintf(row, "{s}%s{m}ON %s set %.1f (in %.1f / out %.1f){e}",
nm, mn, d_stemp[u], d_htemp[u], d_otemp[u]);
} else {
sprintf(row, "{s}%s{m}OFF (in %.1f / out %.1f){e}",
nm, d_htemp[u], d_otemp[u]);
}
webSend(row);
}
// Status rows + interactive controls on the device main page. Widgets MUST be
// rendered here (FUNC_WEB_SENSOR), never in main() — emitting them with no active
// web request corrupts the heap. The widget vars are bound by reference, so each
// unit is spelled out (a helper param can't carry a watched-global binding).
void WebCall() {
status_row(nm0, 0);
webPulldown(c_state0, "WZ Mode", "Off|Cool|Heat|Dry|Fan|Auto");
webSlider(c_temp0, 16, 30, "WZ Setpoint");
webButton(c_apply0, "Apply WZ|Sent");
status_row(nm1, 1);
webPulldown(c_state1, "SZ Mode", "Off|Cool|Heat|Dry|Fan|Auto");
webSlider(c_temp1, 16, 30, "SZ Setpoint");
webButton(c_apply1, "Apply SZ|Sent");
}
// React to Apply clicks (out-of-band writes set c_applyN to 1). No httpGet here —
// just queue; TaskLoop sends it. Skip while a request is still unconsumed so two
// quick Applies can't clobber each other; the button stays set and retries.
void EverySecond() {
if (c_apply0 != 0 && req_pending == 0) { queue_unit(0, c_state0, c_temp0); c_apply0 = 0; }
if (c_apply1 != 0 && req_pending == 0) { queue_unit(1, c_state1, c_temp1); c_apply1 = 0; }
}
// Control endpoint: /dk?u=&pow=&mode=&stemp= (omitted fields keep current).
// Runs on the web task — it MUST NOT call httpGet (see req_pending comment). It
// only records the request; TaskLoop executes the actual set on the VM task and
// also refreshes the cache, so the reply here reflects what's been queued.
void WebOn() {
int h; char a[16]; char resp[96]; int u; int pow; int mode; float stemp;
h = webHandler();
if (h == 1) {
u = 0; pow = -1; mode = -1; stemp = -1.0;
if (webArg("u", a) > 0) { u = (int)atof(a); }
if (u < 0) { u = 0; }
if (u >= NUNITS) { u = NUNITS - 1; }
if (webArg("pow", a) > 0) { pow = (int)atof(a); }
if (webArg("mode", a) > 0) { mode = (int)atof(a); }
if (webArg("stemp", a) > 0) { stemp = atof(a); }
// publish the request, flag LAST so TaskLoop never sees a half-write
req_unit = u; req_pow = pow; req_mode = mode; req_stemp = stemp;
req_pending = 1;
sprintf(resp, "{\"unit\":%d,\"queued\":1,\"pow\":%d,\"mode\":%d,\"stemp\":%.1f}",
u, pow, mode, stemp);
webSend(resp);
}
}
// Execute a queued command on the VM task: fill omitted fields from cache, write
// it, then immediately re-read so the status rows reflect the new state.
void daikin_apply() {
int u; int pow; int mode; float stemp;
u = req_unit; pow = req_pow; mode = req_mode; stemp = req_stemp;
req_pending = 0;
if (pow < 0) { pow = d_pow[u]; }
if (mode < 0) { mode = d_mode[u]; }
if (stemp < 0.0) { stemp = d_stemp[u]; }
if (stemp < 10.0) { stemp = 22.0; } // sane floor if never read yet
if (u == 0) { daikin_set(ip0, pow, mode, stemp); daikin_read(ip0, 0); }
else { daikin_set(ip1, pow, mode, stemp); daikin_read(ip1, 1); }
}
// Poll loop — ALL blocking httpGet lives here (never EverySecond, never a web
// handler). Polls both units ~once a minute, but wakes every second to run any
// command the /dk handler has queued, so control feels responsive.
void TaskLoop() {
int t;
delay(8000); // let WiFi come up
while (1) {
daikin_read(ip0, 0);
delay(2000);
daikin_read(ip1, 1);
if (ui_synced == 0) { // seed widgets from the first real poll
if (d_ok[0]) { c_temp0 = (int)d_stemp[0]; c_state0 = state_from(d_pow[0], d_mode[0]); }
if (d_ok[1]) { c_temp1 = (int)d_stemp[1]; c_state1 = state_from(d_pow[1], d_mode[1]); }
if (d_ok[0] || d_ok[1]) { ui_synced = 1; }
}
t = 0;
while (t < 58) { // ~58 s idle, checking the queue each second
if (req_pending) { daikin_apply(); }
delay(1000);
t = t + 1;
}
}
}
int main() {
int u;
u = 0;
while (u < NUNITS) {
d_htemp[u] = 0.0; d_otemp[u] = 0.0; d_hhum[u] = -1.0;
d_stemp[u] = 0.0; d_pow[u] = 0; d_mode[u] = 0; d_cmp[u] = 0; d_ok[u] = 0;
u = u + 1;
}
req_pending = 0; req_unit = 0; req_pow = -1; req_mode = -1; req_stemp = -1.0;
c_state0 = 0; c_temp0 = 22; c_apply0 = 0;
c_state1 = 0; c_temp1 = 22; c_apply1 = 0;
ui_synced = 0;
webOn(1, "/dk");
printStr("Daikin controller ready.\n");
printStr("UI: device main page (Mode/Setpoint/Apply). API: /dk?u=0&pow=1&mode=4&stemp=22\n");
return 0;
}