mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-11-01 22:32:13 +01:00
Merge pull request #42 from cjkas/refactor/model-editor
refactor: apply shade model edits on loop() through ModelEditor
This commit is contained in:
commit
e39ec13884
20 changed files with 1035 additions and 918 deletions
|
|
@ -245,7 +245,7 @@ var httpStatusText = {
|
|||
'504': 'Gateway Timeout',
|
||||
'505': 'HTTP Version Not Supported'
|
||||
};
|
||||
function getJSON(url, cb) {
|
||||
function getJSON(url, cb, attempt) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
console.log({ get: url });
|
||||
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}${url}` : url, true);
|
||||
|
|
@ -253,6 +253,10 @@ function getJSON(url, cb) {
|
|||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
let status = xhr.status;
|
||||
if (isBusy(status, xhr.response) && (attempt || 0) < 10) {
|
||||
setTimeout(() => getJSON(url, cb, (attempt || 0) + 1), 250);
|
||||
return;
|
||||
}
|
||||
if (status !== 200) {
|
||||
let err = xhr.response || {};
|
||||
err.htmlError = status;
|
||||
|
|
@ -274,12 +278,17 @@ function getJSON(url, cb) {
|
|||
};
|
||||
xhr.send();
|
||||
}
|
||||
function getJSONSync(url, cb) {
|
||||
function getJSONSync(url, cb, attempt) {
|
||||
let overlay = ui.waitMessage(document.getElementById('divContainer'));
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.responseType = 'json';
|
||||
xhr.onload = () => {
|
||||
let status = xhr.status;
|
||||
if (isBusy(status, xhr.response) && (attempt || 0) < 10) {
|
||||
if (typeof overlay !== 'undefined') overlay.remove();
|
||||
setTimeout(() => getJSONSync(url, cb, (attempt || 0) + 1), 250);
|
||||
return;
|
||||
}
|
||||
if (status !== 200) {
|
||||
let err = xhr.response || {};
|
||||
err.htmlError = status;
|
||||
|
|
@ -453,6 +462,79 @@ function putJSONSync(url, data, cb) {
|
|||
xhr.send(JSON.stringify(data));
|
||||
} catch (err) { ui.serviceError(document.getElementById('divContainer'), err); }
|
||||
}
|
||||
// Edits of rooms, shades, groups and repeaters run on the device's main loop
|
||||
// after the reply, so the reply only says the edit was accepted. How it went
|
||||
// comes over the socket as requestResult with the same requestId. On success
|
||||
// readUrl(id) is read back, so cb gets what the edit route itself used to
|
||||
// return; without readUrl cb gets the result.
|
||||
var pendingEdits = {};
|
||||
var earlyResults = {};
|
||||
function editJSON(url, data, readUrl, cb) {
|
||||
let overlay = ui.waitMessage(document.getElementById('divContainer'));
|
||||
let attempts = 0;
|
||||
let finish = (result) => {
|
||||
overlay.remove();
|
||||
if (!result.ok) {
|
||||
let err = { htmlError: 500, service: `PUT ${url}`, desc: result.desc, data: data };
|
||||
if (result.code) err.code = result.code;
|
||||
cb(err, null);
|
||||
}
|
||||
else if (typeof readUrl === 'function') getJSONSync(readUrl(result.id), cb);
|
||||
else cb(null, result);
|
||||
};
|
||||
let send = () => {
|
||||
putJSON(url, data, (err, reply) => {
|
||||
// Another edit is still being applied.
|
||||
if (err && err.htmlError === 503 && typeof err.desc === 'string' && err.desc.startsWith('Busy') && ++attempts < 10) {
|
||||
setTimeout(send, 300);
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
overlay.remove();
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
let id = reply.requestId;
|
||||
if (typeof earlyResults[id] !== 'undefined') {
|
||||
let result = earlyResults[id];
|
||||
delete earlyResults[id];
|
||||
finish(result);
|
||||
return;
|
||||
}
|
||||
let timer = setTimeout(() => {
|
||||
delete pendingEdits[id];
|
||||
overlay.remove();
|
||||
let prompt = ui.promptMessage('The device did not confirm the change.', () => { location.reload(); });
|
||||
prompt.querySelector('.sub-message').innerHTML = '<p>It may or may not have been made. Press YES to reload the page and see.</p>';
|
||||
}, 5000);
|
||||
pendingEdits[id] = (result) => {
|
||||
clearTimeout(timer);
|
||||
finish(result);
|
||||
};
|
||||
});
|
||||
};
|
||||
send();
|
||||
}
|
||||
function procRequestResult(result) {
|
||||
let fn = pendingEdits[result.requestId];
|
||||
if (typeof fn === 'function') {
|
||||
delete pendingEdits[result.requestId];
|
||||
fn(result);
|
||||
}
|
||||
// The socket can beat the reply that carries the id. Results of other
|
||||
// clients' edits land here too, so only the latest few are kept.
|
||||
else {
|
||||
earlyResults[result.requestId] = result;
|
||||
let ids = Object.keys(earlyResults);
|
||||
if (ids.length > 10) delete earlyResults[ids[0]];
|
||||
}
|
||||
}
|
||||
function editsPending() { return Object.keys(pendingEdits).length > 0; }
|
||||
// Reads are served from a copy of the shade model the device takes for each,
|
||||
// and it takes at most two at once: a third is told to try again.
|
||||
function isBusy(status, response) {
|
||||
return status === 503 && response && typeof response.desc === 'string' && response.desc.startsWith('Busy');
|
||||
}
|
||||
var socket;
|
||||
var tConnect = null;
|
||||
var sockIsOpen = false;
|
||||
|
|
@ -515,15 +597,24 @@ async function initSockets() {
|
|||
case 'shadeCommand':
|
||||
console.log(msg);
|
||||
break;
|
||||
case 'requestResult':
|
||||
procRequestResult(msg);
|
||||
break;
|
||||
case 'roomRemoved':
|
||||
somfy.procRoomRemoved(msg);
|
||||
break;
|
||||
case 'roomAdded':
|
||||
somfy.procRoomAdded(msg);
|
||||
break;
|
||||
// Another client changed the lists. This one's own edits
|
||||
// refresh them when their result comes in.
|
||||
case 'shadeRemoved':
|
||||
break;
|
||||
case 'shadeAdded':
|
||||
if (!editsPending()) somfy.updateShadeList();
|
||||
break;
|
||||
case 'groupRemoved':
|
||||
case 'groupAdded':
|
||||
if (!editsPending()) somfy.updateGroupList();
|
||||
break;
|
||||
case 'ethernet':
|
||||
wifi.procEthernet(msg);
|
||||
|
|
@ -2181,7 +2272,7 @@ class Somfy {
|
|||
if (room.roomId === 0) return;
|
||||
let r = _rooms.find(x => x.roomId === room.roomId);
|
||||
if (typeof r !== 'undefined' && r.roomId === room.roomId) {
|
||||
_rooms = _rooms.filter(x => x.roomId === room.roomId);
|
||||
_rooms = _rooms.filter(x => x.roomId !== room.roomId);
|
||||
_rooms.sort((a, b) => { return a.sortOrder - b.sortOrder });
|
||||
this.setRoomsList(_rooms);
|
||||
let rs = document.getElementById('divRoomSelector');
|
||||
|
|
@ -2259,7 +2350,7 @@ class Somfy {
|
|||
order.push(parseInt(items[i].getAttribute('data-roomid'), 10));
|
||||
// Reorder the shades on the main page.
|
||||
}
|
||||
putJSONSync('/roomSortOrder', order, (err) => {
|
||||
editJSON('/roomSortOrder', order, null, (err) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else this.updateRoomsList();
|
||||
});
|
||||
|
|
@ -2455,7 +2546,7 @@ class Somfy {
|
|||
order.push(parseInt(items[i].getAttribute('data-shadeid'), 10));
|
||||
// Reorder the shades on the main page.
|
||||
}
|
||||
putJSONSync('/shadeSortOrder', order, (err) => {
|
||||
editJSON('/shadeSortOrder', order, null, (err) => {
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
let el = shadeControls.querySelector(`.somfyShadeCtl[data-shadeid="${order[i]}"`);
|
||||
if (el) {
|
||||
|
|
@ -2700,7 +2791,7 @@ class Somfy {
|
|||
order.push(parseInt(items[i].getAttribute('data-groupid'), 10));
|
||||
// Reorder the shades on the main page.
|
||||
}
|
||||
putJSONSync('/groupSortOrder', order, (err) => {
|
||||
editJSON('/groupSortOrder', order, null, (err) => {
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
let el = groupControls.querySelector(`.somfyGroupCtl[data-groupid="${order[i]}"`);
|
||||
if (el) {
|
||||
|
|
@ -2909,18 +3000,17 @@ class Somfy {
|
|||
remoteAddress: frame.address,
|
||||
rollingCode: frame.rcode
|
||||
};
|
||||
let overlay = ui.waitMessage(document.getElementById('divLinking'));
|
||||
putJSON('/linkRemote', obj, (err, shade) => {
|
||||
editJSON('/linkRemote', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||
console.log(shade);
|
||||
overlay.remove();
|
||||
lnk.remove();
|
||||
this.setLinkedRemotesList(shade);
|
||||
if (err) ui.serviceError(err);
|
||||
else this.setLinkedRemotesList(shade);
|
||||
});
|
||||
}
|
||||
else {
|
||||
lnk = document.getElementById('divLinkRepeater');
|
||||
if (lnk) {
|
||||
putJSONSync(`/linkRepeater`, {address:frame.address}, (err, repeaters) => {
|
||||
editJSON(`/linkRepeater`, {address:frame.address}, () => '/repeaters', (err, repeaters) => {
|
||||
lnk.remove();
|
||||
if (err) ui.serviceError(err);
|
||||
else this.setRepeaterList(repeaters);
|
||||
|
|
@ -3251,7 +3341,7 @@ class Somfy {
|
|||
if (valid) {
|
||||
if (isNaN(roomId) || roomId === 0) {
|
||||
// We are adding.
|
||||
putJSONSync('/addRoom', obj, (err, room) => {
|
||||
editJSON('/addRoom', obj, (id) => `/room?roomId=${id}`, (err, room) => {
|
||||
if (err) {
|
||||
ui.serviceError(err);
|
||||
console.log(err);
|
||||
|
|
@ -3267,7 +3357,7 @@ class Somfy {
|
|||
}
|
||||
else {
|
||||
obj.roomId = roomId;
|
||||
putJSONSync('/saveRoom', obj, (err, room) => {
|
||||
editJSON('/saveRoom', obj, (id) => `/room?roomId=${id}`, (err, room) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else this.updateRoomsList();
|
||||
console.log(room);
|
||||
|
|
@ -3324,7 +3414,7 @@ class Somfy {
|
|||
if (valid) {
|
||||
if (isNaN(shadeId) || shadeId >= 255) {
|
||||
// We are adding.
|
||||
putJSONSync('/addShade', obj, (err, shade) => {
|
||||
editJSON('/addShade', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||
if (err) {
|
||||
ui.serviceError(err);
|
||||
console.log(err);
|
||||
|
|
@ -3343,9 +3433,12 @@ class Somfy {
|
|||
}
|
||||
else {
|
||||
obj.shadeId = shadeId;
|
||||
putJSONSync('/saveShade', obj, (err, shade) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else this.updateShadeList();
|
||||
editJSON('/saveShade', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||
if (err) {
|
||||
ui.serviceError(err);
|
||||
return;
|
||||
}
|
||||
this.updateShadeList();
|
||||
console.log(shade);
|
||||
let ico = document.getElementById('icoShade');
|
||||
let tilt = ico.parentElement.querySelector('i.icss-window-tilt');
|
||||
|
|
@ -3376,7 +3469,7 @@ class Somfy {
|
|||
if (valid) {
|
||||
if (isNaN(groupId) || groupId >= 255) {
|
||||
// We are adding.
|
||||
putJSONSync('/addGroup', obj, (err, group) => {
|
||||
editJSON('/addGroup', obj, (id) => `/group?groupId=${id}`, (err, group) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else {
|
||||
console.log(group);
|
||||
|
|
@ -3391,7 +3484,7 @@ class Somfy {
|
|||
}
|
||||
else {
|
||||
obj.groupId = groupId;
|
||||
putJSONSync('/saveGroup', obj, (err, shade) => {
|
||||
editJSON('/saveGroup', obj, (id) => `/group?groupId=${id}`, (err, shade) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else this.updateGroupList();
|
||||
console.log(shade);
|
||||
|
|
@ -3458,7 +3551,7 @@ class Somfy {
|
|||
else {
|
||||
let prompt = ui.promptMessage(`Are you sure you want to delete this room?`, () => {
|
||||
ui.clearErrors();
|
||||
putJSONSync('/deleteRoom', { roomId: roomId }, (err, room) => {
|
||||
editJSON('/deleteRoom', { roomId: roomId }, null, (err, room) => {
|
||||
prompt.remove();
|
||||
if (err) ui.serviceError(err);
|
||||
else
|
||||
|
|
@ -3483,9 +3576,10 @@ class Somfy {
|
|||
else {
|
||||
let prompt = ui.promptMessage(`Are you sure you want to delete this shade?`, () => {
|
||||
ui.clearErrors();
|
||||
putJSONSync('/deleteShade', { shadeId: shadeId }, (err, shade) => {
|
||||
editJSON('/deleteShade', { shadeId: shadeId }, null, (err, shade) => {
|
||||
prompt.remove();
|
||||
if (err) ui.serviceError(err);
|
||||
this.updateShadeList();
|
||||
prompt.remove;
|
||||
});
|
||||
});
|
||||
prompt.querySelector('.sub-message').innerHTML = `<p>If this shade was previously paired with a motor, you should first unpair it from the motor and remove it from any groups. Otherwise its address will remain in the motor memory.</p><p>Press YES to delete ${shade.name} or NO to cancel this operation.</p>`;
|
||||
|
|
@ -3508,7 +3602,7 @@ class Somfy {
|
|||
}
|
||||
else {
|
||||
let prompt = ui.promptMessage(`Are you sure you want to delete this group?`, () => {
|
||||
putJSONSync('/deleteGroup', { groupId: groupId }, (err, g) => {
|
||||
editJSON('/deleteGroup', { groupId: groupId }, null, (err, g) => {
|
||||
if (err) ui.serviceError(err);
|
||||
this.updateGroupList();
|
||||
prompt.remove();
|
||||
|
|
@ -3631,7 +3725,7 @@ class Somfy {
|
|||
let obj = { shadeId: shadeId, paired: paired || false };
|
||||
let div = document.getElementById('divPairing');
|
||||
let overlay = typeof div === 'undefined' ? undefined : ui.waitMessage(div);
|
||||
putJSONSync('/setPaired', obj, (err, shade) => {
|
||||
editJSON('/setPaired', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||
if (overlay) overlay.remove();
|
||||
if (err) {
|
||||
console.log(err);
|
||||
|
|
@ -4004,9 +4098,10 @@ class Somfy {
|
|||
mouseDown = false;
|
||||
let obj = ui.fromElement(div);
|
||||
let prompt = ui.promptMessage('Confirm Motor Response', () => {
|
||||
putJSONSync('/linkToGroup', { groupId: groupId, shadeId: obj.shadeId }, (err, group) => {
|
||||
editJSON('/linkToGroup', { groupId: groupId, shadeId: obj.shadeId }, (id) => `/group?groupId=${id}`, (err, group) => {
|
||||
console.log(group);
|
||||
somfy.setLinkedShadesList(group);
|
||||
if (err) ui.serviceError(err);
|
||||
else somfy.setLinkedShadesList(group);
|
||||
this.updateGroupList();
|
||||
});
|
||||
prompt.remove();
|
||||
|
|
@ -4098,9 +4193,10 @@ class Somfy {
|
|||
if (err) ui.serviceError(err);
|
||||
else {
|
||||
let prompt = ui.promptMessage('Confirm Motor Response', () => {
|
||||
putJSONSync('/unlinkFromGroup', { groupId: groupId, shadeId: shadeId }, (err, group) => {
|
||||
editJSON('/unlinkFromGroup', { groupId: groupId, shadeId: shadeId }, (id) => `/group?groupId=${id}`, (err, group) => {
|
||||
console.log(group);
|
||||
somfy.setLinkedShadesList(group);
|
||||
if (err) ui.serviceError(err);
|
||||
else somfy.setLinkedShadesList(group);
|
||||
this.updateGroupList();
|
||||
});
|
||||
prompt.remove();
|
||||
|
|
@ -4136,7 +4232,7 @@ class Somfy {
|
|||
}
|
||||
unlinkRepeater(address) {
|
||||
let prompt = ui.promptMessage('Are you sure you want to stop repeating frames from this address?', () => {
|
||||
putJSONSync('/unlinkRepeater', { address: address }, (err, repeaters) => {
|
||||
editJSON('/unlinkRepeater', { address: address }, () => '/repeaters', (err, repeaters) => {
|
||||
if (err) ui.serviceError(err);
|
||||
else this.setRepeaterList(repeaters);
|
||||
prompt.remove();
|
||||
|
|
@ -4151,11 +4247,11 @@ class Somfy {
|
|||
shadeId: shadeId,
|
||||
remoteAddress: remoteAddress
|
||||
};
|
||||
putJSONSync('/unlinkRemote', obj, (err, shade) => {
|
||||
|
||||
editJSON('/unlinkRemote', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||
console.log(shade);
|
||||
prompt.remove();
|
||||
this.setLinkedRemotesList(shade);
|
||||
if (err) ui.serviceError(err);
|
||||
else this.setLinkedRemotesList(shade);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## Concurrent mutation during chunked `/controller` response
|
||||
|
||||
**Status:** fixed for the read routes; the web-task writers below are still open.
|
||||
**Status:** fixed: reads in step 2, edits in step 3; command replies still read the live model (below).
|
||||
**Introduced:** 2026-04-19, alongside the chunked-response conversion of `/controller` (the original crash report was about `cbuf::resize` aborting `async_tcp`).
|
||||
**Fixed:** 2026-10-16, step 2 of [LOOP_QUEUE_PLAN.md](LOOP_QUEUE_PLAN.md).
|
||||
**Fixed:** 2026-10-16, steps 2 and 3 of [LOOP_QUEUE_PLAN.md](LOOP_QUEUE_PLAN.md).
|
||||
|
||||
### What it was
|
||||
|
||||
|
|
@ -12,16 +12,13 @@ The chunked `/controller` response read `somfy.rooms`, `somfy.shades`, `somfy.gr
|
|||
|
||||
All eight read routes (`/controller`, `/discovery`, `/rooms`, `/shades`, `/groups`, `/room`, `/shade`, `/group`) now stream a ModelSnapshot that `loop()` fills, so each document describes one moment ([ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md)). That is fix option (c), taken per read in a heap slot of about 5.6 KB.
|
||||
|
||||
### Edits (step 3)
|
||||
|
||||
The handlers that changed the shade model on the web server task (adding, saving and deleting rooms, shades and groups; linking remotes, repeaters and group members; `/setPaired`; the sort orders) now hand the edit to `loop()` through the LoopQueue, and every client learns the result from a `requestResult` socket event. So `/linkRemote` can no longer race a received frame for a linked remote's rolling code: both now change it on `loop()`, in the order they happen.
|
||||
|
||||
### Still open
|
||||
|
||||
- Command and edit replies (`/shadeCommand`, `/saveShade`, `/linkRemote`, ...) still copy the one item they report from the live shade model on async_tcp, so a reply can show a value from the middle of a change. `/room`, `/shade` and `/group` also check on the live model whether the item exists, because the status goes out before the snapshot is filled.
|
||||
- Other handlers on the web server task still change the shade model that `loop()` owns (step 3 of the plan):
|
||||
- `/addRoom`, `/saveRoom`, `/deleteRoom`, `/addShade`, `/saveShade`, `/deleteShade`, `/addGroup`, `/saveGroup` and `/deleteGroup` add, change and remove rooms, shades and groups.
|
||||
- `/linkRemote` and `/unlinkRemote` change a shade's linked remotes. `/linkRemote` also sets a linked remote's rolling code, which `loop()` updates whenever it receives a frame from that remote: the last write wins, so a received frame can undo the code just set.
|
||||
- `/linkToGroup` and `/unlinkFromGroup` change a group's linked shades.
|
||||
- `/setPaired` sets `paired`.
|
||||
|
||||
Shade requests (shade and group commands, set-My, unpair, `/setPositions`, `/setRollingCode`, and every MQTT command), a shades restore and `/updateShadeConfig` already go through the LoopQueue to `loop()`.
|
||||
- Command replies (`/shadeCommand`, `/tiltCommand`, `/unpairShade`, `/setRollingCode`, ...) still copy the one item they report from the live shade model on async_tcp, so a reply can show a value from the middle of a change. `/room`, `/shade`, `/group` and `/groupOptions` also check on the live model whether the item exists, because the status goes out before the snapshot is filled.
|
||||
- The read routes leave out an item that doesn't fit their 3 KB unit, see [Oversized read items are left out](#oversized-read-items-are-left-out).
|
||||
|
||||
## Oversized read items are left out
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Plan: domain modules behind the LoopQueue
|
||||
|
||||
**Status:** steps 0–2 built (PRs #39, #40 and #41); steps 3–5 planned.
|
||||
**Status:** steps 0–3 built (PRs #39, #40, #41 and #42); steps 4 and 5 planned.
|
||||
**Written:** 2026-10-15, against commit `e624a41` on `refactor/entry-points-outputs`. Terms are defined in [GLOSSARY.md](../GLOSSARY.md); the threading rule is [ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
|
||||
## Why
|
||||
|
|
@ -144,6 +144,20 @@ Flash: `esp32devdbg` 1,546,405 → 1,547,381 bytes, `esp32dev` 1,396,957 → 1,3
|
|||
|
||||
Flash: `esp32devdbg` 1,547,381 → 1,547,821 bytes, `esp32dev` 1,398,337 → 1,400,401 bytes.
|
||||
|
||||
### Step 3: what changed in behaviour
|
||||
- **Every edit of the shade model runs on `loop()`.** The 19 edit routes reply `{"status":"accepted","requestId":N}` and queue a copy of the request. `loop()` applies it and broadcasts `requestResult {requestId, ok, id, code, desc}`. The errors that used to come in the HTTP reply ("Shade Id not found.", "Data Error." with its code, and so on) come in `requestResult` instead.
|
||||
- **One edit at a time.** A second edit gets 503 "Busy, try the change again", and the web UI retries it. A body over 4 KB gets 413.
|
||||
- **The web UI waits for the result** and then reads the item back (`/shade`, `/group`, `/room`, `/repeaters`), so the code that used the old reply body is unchanged. If no result comes within 5 s, it offers to reload the page. Reads that get 503 "Busy" are retried too.
|
||||
- **Added, removed and saved items reach every client.** Saving sends `shadeState`, `groupState` or `roomState`. The web UI now acts on `shadeAdded`, `shadeRemoved`, `groupAdded` and `groupRemoved` from other clients.
|
||||
- **A shade that fails validation is no longer added.** `/addShade` used to keep the new shade with defaults when its pins clashed.
|
||||
- **Sort orders and linked repeaters are saved.** Linking a remote, a repeater or a group member reports when the list is full.
|
||||
- **`/getNextRoom`, `/getNextShade`, `/getNextGroup`, `/groupOptions` and the new `/repeaters`** are snapshot reads.
|
||||
- **ModelEditor's rules** (`ModelEditor.h/.cpp`) replace the six `getNext*Id`/`getMax*Order` copies, the remote-address search, the repeater and group-member list code, and the pin-conflict check. The pin check now uses the protocol being saved and checks every pin against Ethernet.
|
||||
|
||||
`SomfyEditor.h` declares the edit handed to `loop()`. The code that applies it lives in `SomfyJson.cpp`, next to `fromJSON`, so ArduinoJson stays in one file. 27 new native tests, 319 in all.
|
||||
|
||||
Flash: `esp32devdbg` 1,547,821 → 1,536,337 bytes, `esp32dev` 1,400,401 → 1,393,221 bytes. The 19 handler bodies became one path.
|
||||
|
||||
### Step 0 on a device
|
||||
NVS stays at `0x9000`, so the rolling codes survive. The file system partition shrinks from `0xE0000` to `0xD0000`, and the existing LittleFS won't mount, because `LittleFS.begin()` doesn't format. So:
|
||||
1. Take a backup through `/backup`.
|
||||
|
|
@ -159,12 +173,17 @@ NVS stays at `0x9000`, so the rolling codes survive. The file system partition s
|
|||
|
||||
## Found during the review, not part of this plan
|
||||
|
||||
These are open, and each can be fixed on its own:
|
||||
Still open, and each can be fixed on its own:
|
||||
- MQTT `position`/`target` published as a constant 50 ([SomfyShade.cpp:305](../src/SomfyShade.cpp#L305), [SomfyMqtt.cpp:36-38](../src/outputs/SomfyMqtt.cpp#L36-L38)). It's a one-line fix that needn't wait for step 4.
|
||||
- A restore without shades skips the shade and group records but not the room records, so it reads the rest of the file from the wrong place ([ConfigFile.cpp:578-580](../src/ConfigFile.cpp#L578-L580)).
|
||||
- The Ethernet pin conflict is only checked for the up pin ([SomfyJson.cpp:63-65](../src/SomfyJson.cpp#L63-L65)).
|
||||
- `linkRepeater` never sets `isDirty`, so a newly linked repeater isn't saved ([ShadeController.cpp:323](../src/ShadeController.cpp#L323)).
|
||||
- `/linkRemote` and `/unlinkRemote` send a 500 and then a second response ([Web.cpp:2055](../src/entryPoints/Web.cpp#L2055), [2083](../src/entryPoints/Web.cpp#L2083)).
|
||||
- `settingPos` is never cleared when the lift arrives, while `settingTiltPos` is. A later arrival may send an extra stop. Not confirmed on a device.
|
||||
- The web UI's `procRoomRemoved` keeps the removed room and drops the others (`data-src/index.js:2184`).
|
||||
- The Home Assistant integration sends no `apikey` header on shade, group and tilt commands.
|
||||
- New ids stop two short of the maximum: 30 shades, 14 groups and 14 rooms instead of 32, 16 and 16 (`getNext*Id` stopped at `MAX - 1`). ModelEditor keeps those limits for now.
|
||||
- `SomfyGroup::fromJSON` reads `linkedShades` into a local array and drops it, so a group's members only change through `/linkToGroup` and `/unlinkFromGroup`.
|
||||
|
||||
Fixed by step 3:
|
||||
- The Ethernet pin conflict was only checked for the up pin, and the check used the shade's current protocol instead of the one being saved.
|
||||
- `linkRepeater` never set `isDirty`, so a newly linked repeater wasn't saved. The sort orders weren't saved either.
|
||||
- `/linkRemote` and `/unlinkRemote` sent a 500 and then a second response.
|
||||
- The web UI's `procRoomRemoved` kept the removed room and dropped the others, and `deleteShade` never closed its prompt (`prompt.remove;`).
|
||||
- The web UI called `/repeaters`, which didn't exist.
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ lib_deps =
|
|||
platform = native
|
||||
test_framework = unity
|
||||
test_build_src = yes
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ModelSnapshot.cpp>
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ModelSnapshot.cpp> +<ModelEditor.cpp>
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
-Isrc
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "ConfigFile.h"
|
||||
#include "Somfy.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Utils.h"
|
||||
|
||||
static const char *TAG = "LoopQueue";
|
||||
|
|
@ -66,6 +67,9 @@ void LoopQueue::drain() {
|
|||
case loop_item_type_t::snapshot_fill:
|
||||
snapshotSlots.fill(item.slot, fillSnapshot);
|
||||
break;
|
||||
case loop_item_type_t::model_edit:
|
||||
runModelEdit(item.edit);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,13 +23,16 @@ enum class loop_item_type_t : uint8_t {
|
|||
shade_request, // run by ShadeRequests
|
||||
restore_shades, // restore from /shades.tmp, then reboot
|
||||
load_shades, // replace the shades with /shades.tmp
|
||||
snapshot_fill // fill a read's ModelSnapshot
|
||||
snapshot_fill, // fill a read's ModelSnapshot
|
||||
model_edit // apply an edit with ModelEditor
|
||||
};
|
||||
struct model_edit_t;
|
||||
struct loop_item_t {
|
||||
loop_item_type_t type = loop_item_type_t::shade_request;
|
||||
shade_request_t request; // shade_request
|
||||
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
||||
uint8_t slot = 0; // snapshot_fill: the SnapshotSlots slot
|
||||
shade_request_t request; // shade_request
|
||||
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
||||
uint8_t slot = 0; // snapshot_fill: the SnapshotSlots slot
|
||||
model_edit_t *edit = nullptr; // model_edit: loop() runs and frees it
|
||||
};
|
||||
|
||||
// Whether the item sends anything on air.
|
||||
|
|
|
|||
78
src/ModelEditor.cpp
Normal file
78
src/ModelEditor.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include "ModelEditor.h"
|
||||
|
||||
uint8_t firstFreeId(const uint8_t *usedIds, size_t count, uint8_t lastId) {
|
||||
for(uint16_t id = 1; id <= lastId; id++) {
|
||||
bool used = false;
|
||||
for(size_t i = 0; i < count && !used; i++) used = usedIds[i] == id;
|
||||
if(!used) return static_cast<uint8_t>(id);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int8_t nextSortOrder(const int8_t *orders, size_t count) {
|
||||
int8_t order = -1;
|
||||
for(size_t i = 0; i < count; i++)
|
||||
if(orders[i] > order) order = orders[i];
|
||||
return order + 1;
|
||||
}
|
||||
uint32_t freeRemoteAddress(uint32_t candidate, const uint32_t *used, size_t count) {
|
||||
// Start over after every bump: the new address may be taken too.
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
if(used[i] == candidate) {
|
||||
candidate++;
|
||||
i = static_cast<size_t>(-1);
|
||||
}
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool addTo(T *list, size_t size, T item) {
|
||||
for(size_t i = 0; i < size; i++)
|
||||
if(list[i] == item) return true;
|
||||
for(size_t i = 0; i < size; i++) {
|
||||
if(list[i] == 0) {
|
||||
list[i] = item;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
template<typename T>
|
||||
static bool removeFrom(T *list, size_t size, T item) {
|
||||
bool removed = false;
|
||||
size_t j = 0;
|
||||
for(size_t i = 0; i < size; i++) {
|
||||
if(list[i] == 0) continue;
|
||||
if(list[i] == item) removed = true;
|
||||
else list[j++] = list[i];
|
||||
}
|
||||
while(j < size) list[j++] = 0;
|
||||
return removed;
|
||||
}
|
||||
void packList(uint8_t *list, size_t size) { removeFrom<uint8_t>(list, size, 0); }
|
||||
bool addToList(uint8_t *list, size_t size, uint8_t item) { return addTo<uint8_t>(list, size, item); }
|
||||
bool addToList(uint32_t *list, size_t size, uint32_t item) { return addTo<uint32_t>(list, size, item); }
|
||||
bool removeFromList(uint8_t *list, size_t size, uint8_t item) { return removeFrom<uint8_t>(list, size, item); }
|
||||
bool removeFromList(uint32_t *list, size_t size, uint32_t item) { return removeFrom<uint32_t>(list, size, item); }
|
||||
|
||||
int8_t gpioPinConflict(shade_types type, radio_proto proto, uint8_t upPin, uint8_t downPin, uint8_t myPin,
|
||||
const pin_users_t &radio, const pin_users_t ðernet, const pin_users_t &otherShades) {
|
||||
if(proto != radio_proto::GP_Relay && proto != radio_proto::GP_Remote) return 0;
|
||||
// A dry contact or a single button gate driven by a remote only uses the down
|
||||
// pin; a two way dry contact and a relay don't use My.
|
||||
if(type == shade_types::drycontact ||
|
||||
((type == shade_types::garage1 || type == shade_types::lgate1 || type == shade_types::cgate1 || type == shade_types::rgate1)
|
||||
&& proto == radio_proto::GP_Remote)) upPin = myPin = 255;
|
||||
else if(type == shade_types::drycontact2) myPin = 255;
|
||||
if(proto == radio_proto::GP_Relay) myPin = 255;
|
||||
const uint8_t pins[] = { upPin, downPin, myPin };
|
||||
auto clash = [&pins](const pin_users_t &users) {
|
||||
for(uint8_t pin : pins)
|
||||
if(pin != 255 && users.usesPin(pin)) return true;
|
||||
return false;
|
||||
};
|
||||
if(clash(radio)) return PIN_CONFLICT_RADIO;
|
||||
if(clash(ethernet)) return PIN_CONFLICT_ETHERNET;
|
||||
if(clash(otherShades)) return PIN_CONFLICT_SHADE;
|
||||
return 0;
|
||||
}
|
||||
77
src/ModelEditor.h
Normal file
77
src/ModelEditor.h
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#ifndef MODEL_EDITOR_H
|
||||
#define MODEL_EDITOR_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "SomfyFrame.h"
|
||||
#include "ShadeTypes.h"
|
||||
|
||||
// The rules ModelEditor applies when the shade model is edited: which id, sort
|
||||
// order and remote address a new room, shade or group gets, how ids go into
|
||||
// and out of a list, and which GPIO pins a shade may not use. Kept free of the
|
||||
// shade model so it can be unit tested on the PC (pio test -e native). The
|
||||
// edits themselves are applied on loop() (SomfyJson.cpp), since only loop()
|
||||
// changes the shade model (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
|
||||
// The edits, one per edit route.
|
||||
enum class model_edit_type_t : uint8_t {
|
||||
add_room, add_shade, add_group,
|
||||
save_room, save_shade, save_group,
|
||||
delete_room, delete_shade, delete_group,
|
||||
link_remote, unlink_remote,
|
||||
link_to_group, unlink_from_group,
|
||||
link_repeater, unlink_repeater,
|
||||
set_paired,
|
||||
room_order, shade_order, group_order
|
||||
};
|
||||
// What an edit did, sent to the clients as requestResult. id is the room,
|
||||
// shade or group it added or changed, 0 when none. code is the validation
|
||||
// error of a shade that wasn't saved, 0 otherwise.
|
||||
struct edit_result_t {
|
||||
bool ok = true;
|
||||
uint8_t id = 0;
|
||||
int8_t code = 0;
|
||||
const char *desc = "";
|
||||
static edit_result_t error(const char *desc, int8_t code = 0) {
|
||||
edit_result_t r;
|
||||
r.ok = false;
|
||||
r.desc = desc;
|
||||
r.code = code;
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
// The lowest id from 1 to lastId that isn't in usedIds, or 0 when every one
|
||||
// is taken.
|
||||
uint8_t firstFreeId(const uint8_t *usedIds, size_t count, uint8_t lastId);
|
||||
// The sort order that puts a new item after every existing one.
|
||||
int8_t nextSortOrder(const int8_t *orders, size_t count);
|
||||
// candidate, or the next address after it that isn't in used. No two shades or
|
||||
// groups may share a remote address.
|
||||
uint32_t freeRemoteAddress(uint32_t candidate, const uint32_t *used, size_t count);
|
||||
|
||||
// A list of ids or addresses with 0 for an empty slot, kept packed at the front.
|
||||
// Adding one that is already there changes nothing and succeeds; false when the
|
||||
// list is full.
|
||||
bool addToList(uint8_t *list, size_t size, uint8_t item);
|
||||
bool addToList(uint32_t *list, size_t size, uint32_t item);
|
||||
// Removes every copy and packs the rest to the front. False when it wasn't there.
|
||||
bool removeFromList(uint8_t *list, size_t size, uint8_t item);
|
||||
bool removeFromList(uint32_t *list, size_t size, uint32_t item);
|
||||
// Packs the items to the front, keeping their order.
|
||||
void packList(uint8_t *list, size_t size);
|
||||
|
||||
// Who else drives a GPIO pin.
|
||||
struct pin_users_t {
|
||||
bool (*uses)(uint8_t pin, void *ctx) = nullptr;
|
||||
void *ctx = nullptr;
|
||||
bool usesPin(uint8_t pin) const { return this->uses && this->uses(pin, this->ctx); }
|
||||
};
|
||||
#define PIN_CONFLICT_RADIO -10
|
||||
#define PIN_CONFLICT_ETHERNET -11
|
||||
#define PIN_CONFLICT_SHADE -12
|
||||
// Whether a shade of type driven over proto may use these pins (255 for none):
|
||||
// 0, or which of the radio, the Ethernet PHY or another shade already uses one.
|
||||
// Only the pins the shade type and protocol actually drive are checked.
|
||||
int8_t gpioPinConflict(shade_types type, radio_proto proto, uint8_t upPin, uint8_t downPin, uint8_t myPin,
|
||||
const pin_users_t &radio, const pin_users_t ðernet, const pin_users_t &otherShades);
|
||||
#endif
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "ModelSnapshot.h"
|
||||
#include "ModelEditor.h"
|
||||
|
||||
static const char *TAG = "Snapshot";
|
||||
|
||||
|
|
@ -118,6 +119,35 @@ void writeGroup(JsonFormatter &json, const group_snapshot_t &group, shade_lookup
|
|||
json.endArray();
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* What the next room, shade or group would get (ModelEditor's rules,
|
||||
* as SomfyShadeController applies them when one is added)
|
||||
********************************************************************/
|
||||
uint8_t nextRoomId(const model_snapshot_t &snap) {
|
||||
uint8_t ids[SOMFY_MAX_ROOMS];
|
||||
for(uint8_t i = 0; i < snap.roomCount; i++) ids[i] = snap.rooms[i].roomId;
|
||||
return firstFreeId(ids, snap.roomCount, SOMFY_MAX_ROOMS - 2);
|
||||
}
|
||||
uint8_t nextShadeId(const model_snapshot_t &snap) {
|
||||
uint8_t ids[SOMFY_MAX_SHADES];
|
||||
for(uint8_t i = 0; i < snap.shadeCount; i++) ids[i] = snap.shades[i].shadeId;
|
||||
const uint8_t id = firstFreeId(ids, snap.shadeCount, SOMFY_MAX_SHADES - 2);
|
||||
return id == 0 ? 255 : id;
|
||||
}
|
||||
uint8_t nextGroupId(const model_snapshot_t &snap) {
|
||||
uint8_t ids[SOMFY_MAX_GROUPS];
|
||||
for(uint8_t i = 0; i < snap.groupCount; i++) ids[i] = snap.groups[i].groupId;
|
||||
const uint8_t id = firstFreeId(ids, snap.groupCount, SOMFY_MAX_GROUPS - 2);
|
||||
return id == 0 ? 255 : id;
|
||||
}
|
||||
uint32_t nextRemoteAddress(const model_snapshot_t &snap, uint8_t id) {
|
||||
uint32_t used[SOMFY_MAX_SHADES + SOMFY_MAX_GROUPS];
|
||||
size_t count = 0;
|
||||
for(uint8_t i = 0; i < snap.shadeCount; i++) used[count++] = snap.shades[i].remoteAddress;
|
||||
for(uint8_t i = 0; i < snap.groupCount; i++) used[count++] = snap.groups[i].remoteAddress;
|
||||
return freeRemoteAddress(snap.startingAddress + id, used, count);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Documents
|
||||
********************************************************************/
|
||||
|
|
@ -136,6 +166,11 @@ enum : uint8_t {
|
|||
ST_TO_GROUPS, // ],"groups":[
|
||||
ST_TO_REPEATERS, // ],"repeaters":[
|
||||
ST_CLOSE_DOC, // ]}
|
||||
ST_NEXT_ROOM,
|
||||
ST_NEXT_SHADE,
|
||||
ST_NEXT_GROUP,
|
||||
ST_GROUP_OPTIONS, // the group, then ],"availShades":[
|
||||
ST_AVAIL_SHADES, // the shades not in that group
|
||||
ST_END
|
||||
};
|
||||
static const uint8_t CONTROLLER_STEPS[] = { ST_HEADER, ST_ROOMS, ST_TO_SHADES, ST_SHADES, ST_TO_GROUPS, ST_GROUPS, ST_TO_REPEATERS, ST_REPEATERS, ST_CLOSE_DOC, ST_END };
|
||||
|
|
@ -146,6 +181,11 @@ static const uint8_t GROUPS_STEPS[] = { ST_OPEN_ARRAY, ST_GROUPS, ST_CLOSE_ARRAY
|
|||
static const uint8_t ROOM_STEPS[] = { ST_ROOM, ST_END };
|
||||
static const uint8_t SHADE_STEPS[] = { ST_SHADE, ST_END };
|
||||
static const uint8_t GROUP_STEPS[] = { ST_GROUP, ST_END };
|
||||
static const uint8_t REPEATERS_STEPS[] = { ST_OPEN_ARRAY, ST_REPEATERS, ST_CLOSE_ARRAY, ST_END };
|
||||
static const uint8_t NEXT_ROOM_STEPS[] = { ST_NEXT_ROOM, ST_END };
|
||||
static const uint8_t NEXT_SHADE_STEPS[] = { ST_NEXT_SHADE, ST_END };
|
||||
static const uint8_t NEXT_GROUP_STEPS[] = { ST_NEXT_GROUP, ST_END };
|
||||
static const uint8_t GROUP_OPTIONS_STEPS[] = { ST_GROUP_OPTIONS, ST_AVAIL_SHADES, ST_CLOSE_DOC, ST_END };
|
||||
|
||||
static const uint8_t *stepsFor(snapshot_doc_t doc) {
|
||||
switch(doc) {
|
||||
|
|
@ -157,6 +197,11 @@ static const uint8_t *stepsFor(snapshot_doc_t doc) {
|
|||
case snapshot_doc_t::room: return ROOM_STEPS;
|
||||
case snapshot_doc_t::shade: return SHADE_STEPS;
|
||||
case snapshot_doc_t::group: return GROUP_STEPS;
|
||||
case snapshot_doc_t::repeaters: return REPEATERS_STEPS;
|
||||
case snapshot_doc_t::nextRoom: return NEXT_ROOM_STEPS;
|
||||
case snapshot_doc_t::nextShade: return NEXT_SHADE_STEPS;
|
||||
case snapshot_doc_t::nextGroup: return NEXT_GROUP_STEPS;
|
||||
case snapshot_doc_t::groupOptions: return GROUP_OPTIONS_STEPS;
|
||||
}
|
||||
return SHADES_STEPS;
|
||||
}
|
||||
|
|
@ -199,6 +244,11 @@ void SnapshotWriter::literal(const char *text) {
|
|||
this->unitLen = strlen(this->unit);
|
||||
this->nextStep();
|
||||
}
|
||||
// Writes text as the whole rest of the document.
|
||||
void SnapshotWriter::finish(const char *text) {
|
||||
this->literal(text);
|
||||
while(this->steps[this->step] != ST_END) this->step++;
|
||||
}
|
||||
void SnapshotWriter::resetFmt(size_t offset) {
|
||||
this->unit[offset] = '\0';
|
||||
this->fmt.setBuffer(this->unit + offset, this->unitSize - offset);
|
||||
|
|
@ -218,7 +268,7 @@ size_t SnapshotWriter::itemStart() {
|
|||
}
|
||||
// One piece of a group: its ref, then each linked shade, then the close.
|
||||
// inList is whether it is an item of a list rather than the whole document.
|
||||
void SnapshotWriter::groupUnit(const group_snapshot_t &group, bool inList) {
|
||||
void SnapshotWriter::groupUnit(const group_snapshot_t &group, bool inList, const char *close) {
|
||||
auto done = [this, inList]() {
|
||||
this->groupOpen = false;
|
||||
if(inList) this->idx++;
|
||||
|
|
@ -241,8 +291,8 @@ void SnapshotWriter::groupUnit(const group_snapshot_t &group, bool inList) {
|
|||
const shade_snapshot_t *shade = nullptr;
|
||||
while(this->lsIdx < group.linkedShadeCount && !(shade = this->snap.shade(group.linkedShades[this->lsIdx]))) this->lsIdx++;
|
||||
if(!shade) {
|
||||
strcpy(this->unit, "]}");
|
||||
this->unitLen = 2;
|
||||
strcpy(this->unit, close);
|
||||
this->unitLen = strlen(close);
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
|
@ -335,6 +385,52 @@ void SnapshotWriter::produceUnit() {
|
|||
this->groupUnit(*group, false);
|
||||
return;
|
||||
}
|
||||
case ST_NEXT_ROOM:
|
||||
case ST_NEXT_SHADE:
|
||||
case ST_NEXT_GROUP:
|
||||
this->resetFmt(0);
|
||||
this->fmt.beginObject();
|
||||
if(this->steps[this->step] == ST_NEXT_ROOM) this->fmt.addElem("roomId", nextRoomId(this->snap));
|
||||
else if(this->steps[this->step] == ST_NEXT_SHADE) {
|
||||
const uint8_t shadeId = nextShadeId(this->snap);
|
||||
this->fmt.addElem("shadeId", shadeId);
|
||||
this->fmt.addElem("remoteAddress", nextRemoteAddress(this->snap, shadeId));
|
||||
this->fmt.addElem("bitLength", this->snap.radioBitLength);
|
||||
this->fmt.addElem("stepSize", (uint8_t)100);
|
||||
this->fmt.addElem("proto", this->snap.radioProto);
|
||||
}
|
||||
else {
|
||||
const uint8_t groupId = nextGroupId(this->snap);
|
||||
this->fmt.addElem("groupId", groupId);
|
||||
this->fmt.addElem("remoteAddress", nextRemoteAddress(this->snap, groupId));
|
||||
this->fmt.addElem("bitLength", this->snap.radioBitLength);
|
||||
this->fmt.addElem("proto", this->snap.radioProto);
|
||||
}
|
||||
this->fmt.endObject();
|
||||
this->unitLen = this->fmt.length();
|
||||
this->nextStep();
|
||||
return;
|
||||
case ST_GROUP_OPTIONS: {
|
||||
const group_snapshot_t *group = this->snap.group(this->id);
|
||||
if(!group) { this->finish("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"); return; }
|
||||
this->groupUnit(*group, false, "],\"availShades\":[");
|
||||
return;
|
||||
}
|
||||
case ST_AVAIL_SHADES: {
|
||||
const group_snapshot_t *group = this->snap.group(this->id);
|
||||
if(!group || this->idx >= this->snap.shadeCount) { this->nextStep(); return; }
|
||||
const shade_snapshot_t &shade = this->snap.shades[this->idx++];
|
||||
for(uint8_t i = 0; i < group->linkedShadeCount; i++)
|
||||
if(group->linkedShades[i] == shade.shadeId) return;
|
||||
const size_t pos = this->itemStart();
|
||||
this->fmt.beginObject();
|
||||
writeShadeRef(this->fmt, shade);
|
||||
this->fmt.endObject();
|
||||
if(this->overflowed("shade", shade.shadeId)) return;
|
||||
this->unitLen = pos + this->fmt.length();
|
||||
this->firstItem = false;
|
||||
return;
|
||||
}
|
||||
case ST_OPEN_ARRAY: this->literal("["); return;
|
||||
case ST_CLOSE_ARRAY: this->literal("]"); return;
|
||||
case ST_TO_SHADES: this->literal("],\"shades\":["); return;
|
||||
|
|
@ -379,6 +475,7 @@ void SnapshotSlots::fill(uint8_t slot, void (*fill)(model_snapshot_t &snap)) {
|
|||
}
|
||||
model_snapshot_t &snap = *this->snaps[slot];
|
||||
snap.startingAddress = 0;
|
||||
snap.radioBitLength = snap.radioProto = 0;
|
||||
snap.roomCount = snap.shadeCount = snap.groupCount = snap.repeaterCount = 0;
|
||||
fill(snap);
|
||||
expected = FILLING;
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ struct group_snapshot_t {
|
|||
// Only the rooms, shades, groups and repeaters that exist, in model order.
|
||||
struct model_snapshot_t {
|
||||
uint32_t startingAddress;
|
||||
uint8_t radioBitLength; // what a new shade or group gets
|
||||
uint8_t radioProto;
|
||||
uint8_t roomCount;
|
||||
uint8_t shadeCount;
|
||||
uint8_t groupCount;
|
||||
|
|
@ -115,7 +117,12 @@ enum class snapshot_doc_t : uint8_t {
|
|||
groups, // /groups
|
||||
room, // /room?roomId=
|
||||
shade, // /shade?shadeId=
|
||||
group // /group?groupId=
|
||||
group, // /group?groupId=
|
||||
repeaters, // /repeaters
|
||||
nextRoom, // /getNextRoom: the id and defaults a new room would get
|
||||
nextShade, // /getNextShade
|
||||
nextGroup, // /getNextGroup
|
||||
groupOptions // /groupOptions?groupId=: the group and the shades it could link
|
||||
};
|
||||
// Writes the fields of /controller or /discovery that aren't in the snapshot
|
||||
// (device, radio and firmware details) into the open top-level object.
|
||||
|
|
@ -154,8 +161,15 @@ class SnapshotWriter {
|
|||
void resetFmt(size_t offset);
|
||||
bool overflowed(const char *what, uint8_t itemId);
|
||||
size_t itemStart();
|
||||
void groupUnit(const group_snapshot_t &group, bool comma);
|
||||
void groupUnit(const group_snapshot_t &group, bool inList, const char *close = "]}");
|
||||
void finish(const char *text);
|
||||
};
|
||||
// What the next room, shade or group added would get: its id (0 for a room and
|
||||
// 255 for a shade or group when there is no room for one) and remote address.
|
||||
uint8_t nextRoomId(const model_snapshot_t &snap);
|
||||
uint8_t nextShadeId(const model_snapshot_t &snap);
|
||||
uint8_t nextGroupId(const model_snapshot_t &snap);
|
||||
uint32_t nextRemoteAddress(const model_snapshot_t &snap, uint8_t id);
|
||||
|
||||
// The snapshots reads are served from. A read takes a slot on the web server
|
||||
// task and queues a fill; loop() fills it; the read streams it and lets it go.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "Somfy.h"
|
||||
#include "ConfigFile.h"
|
||||
#include "GitOTA.h"
|
||||
#include "ModelEditor.h"
|
||||
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
|
|
@ -117,17 +118,6 @@ void SomfyRoom::clear() {
|
|||
this->roomId = 0;
|
||||
strcpy(this->name, "");
|
||||
}
|
||||
void SomfyShadeController::compressRepeaters() {
|
||||
for(uint8_t i = 0, j = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
||||
if(this->repeaters[i] != 0) {
|
||||
if(i != j) {
|
||||
this->repeaters[j] = this->repeaters[i];
|
||||
this->repeaters[i] = 0;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool SomfyRoom::save() { somfy.commit(); return true; }
|
||||
void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
|
|
@ -158,89 +148,45 @@ void SomfyShadeController::processWaitingFrame() {
|
|||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||
if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame();
|
||||
}
|
||||
// A deleted shade, group or room leaves a gap, so a new one takes the lowest
|
||||
// free id. Ids stop two short of the maximum, as they always have.
|
||||
uint8_t SomfyShadeController::getNextShadeId() {
|
||||
// There is no shortcut for this since the deletion of
|
||||
// a shade in the middle makes all of this very difficult.
|
||||
for(uint8_t i = 1; i < SOMFY_MAX_SHADES - 1; i++) {
|
||||
bool id_exists = false;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_SHADES; j++) {
|
||||
SomfyShade *shade = &this->shades[j];
|
||||
if(shade->getShadeId() == i) {
|
||||
id_exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!id_exists) {
|
||||
ESP_LOGD(TAG, "Got next Shade Id:%d", i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 255;
|
||||
uint8_t ids[SOMFY_MAX_SHADES];
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) ids[i] = this->shades[i].getShadeId();
|
||||
const uint8_t id = firstFreeId(ids, SOMFY_MAX_SHADES, SOMFY_MAX_SHADES - 2);
|
||||
return id == 0 ? 255 : id;
|
||||
}
|
||||
int8_t SomfyShadeController::getMaxShadeOrder() {
|
||||
int8_t order = -1;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &this->shades[i];
|
||||
if(shade->getShadeId() == 255) continue;
|
||||
if(order < shade->sortOrder) order = shade->sortOrder;
|
||||
}
|
||||
return order;
|
||||
int8_t orders[SOMFY_MAX_SHADES];
|
||||
size_t count = 0;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||
if(this->shades[i].getShadeId() != 255) orders[count++] = this->shades[i].sortOrder;
|
||||
return nextSortOrder(orders, count) - 1;
|
||||
}
|
||||
int8_t SomfyShadeController::getMaxGroupOrder() {
|
||||
int8_t order = -1;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
SomfyGroup *group = &this->groups[i];
|
||||
if(group->getGroupId() == 255) continue;
|
||||
if(order < group->sortOrder) order = group->sortOrder;
|
||||
}
|
||||
return order;
|
||||
int8_t orders[SOMFY_MAX_GROUPS];
|
||||
size_t count = 0;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++)
|
||||
if(this->groups[i].getGroupId() != 255) orders[count++] = this->groups[i].sortOrder;
|
||||
return nextSortOrder(orders, count) - 1;
|
||||
}
|
||||
uint8_t SomfyShadeController::getNextGroupId() {
|
||||
// There is no shortcut for this since the deletion of
|
||||
// a group in the middle makes all of this very difficult.
|
||||
for(uint8_t i = 1; i < SOMFY_MAX_GROUPS - 1; i++) {
|
||||
bool id_exists = false;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPS; j++) {
|
||||
SomfyGroup *group = &this->groups[j];
|
||||
if(group->getGroupId() == i) {
|
||||
id_exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!id_exists) {
|
||||
ESP_LOGD(TAG, "Got next Group Id:%d", i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 255;
|
||||
uint8_t ids[SOMFY_MAX_GROUPS];
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) ids[i] = this->groups[i].getGroupId();
|
||||
const uint8_t id = firstFreeId(ids, SOMFY_MAX_GROUPS, SOMFY_MAX_GROUPS - 2);
|
||||
return id == 0 ? 255 : id;
|
||||
}
|
||||
uint8_t SomfyShadeController::getNextRoomId() {
|
||||
// There is no shortcut for this since the deletion of
|
||||
// a room in the middle makes all of this very difficult.
|
||||
for(uint8_t i = 1; i < SOMFY_MAX_ROOMS - 1; i++) {
|
||||
bool id_exists = false;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_ROOMS; j++) {
|
||||
SomfyRoom *room = &this->rooms[j];
|
||||
if(room->roomId == i) {
|
||||
id_exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!id_exists) {
|
||||
ESP_LOGD(TAG, "Got next room Id:%d", i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
uint8_t ids[SOMFY_MAX_ROOMS];
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) ids[i] = this->rooms[i].roomId;
|
||||
return firstFreeId(ids, SOMFY_MAX_ROOMS, SOMFY_MAX_ROOMS - 2);
|
||||
}
|
||||
int8_t SomfyShadeController::getMaxRoomOrder() {
|
||||
int8_t order = -1;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
||||
SomfyRoom *room = &this->rooms[i];
|
||||
if(room->roomId == 0) continue;
|
||||
if(order < room->sortOrder) order = room->sortOrder;
|
||||
}
|
||||
return order;
|
||||
int8_t orders[SOMFY_MAX_ROOMS];
|
||||
size_t count = 0;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++)
|
||||
if(this->rooms[i].roomId != 0) orders[count++] = this->rooms[i].sortOrder;
|
||||
return nextSortOrder(orders, count) - 1;
|
||||
}
|
||||
uint8_t SomfyShadeController::repeaterCount() {
|
||||
uint8_t count = 0;
|
||||
|
|
@ -270,31 +216,6 @@ uint8_t SomfyShadeController::groupCount() {
|
|||
}
|
||||
return count;
|
||||
}
|
||||
uint32_t SomfyShadeController::getNextRemoteAddress(uint8_t id) {
|
||||
uint32_t address = this->startingAddress + id;
|
||||
uint8_t i = 0;
|
||||
// The assumption here is that the max number of groups will
|
||||
// always be less than or equal to the max number of shades.
|
||||
while(i < SOMFY_MAX_SHADES) {
|
||||
if((i < SOMFY_MAX_SHADES && this->shades[i].getShadeId() != 255 && this->shades[i].getRemoteAddress() == address) ||
|
||||
(i < SOMFY_MAX_GROUPS && this->groups[i].getGroupId() != 255 && this->groups[i].getRemoteAddress() == address)) {
|
||||
address++;
|
||||
i = 0; // Start over we cannot share addresses.
|
||||
}
|
||||
else i++;
|
||||
}
|
||||
i = 0;
|
||||
return address;
|
||||
}
|
||||
SomfyShade *SomfyShadeController::addShade(JsonObject &obj) {
|
||||
SomfyShade *shade = this->addShade();
|
||||
if(shade) {
|
||||
shade->fromJSON(obj);
|
||||
shade->save();
|
||||
shade->emitState("shadeAdded");
|
||||
}
|
||||
return shade;
|
||||
}
|
||||
SomfyShade *SomfyShadeController::addShade() {
|
||||
Preferences pref;
|
||||
uint8_t shadeId = this->getNextShadeId();
|
||||
|
|
@ -313,37 +234,15 @@ SomfyShade *SomfyShadeController::addShade() {
|
|||
return shade;
|
||||
}
|
||||
bool SomfyShadeController::unlinkRepeater(uint32_t address) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
||||
if(this->repeaters[i] == address) this->repeaters[i] = 0;
|
||||
}
|
||||
this->compressRepeaters();
|
||||
removeFromList(this->repeaters, SOMFY_MAX_REPEATERS, address);
|
||||
this->isDirty = true;
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeController::linkRepeater(uint32_t address) {
|
||||
bool bSet = false;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
||||
if(!bSet && this->repeaters[i] == address) bSet = true;
|
||||
else if(bSet && this->repeaters[i] == address) this->repeaters[i] = 0;
|
||||
}
|
||||
if(!bSet) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
||||
if(this->repeaters[i] == 0) {
|
||||
this->repeaters[i] = address;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
SomfyRoom *SomfyShadeController::addRoom(JsonObject &obj) {
|
||||
SomfyRoom *room = this->addRoom();
|
||||
if(room) {
|
||||
room->fromJSON(obj);
|
||||
room->save();
|
||||
room->emitState("roomAdded");
|
||||
}
|
||||
return room;
|
||||
// False when every repeater slot is taken.
|
||||
bool SomfyShadeController::linkRepeater(uint32_t address) {
|
||||
if(!addToList(this->repeaters, SOMFY_MAX_REPEATERS, address)) return false;
|
||||
this->isDirty = true;
|
||||
return true;
|
||||
}
|
||||
SomfyRoom *SomfyShadeController::addRoom() {
|
||||
uint8_t roomId = this->getNextRoomId();
|
||||
|
|
@ -357,15 +256,6 @@ SomfyRoom *SomfyShadeController::addRoom() {
|
|||
}
|
||||
return room;
|
||||
}
|
||||
SomfyGroup *SomfyShadeController::addGroup(JsonObject &obj) {
|
||||
SomfyGroup *group = this->addGroup();
|
||||
if(group) {
|
||||
group->fromJSON(obj);
|
||||
group->save();
|
||||
group->emitState("groupAdded");
|
||||
}
|
||||
return group;
|
||||
}
|
||||
SomfyGroup *SomfyShadeController::addGroup() {
|
||||
uint8_t groupId = this->getNextGroupId();
|
||||
// So the next shade id will be the first one we run into with an id of 255 so
|
||||
|
|
|
|||
|
|
@ -274,22 +274,17 @@ class SomfyShadeController {
|
|||
int8_t getMaxRoomOrder();
|
||||
int8_t getMaxShadeOrder();
|
||||
int8_t getMaxGroupOrder();
|
||||
uint32_t getNextRemoteAddress(uint8_t shadeId);
|
||||
SomfyShadeController();
|
||||
Transceiver transceiver;
|
||||
SomfyRoom *addRoom();
|
||||
SomfyRoom *addRoom(JsonObject &obj);
|
||||
SomfyShade *addShade();
|
||||
SomfyShade *addShade(JsonObject &obj);
|
||||
SomfyGroup *addGroup();
|
||||
SomfyGroup *addGroup(JsonObject &obj);
|
||||
bool deleteRoom(uint8_t roomId);
|
||||
bool deleteShade(uint8_t shadeId);
|
||||
bool deleteGroup(uint8_t groupId);
|
||||
bool begin();
|
||||
void loop();
|
||||
void end();
|
||||
void compressRepeaters();
|
||||
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
|
||||
SomfyRoom rooms[SOMFY_MAX_ROOMS];
|
||||
SomfyShade shades[SOMFY_MAX_SHADES];
|
||||
|
|
|
|||
29
src/SomfyEditor.h
Normal file
29
src/SomfyEditor.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef SOMFY_EDITOR_H
|
||||
#define SOMFY_EDITOR_H
|
||||
#include <ArduinoJson.h>
|
||||
#include "ModelEditor.h"
|
||||
|
||||
// ModelEditor on the device: an edit of the shade model handed from the web
|
||||
// server task to loop(), which applies it with the ModelEditor rules and tells
|
||||
// the clients how it went.
|
||||
|
||||
// One edit on its way to loop(): the edit route and a copy of its request.
|
||||
struct model_edit_t {
|
||||
model_edit_type_t type = model_edit_type_t::add_room;
|
||||
uint32_t requestId = 0;
|
||||
JsonDocument doc;
|
||||
};
|
||||
|
||||
// One edit at a time: false when one is already waiting or running. Any task.
|
||||
bool beginModelEdit();
|
||||
// The id the HTTP reply and the requestResult event carry. Any task.
|
||||
uint32_t nextEditRequestId();
|
||||
// Gives up an edit that was begun but never queued. Any task.
|
||||
void cancelModelEdit(model_edit_t *edit);
|
||||
// Applies the edit, saves it, sends requestResult and frees it. loop() only.
|
||||
void runModelEdit(model_edit_t *edit);
|
||||
// Applies the edit to the shade model and saves it. loop() only.
|
||||
edit_result_t applyModelEdit(model_edit_t &edit);
|
||||
// Tells every client how the edit went (SomfyEvents.cpp).
|
||||
void emitRequestResult(uint32_t requestId, const edit_result_t &result);
|
||||
#endif
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "esp_log.h"
|
||||
#include "Somfy.h"
|
||||
#include "ModelEditor.h"
|
||||
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
|
|
@ -13,60 +14,12 @@ void SomfyGroup::clear() {
|
|||
this->name[0] = 0x00;
|
||||
memset(&this->linkedShades, 0x00, sizeof(this->linkedShades));
|
||||
}
|
||||
bool SomfyGroup::linkShade(uint8_t shadeId) {
|
||||
// Check to see if the shade is already linked. If it is just return true
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == shadeId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == 0) {
|
||||
this->linkedShades[i] = shadeId;
|
||||
somfy.commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool SomfyGroup::unlinkShade(uint8_t shadeId) {
|
||||
bool removed = false;
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == shadeId) {
|
||||
this->linkedShades[i] = 0;
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
// Compress the linked shade ids so we can stop looking on the first 0
|
||||
if(removed) {
|
||||
this->compressLinkedShadeIds();
|
||||
somfy.commit();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
void SomfyGroup::compressLinkedShadeIds() {
|
||||
// [1,0,4,3,0,0,0] i:0,j:0
|
||||
// [1,0,4,3,0,0,0] i:1,j:1
|
||||
// [1,4,0,3,0,0,0] i:2,j:1
|
||||
// [1,4,3,0,0,0,0] i:3,j:2
|
||||
// [1,4,3,0,0,0,0] i:4,j:2
|
||||
|
||||
// [1,2,0,0,3,0,0] i:0,j:0
|
||||
// [1,2,0,0,3,0,0] i:1,j:1
|
||||
// [1,2,0,0,3,0,0] i:2,j:2
|
||||
// [1,2,0,0,3,0,0] i:3,j:2
|
||||
// [1,2,3,0,0,0,0] i:4,j:2
|
||||
// [1,2,3,0,0,0,0] i:5,j:3
|
||||
for(uint8_t i = 0, j = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] != 0) {
|
||||
if(i != j) {
|
||||
this->linkedShades[j] = this->linkedShades[i];
|
||||
this->linkedShades[i] = 0;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// False when the group is full. The caller saves the change.
|
||||
bool SomfyGroup::linkShade(uint8_t shadeId) { return addToList(this->linkedShades, SOMFY_MAX_GROUPED_SHADES, shadeId); }
|
||||
// False when the shade wasn't in the group. The caller saves the change.
|
||||
bool SomfyGroup::unlinkShade(uint8_t shadeId) { return removeFromList(this->linkedShades, SOMFY_MAX_GROUPED_SHADES, shadeId); }
|
||||
// Linked shade ids are kept packed so a search can stop at the first 0.
|
||||
void SomfyGroup::compressLinkedShadeIds() { packList(this->linkedShades, SOMFY_MAX_GROUPED_SHADES); }
|
||||
bool SomfyGroup::hasShadeId(uint8_t shadeId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||
if(this->linkedShades[i] == 0) break;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "esp_log.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "Somfy.h"
|
||||
#include <atomic>
|
||||
#include "ModelEditor.h"
|
||||
#include "SomfyEditor.h"
|
||||
|
||||
static const char *TAG = "Somfy";
|
||||
|
||||
|
|
@ -36,50 +39,32 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) {
|
|||
type = shade_types::drycontact;
|
||||
}
|
||||
else {
|
||||
this->shadeType = static_cast<shade_types>(obj["shadeType"].as<uint8_t>());
|
||||
type = static_cast<shade_types>(obj["shadeType"].as<uint8_t>());
|
||||
}
|
||||
}
|
||||
if(!obj["proto"].isNull()) {
|
||||
radio_proto proto = this->proto;
|
||||
if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) {
|
||||
// Check to see if we are using the up and or down
|
||||
// GPIOs anywhere else.
|
||||
uint8_t upPin = !obj["gpioUp"].isNull() ? obj["gpioUp"].as<uint8_t>() : this->gpioUp;
|
||||
uint8_t downPin = !obj["gpioDown"].isNull() ? obj["gpioDown"].as<uint8_t>() : this->gpioDown;
|
||||
uint8_t myPin = !obj["gpioMy"].isNull() ? obj["gpioMy"].as<uint8_t>() : this->gpioMy;
|
||||
if(type == shade_types::drycontact ||
|
||||
((type == shade_types::garage1 || type == shade_types::lgate1 || type == shade_types::cgate1 || type == shade_types::rgate1)
|
||||
&& proto == radio_proto::GP_Remote)) upPin = myPin = 255;
|
||||
else if(type == shade_types::drycontact2) myPin = 255;
|
||||
if(proto == radio_proto::GP_Relay) myPin = 255;
|
||||
if(somfy.transceiver.config.enabled) {
|
||||
if((upPin != 255 && somfy.transceiver.usesPin(upPin)) ||
|
||||
(downPin != 255 && somfy.transceiver.usesPin(downPin)) ||
|
||||
(myPin != 255 && somfy.transceiver.usesPin(myPin)))
|
||||
ret = -10;
|
||||
}
|
||||
// The pins and protocol the shade is about to have.
|
||||
const radio_proto proto = !obj["proto"].isNull() ? static_cast<radio_proto>(obj["proto"].as<uint8_t>()) : this->proto;
|
||||
const uint8_t upPin = !obj["gpioUp"].isNull() ? obj["gpioUp"].as<uint8_t>() : this->gpioUp;
|
||||
const uint8_t downPin = !obj["gpioDown"].isNull() ? obj["gpioDown"].as<uint8_t>() : this->gpioDown;
|
||||
const uint8_t myPin = !obj["gpioMy"].isNull() ? obj["gpioMy"].as<uint8_t>() : this->gpioMy;
|
||||
pin_users_t radio, ethernet, others;
|
||||
if(somfy.transceiver.config.enabled)
|
||||
radio.uses = [](uint8_t pin, void *) { return somfy.transceiver.usesPin(pin); };
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) {
|
||||
if((upPin != 255 && settings.Ethernet.usesPin(upPin)) ||
|
||||
(downPin != 255 && somfy.transceiver.usesPin(downPin)) ||
|
||||
(myPin != 255 && somfy.transceiver.usesPin(myPin)))
|
||||
ret = -11;
|
||||
}
|
||||
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref)
|
||||
ethernet.uses = [](uint8_t pin, void *) { return settings.Ethernet.usesPin(pin); };
|
||||
#endif
|
||||
if(ret == 0) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &somfy.shades[i];
|
||||
if(shade->getShadeId() == this->getShadeId() || shade->getShadeId() == 255) continue;
|
||||
if((upPin != 255 && shade->usesPin(upPin)) ||
|
||||
(downPin != 255 && shade->usesPin(downPin)) ||
|
||||
(myPin != 255 && shade->usesPin(myPin))){
|
||||
ret = -12;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
others.ctx = this;
|
||||
others.uses = [](uint8_t pin, void *ctx) {
|
||||
const uint8_t self = static_cast<SomfyShade *>(ctx)->getShadeId();
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade &shade = somfy.shades[i];
|
||||
if(shade.getShadeId() == self || shade.getShadeId() == 255) continue;
|
||||
if(shade.usesPin(pin)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
ret = gpioPinConflict(type, proto, upPin, downPin, myPin, radio, ethernet, others);
|
||||
return ret;
|
||||
}
|
||||
int8_t SomfyShade::fromJSON(JsonObject &obj) {
|
||||
|
|
@ -233,3 +218,178 @@ void transceiver_config_t::fromJSON(JsonObject& obj) {
|
|||
if(!obj["noiseDetection"].isNull()) this->noiseDetection = obj["noiseDetection"];
|
||||
ESP_LOGD(TAG, "SCK:%u MISO:%u MOSI:%u CSN:%u RX:%u TX:%u", this->SCKPin, this->MISOPin, this->MOSIPin, this->CSNPin, this->RXPin, this->TXPin);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* ModelEditor on the device (SomfyEditor.h). Here rather than in a file of
|
||||
* its own because every file that uses ArduinoJson carries its own copy of the
|
||||
* library's helpers (docs/SOMFY_SPLIT_PLAN.md).
|
||||
********************************************************************/
|
||||
static std::atomic<bool> editBusy(false);
|
||||
static std::atomic<uint32_t> lastRequestId(0);
|
||||
|
||||
bool beginModelEdit() { return !editBusy.exchange(true); }
|
||||
uint32_t nextEditRequestId() { return ++lastRequestId; }
|
||||
void cancelModelEdit(model_edit_t *edit) {
|
||||
delete edit;
|
||||
editBusy.store(false);
|
||||
}
|
||||
void runModelEdit(model_edit_t *edit) {
|
||||
const edit_result_t result = applyModelEdit(*edit);
|
||||
if(!result.ok) ESP_LOGW(TAG, "Edit %u refused: %s", static_cast<uint8_t>(edit->type), result.desc);
|
||||
emitRequestResult(edit->requestId, result);
|
||||
cancelModelEdit(edit);
|
||||
}
|
||||
|
||||
static edit_result_t done(uint8_t id = 0) {
|
||||
edit_result_t r;
|
||||
r.id = id;
|
||||
return r;
|
||||
}
|
||||
// Gives each id in the request the next sort order, in the order given.
|
||||
template<typename Find>
|
||||
static edit_result_t applyOrder(JsonDocument &doc, Find find) {
|
||||
if(!doc.is<JsonArray>()) return edit_result_t::error("JSON parse error");
|
||||
int8_t order = 0;
|
||||
for(JsonVariant v : doc.as<JsonArray>()) {
|
||||
auto *item = find(v.as<uint8_t>());
|
||||
if(item) item->sortOrder = order++;
|
||||
}
|
||||
somfy.commit();
|
||||
return done();
|
||||
}
|
||||
edit_result_t applyModelEdit(model_edit_t &edit) {
|
||||
JsonObject obj = edit.doc.as<JsonObject>();
|
||||
switch(edit.type) {
|
||||
case model_edit_type_t::add_room: {
|
||||
SomfyRoom *room = somfy.addRoom();
|
||||
if(!room) return edit_result_t::error("Maximum number of rooms exceeded.");
|
||||
room->fromJSON(obj);
|
||||
room->save();
|
||||
room->emitState("roomAdded");
|
||||
return done(room->roomId);
|
||||
}
|
||||
case model_edit_type_t::add_shade: {
|
||||
SomfyShade *shade = somfy.addShade();
|
||||
if(!shade) return edit_result_t::error("Maximum number of shades exceeded.");
|
||||
const int8_t err = shade->fromJSON(obj);
|
||||
if(err != 0) {
|
||||
// Not added after all: free the slot it took.
|
||||
shade->clear();
|
||||
return edit_result_t::error("Data Error.", err);
|
||||
}
|
||||
shade->save();
|
||||
shade->emitState("shadeAdded");
|
||||
return done(shade->getShadeId());
|
||||
}
|
||||
case model_edit_type_t::add_group: {
|
||||
SomfyGroup *group = somfy.addGroup();
|
||||
if(!group) return edit_result_t::error("Maximum number of groups exceeded.");
|
||||
group->fromJSON(obj);
|
||||
group->save();
|
||||
group->emitState("groupAdded");
|
||||
return done(group->getGroupId());
|
||||
}
|
||||
case model_edit_type_t::save_room: {
|
||||
if(obj["roomId"].isNull()) return edit_result_t::error("No room id was supplied.");
|
||||
SomfyRoom *room = somfy.getRoomById(obj["roomId"]);
|
||||
if(!room) return edit_result_t::error("Room Id not found.");
|
||||
room->fromJSON(obj);
|
||||
room->save();
|
||||
room->emitState();
|
||||
return done(room->roomId);
|
||||
}
|
||||
case model_edit_type_t::save_shade: {
|
||||
if(obj["shadeId"].isNull()) return edit_result_t::error("No shade id was supplied.");
|
||||
SomfyShade *shade = somfy.getShadeById(obj["shadeId"]);
|
||||
if(!shade) return edit_result_t::error("Shade Id not found.");
|
||||
const int8_t err = shade->fromJSON(obj);
|
||||
if(err != 0) return edit_result_t::error("Data Error.", err);
|
||||
shade->save();
|
||||
shade->emitState();
|
||||
return done(shade->getShadeId());
|
||||
}
|
||||
case model_edit_type_t::save_group: {
|
||||
if(obj["groupId"].isNull()) return edit_result_t::error("No group id was supplied.");
|
||||
SomfyGroup *group = somfy.getGroupById(obj["groupId"]);
|
||||
if(!group) return edit_result_t::error("Group Id not found.");
|
||||
group->fromJSON(obj);
|
||||
group->save();
|
||||
group->emitState();
|
||||
return done(group->getGroupId());
|
||||
}
|
||||
case model_edit_type_t::delete_room: {
|
||||
if(obj["roomId"].isNull()) return edit_result_t::error("No room id was supplied.");
|
||||
const uint8_t roomId = obj["roomId"];
|
||||
if(!somfy.getRoomById(roomId)) return edit_result_t::error("Room with the specified id not found.");
|
||||
somfy.deleteRoom(roomId);
|
||||
return done(roomId);
|
||||
}
|
||||
case model_edit_type_t::delete_shade: {
|
||||
if(obj["shadeId"].isNull()) return edit_result_t::error("No shade id was supplied.");
|
||||
const uint8_t shadeId = obj["shadeId"];
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) return edit_result_t::error("Shade with the specified id not found.");
|
||||
if(shade->isInGroup()) return edit_result_t::error("This shade is a member of a group and cannot be deleted.");
|
||||
somfy.deleteShade(shadeId);
|
||||
return done(shadeId);
|
||||
}
|
||||
case model_edit_type_t::delete_group: {
|
||||
if(obj["groupId"].isNull()) return edit_result_t::error("No group id was supplied.");
|
||||
const uint8_t groupId = obj["groupId"];
|
||||
if(!somfy.getGroupById(groupId)) return edit_result_t::error("Group with the specified id not found.");
|
||||
somfy.deleteGroup(groupId);
|
||||
return done(groupId);
|
||||
}
|
||||
case model_edit_type_t::link_remote:
|
||||
case model_edit_type_t::unlink_remote: {
|
||||
if(obj["shadeId"].isNull()) return edit_result_t::error("No shade id was supplied.");
|
||||
SomfyShade *shade = somfy.getShadeById(obj["shadeId"]);
|
||||
if(!shade) return edit_result_t::error("Shade Id not found.");
|
||||
if(obj["remoteAddress"].isNull()) return edit_result_t::error("Remote address not provided.");
|
||||
const uint32_t address = obj["remoteAddress"];
|
||||
if(edit.type == model_edit_type_t::unlink_remote) shade->unlinkRemote(address);
|
||||
else if(!shade->linkRemote(address, obj["rollingCode"] | (uint16_t)0))
|
||||
return edit_result_t::error("This shade already has the most linked remotes it can.");
|
||||
return done(shade->getShadeId());
|
||||
}
|
||||
case model_edit_type_t::link_to_group:
|
||||
case model_edit_type_t::unlink_from_group: {
|
||||
const uint8_t groupId = obj["groupId"] | (uint8_t)0;
|
||||
const uint8_t shadeId = obj["shadeId"] | (uint8_t)0;
|
||||
if(groupId == 0) return edit_result_t::error("Group id not provided.");
|
||||
if(shadeId == 0) return edit_result_t::error("Shade id not provided.");
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) return edit_result_t::error("Group id not found.");
|
||||
if(!somfy.getShadeById(shadeId)) return edit_result_t::error("Shade id not found.");
|
||||
if(edit.type == model_edit_type_t::unlink_from_group) group->unlinkShade(shadeId);
|
||||
else if(!group->linkShade(shadeId)) return edit_result_t::error("This group already has the most shades it can.");
|
||||
somfy.commit();
|
||||
group->emitState();
|
||||
return done(groupId);
|
||||
}
|
||||
case model_edit_type_t::link_repeater:
|
||||
case model_edit_type_t::unlink_repeater: {
|
||||
const uint32_t address = !obj["address"].isNull() ? obj["address"].as<uint32_t>() : (obj["remoteAddress"] | (uint32_t)0);
|
||||
if(address == 0) return edit_result_t::error("No repeater address was supplied.");
|
||||
if(edit.type == model_edit_type_t::unlink_repeater) somfy.unlinkRepeater(address);
|
||||
else if(!somfy.linkRepeater(address)) return edit_result_t::error("Maximum number of repeaters exceeded.");
|
||||
somfy.commit();
|
||||
return done();
|
||||
}
|
||||
case model_edit_type_t::set_paired: {
|
||||
SomfyShade *shade = obj["shadeId"].isNull() ? nullptr : somfy.getShadeById(obj["shadeId"]);
|
||||
if(!shade) return edit_result_t::error("Shade not found to pair");
|
||||
shade->paired = obj["paired"] | false;
|
||||
shade->save();
|
||||
shade->emitState();
|
||||
return done(shade->getShadeId());
|
||||
}
|
||||
case model_edit_type_t::room_order:
|
||||
return applyOrder(edit.doc, [](uint8_t id) { return id != 0 ? somfy.getRoomById(id) : nullptr; });
|
||||
case model_edit_type_t::shade_order:
|
||||
return applyOrder(edit.doc, [](uint8_t id) { return id != 255 ? somfy.getShadeById(id) : nullptr; });
|
||||
case model_edit_type_t::group_order:
|
||||
return applyOrder(edit.doc, [](uint8_t id) { return id != 255 ? somfy.getGroupById(id) : nullptr; });
|
||||
}
|
||||
return edit_result_t::error("Unknown edit.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ void snapshotGroup(SomfyGroup &group, group_snapshot_t &out) {
|
|||
}
|
||||
void fillSnapshot(model_snapshot_t &snap) {
|
||||
snap.startingAddress = somfy.startingAddress;
|
||||
snap.radioBitLength = somfy.transceiver.config.type;
|
||||
snap.radioProto = static_cast<uint8_t>(somfy.transceiver.config.proto);
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++)
|
||||
if(somfy.rooms[i].roomId != 0) snapshotRoom(somfy.rooms[i], snap.rooms[snap.roomCount++]);
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include "LoopQueue.h"
|
||||
#include "ModelSnapshot.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Web.h"
|
||||
#include "MQTT.h"
|
||||
#include "GitOTA.h"
|
||||
|
|
@ -208,11 +209,6 @@ static bool asyncHasParam(AsyncWebServerRequest *request, const char *name) {
|
|||
// replies) copy one item into the ModelSnapshot types and write it the way the
|
||||
// read routes do. The read routes themselves write from a ModelSnapshot that
|
||||
// loop() fills (sendSnapshot).
|
||||
static void serializeRoom(SomfyRoom *room, JsonFormatter &json) {
|
||||
room_snapshot_t s;
|
||||
snapshotRoom(*room, s);
|
||||
writeRoom(json, s);
|
||||
}
|
||||
static void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) {
|
||||
shade_snapshot_t s;
|
||||
snapshotShade(*shade, s);
|
||||
|
|
@ -240,11 +236,6 @@ static void serializeGroup(SomfyGroup *group, JsonFormatter &json) {
|
|||
shade_snapshot_t linked;
|
||||
writeGroup(json, s, liveShade, &linked);
|
||||
}
|
||||
static void serializeRepeaters(JsonFormatter &json) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
||||
if(somfy.repeaters[i] != 0) json.addElem((uint32_t)somfy.repeaters[i]);
|
||||
}
|
||||
}
|
||||
static void serializeTransceiverConfig(JsonFormatter &json) {
|
||||
auto &cfg = somfy.transceiver.config;
|
||||
json.addElem("type", cfg.type);
|
||||
|
|
@ -385,6 +376,58 @@ static void sendSnapshot(AsyncWebServerRequest *request, snapshot_doc_t doc, uin
|
|||
request->send(response);
|
||||
}
|
||||
|
||||
// Hands an edit of the shade model to loop() (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
// The reply only says it was accepted; the result reaches every client as a
|
||||
// requestResult event carrying the same requestId once loop() has applied it.
|
||||
static void queueModelEdit(AsyncWebServerRequest *request, model_edit_type_t type, JsonVariantConst body) {
|
||||
if(!beginModelEdit()) {
|
||||
request->send(503, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Busy, try the change again\"}"));
|
||||
return;
|
||||
}
|
||||
model_edit_t *edit = new (std::nothrow) model_edit_t();
|
||||
if(!edit) {
|
||||
cancelModelEdit(nullptr);
|
||||
sendQueueFull(request);
|
||||
return;
|
||||
}
|
||||
edit->type = type;
|
||||
edit->requestId = nextEditRequestId();
|
||||
edit->doc.set(body);
|
||||
const uint32_t requestId = edit->requestId;
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::model_edit;
|
||||
item.edit = edit;
|
||||
if(edit->doc.overflowed() || !loopQueue.submit(item)) {
|
||||
cancelModelEdit(edit);
|
||||
sendQueueFull(request);
|
||||
return;
|
||||
}
|
||||
char reply[64];
|
||||
snprintf(reply, sizeof(reply), "{\"status\":\"accepted\",\"requestId\":%lu}", (unsigned long)requestId);
|
||||
request->send(200, _encoding_json, reply);
|
||||
}
|
||||
// The JSON (PUT/POST) form of an edit route. Bodies over 4 KB are refused with
|
||||
// 413 before they reach the heap.
|
||||
static AsyncCallbackJsonWebHandler *editHandler(const char *uri, model_edit_type_t type) {
|
||||
auto *handler = new AsyncCallbackJsonWebHandler(uri, [type](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No object supplied.\"}")); return; }
|
||||
queueModelEdit(request, type, json);
|
||||
});
|
||||
handler->setMaxContentLength(4096);
|
||||
return handler;
|
||||
}
|
||||
// The GET form of an edit route: its query parameters become the request.
|
||||
static void queueModelEditFromParams(AsyncWebServerRequest *request, model_edit_type_t type, std::initializer_list<const char *> names) {
|
||||
JsonDocument body;
|
||||
for(const char *name : names) {
|
||||
if(!asyncHasParam(request, name)) continue;
|
||||
const String value = asyncParam(request, name);
|
||||
if(strcmp(name, "paired") == 0) body[name] = toBoolean(value.c_str(), false);
|
||||
else body[name] = strtoul(value.c_str(), nullptr, 10);
|
||||
}
|
||||
queueModelEdit(request, type, body.as<JsonVariantConst>());
|
||||
}
|
||||
|
||||
// -- Async handler implementations --
|
||||
void Web::handleDiscovery(AsyncWebServerRequest *request) {
|
||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) {
|
||||
|
|
@ -1057,220 +1100,29 @@ void Web::begin() {
|
|||
asyncServer.on("/shade", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleShade(request); });
|
||||
asyncServer.on("/group", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGroup(request); });
|
||||
|
||||
asyncServer.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.addElem("roomId", somfy.getNextRoomId());
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
asyncServer.on("/getNextShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = somfy.getNextShadeId();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.addElem("shadeId", shadeId);
|
||||
resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(shadeId));
|
||||
resp.addElem("bitLength", somfy.transceiver.config.type);
|
||||
resp.addElem("stepSize", (uint8_t)100);
|
||||
resp.addElem("proto", static_cast<uint8_t>(somfy.transceiver.config.proto));
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
asyncServer.on("/getNextGroup", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t groupId = somfy.getNextGroupId();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
resp.addElem("groupId", groupId);
|
||||
resp.addElem("remoteAddress", (uint32_t)somfy.getNextRemoteAddress(groupId));
|
||||
resp.addElem("bitLength", somfy.transceiver.config.type);
|
||||
resp.addElem("proto", static_cast<uint8_t>(somfy.transceiver.config.proto));
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
});
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addRoom",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Adding a room");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(somfy.roomCount() > SOMFY_MAX_ROOMS) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of rooms exceeded.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyRoom *room = somfy.addRoom(obj);
|
||||
if(room) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeRoom(room, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding room.\"}"));
|
||||
}
|
||||
}));
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addShade",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Adding a shade");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(somfy.shadeCount() > SOMFY_MAX_SHADES) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of shades exceeded.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyShade *shade = somfy.addShade(obj);
|
||||
if(shade) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding shade.\"}"));
|
||||
}
|
||||
}));
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/addGroup",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Adding a group");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(somfy.groupCount() > SOMFY_MAX_GROUPS) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Maximum number of groups exceeded.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyGroup *group = somfy.addGroup(obj);
|
||||
if(group) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Error adding group.\"}"));
|
||||
}
|
||||
}));
|
||||
|
||||
// What a new room, shade or group would get, from a snapshot.
|
||||
asyncServer.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextRoom); });
|
||||
asyncServer.on("/getNextShade", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextShade); });
|
||||
asyncServer.on("/getNextGroup", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextGroup); });
|
||||
asyncServer.on("/groupOptions", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
if(asyncHasParam(request, "groupId")) {
|
||||
int groupId = atoi(asyncParam(request, "groupId").c_str());
|
||||
SomfyGroup* group = somfy.getGroupById(groupId);
|
||||
if(group) {
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.beginArray("availShades");
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &somfy.shades[i];
|
||||
if(shade->getShadeId() != 255) {
|
||||
bool isLinked = false;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
||||
if(group->linkedShades[j] == shade->getShadeId()) {
|
||||
isLinked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!isLinked) {
|
||||
resp.beginObject();
|
||||
serializeShadeRef(shade, resp);
|
||||
resp.endObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.endArray();
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
|
||||
}
|
||||
else {
|
||||
if(!asyncHasParam(request, "groupId")) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid group id.\"}"));
|
||||
return;
|
||||
}
|
||||
const int groupId = atoi(asyncParam(request, "groupId").c_str());
|
||||
// As for /group, existence is checked on the live model.
|
||||
if(somfy.getGroupById(groupId)) sendSnapshot(request, snapshot_doc_t::groupOptions, groupId);
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
|
||||
});
|
||||
// Adding and saving rooms, shades and groups. Edits of the shade model:
|
||||
// loop() applies them and sends requestResult.
|
||||
asyncServer.addHandler(editHandler("/addRoom", model_edit_type_t::add_room));
|
||||
asyncServer.addHandler(editHandler("/addShade", model_edit_type_t::add_shade));
|
||||
asyncServer.addHandler(editHandler("/addGroup", model_edit_type_t::add_group));
|
||||
asyncServer.addHandler(editHandler("/saveRoom", model_edit_type_t::save_room));
|
||||
asyncServer.addHandler(editHandler("/saveShade", model_edit_type_t::save_shade));
|
||||
asyncServer.addHandler(editHandler("/saveGroup", model_edit_type_t::save_group));
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveRoom",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Updating a room");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["roomId"].isNull()) {
|
||||
SomfyRoom* room = somfy.getRoomById(obj["roomId"]);
|
||||
if(room) {
|
||||
room->fromJSON(obj);
|
||||
room->save();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeRoom(room, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}"));
|
||||
}));
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveShade",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Updating a shade");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) {
|
||||
SomfyShade* shade = somfy.getShadeById(obj["shadeId"]);
|
||||
if(shade) {
|
||||
int8_t err = shade->fromJSON(obj);
|
||||
if(err == 0) {
|
||||
shade->save();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else {
|
||||
snprintf(g_async_content, sizeof(g_async_content), "{\"status\":\"DATA\",\"desc\":\"Data Error.\", \"code\":%d}", err);
|
||||
request->send(500, _encoding_json, g_async_content);
|
||||
}
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
|
||||
}));
|
||||
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/saveGroup",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Updating a group");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["groupId"].isNull()) {
|
||||
SomfyGroup* group = somfy.getGroupById(obj["groupId"]);
|
||||
if(group) {
|
||||
group->fromJSON(obj);
|
||||
group->save();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}"));
|
||||
}));
|
||||
|
||||
// setMyPosition - supports both GET with query params and POST/PUT with JSON body
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setMyPosition",
|
||||
|
|
@ -1374,54 +1226,9 @@ void Web::begin() {
|
|||
}
|
||||
});
|
||||
|
||||
// setPaired - supports both query params and JSON body
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setPaired",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t shadeId = 255;
|
||||
bool paired = false;
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["paired"].isNull()) paired = obj["paired"];
|
||||
}
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to pair\"}"));
|
||||
}
|
||||
else {
|
||||
shade->paired = paired;
|
||||
shade->save();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
}));
|
||||
asyncServer.addHandler(editHandler("/setPaired", model_edit_type_t::set_paired));
|
||||
asyncServer.on("/setPaired", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = 255;
|
||||
bool paired = false;
|
||||
if(asyncHasParam(request, "shadeId"))
|
||||
shadeId = atoi(asyncParam(request, "shadeId").c_str());
|
||||
if(asyncHasParam(request, "paired"))
|
||||
paired = toBoolean(asyncParam(request, "paired").c_str(), false);
|
||||
SomfyShade* shade = nullptr;
|
||||
if(shadeId != 255) shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade not found to pair\"}"));
|
||||
}
|
||||
else {
|
||||
shade->paired = paired;
|
||||
shade->save();
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::set_paired, { "shadeId", "paired" });
|
||||
});
|
||||
|
||||
// unpairShade
|
||||
|
|
@ -1467,310 +1274,35 @@ void Web::begin() {
|
|||
}
|
||||
});
|
||||
|
||||
// linkRepeater
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkRepeater",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint32_t address = 0;
|
||||
if(!json.isNull()) {
|
||||
ESP_LOGD(TAG, "Linking a repeater");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["address"].isNull()) address = obj["address"];
|
||||
else if(!obj["remoteAddress"].isNull()) address = obj["remoteAddress"];
|
||||
}
|
||||
if(address == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.linkRepeater(address);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginArray();
|
||||
serializeRepeaters(resp);
|
||||
resp.endArray();
|
||||
resp.endResponse();
|
||||
}
|
||||
}));
|
||||
// Linking repeaters, remotes and group members, and deleting. Edits of the
|
||||
// shade model: loop() applies them and sends requestResult.
|
||||
asyncServer.addHandler(editHandler("/linkRepeater", model_edit_type_t::link_repeater));
|
||||
asyncServer.on("/linkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint32_t address = 0;
|
||||
if(asyncHasParam(request, "address"))
|
||||
address = atoi(asyncParam(request, "address").c_str());
|
||||
if(address == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.linkRepeater(address);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginArray();
|
||||
serializeRepeaters(resp);
|
||||
resp.endArray();
|
||||
resp.endResponse();
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::link_repeater, { "address" });
|
||||
});
|
||||
|
||||
// unlinkRepeater
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkRepeater",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint32_t address = 0;
|
||||
if(!json.isNull()) {
|
||||
ESP_LOGD(TAG, "Unlinking a repeater");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["address"].isNull()) address = obj["address"];
|
||||
else if(!obj["remoteAddress"].isNull()) address = obj["remoteAddress"];
|
||||
}
|
||||
if(address == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.unlinkRepeater(address);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginArray();
|
||||
serializeRepeaters(resp);
|
||||
resp.endArray();
|
||||
resp.endResponse();
|
||||
}
|
||||
}));
|
||||
asyncServer.addHandler(editHandler("/unlinkRepeater", model_edit_type_t::unlink_repeater));
|
||||
asyncServer.on("/unlinkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint32_t address = 0;
|
||||
if(asyncHasParam(request, "address"))
|
||||
address = atoi(asyncParam(request, "address").c_str());
|
||||
if(address == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No repeater address was supplied.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.unlinkRepeater(address);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginArray();
|
||||
serializeRepeaters(resp);
|
||||
resp.endArray();
|
||||
resp.endResponse();
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::unlink_repeater, { "address" });
|
||||
});
|
||||
|
||||
// unlinkRemote
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkRemote",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); return; }
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) {
|
||||
SomfyShade* shade = somfy.getShadeById(obj["shadeId"]);
|
||||
if(shade) {
|
||||
if(!obj["remoteAddress"].isNull()) {
|
||||
shade->unlinkRemote(obj["remoteAddress"]);
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}"));
|
||||
}
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
|
||||
}));
|
||||
|
||||
// linkRemote
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkRemote",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Linking a remote");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) {
|
||||
SomfyShade* shade = somfy.getShadeById(obj["shadeId"]);
|
||||
if(shade) {
|
||||
if(!obj["remoteAddress"].isNull()) {
|
||||
if(!obj["rollingCode"].isNull()) shade->linkRemote(obj["remoteAddress"], obj["rollingCode"]);
|
||||
else shade->linkRemote(obj["remoteAddress"]);
|
||||
}
|
||||
else {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Remote address not provided.\"}"));
|
||||
}
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeShade(shade, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}"));
|
||||
}
|
||||
else request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}"));
|
||||
}));
|
||||
|
||||
// linkToGroup
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkToGroup",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No linking object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Linking a shade to a group");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
uint8_t shadeId = !obj["shadeId"].isNull() ? obj["shadeId"] : 0;
|
||||
uint8_t groupId = !obj["groupId"].isNull() ? obj["groupId"] : 0;
|
||||
if(groupId == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}"));
|
||||
return;
|
||||
}
|
||||
if(shadeId == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}"));
|
||||
return;
|
||||
}
|
||||
group->linkShade(shadeId);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}));
|
||||
|
||||
// unlinkFromGroup
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkFromGroup",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No unlinking object supplied.\"}")); return; }
|
||||
ESP_LOGD(TAG, "Unlinking a shade from a group");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
uint8_t shadeId = !obj["shadeId"].isNull() ? obj["shadeId"] : 0;
|
||||
uint8_t groupId = !obj["groupId"].isNull() ? obj["groupId"] : 0;
|
||||
if(groupId == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not provided.\"}"));
|
||||
return;
|
||||
}
|
||||
if(shadeId == 0) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not provided.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group id not found.\"}"));
|
||||
return;
|
||||
}
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) {
|
||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade id not found.\"}"));
|
||||
return;
|
||||
}
|
||||
group->unlinkShade(shadeId);
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
serializeGroup(group, resp);
|
||||
resp.endObject();
|
||||
resp.endResponse();
|
||||
}));
|
||||
|
||||
// deleteRoom - supports GET with query params and POST/PUT with JSON body
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteRoom",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t roomId = 0;
|
||||
if(!json.isNull()) {
|
||||
ESP_LOGD(TAG, "Deleting a Room");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["roomId"].isNull()) roomId = obj["roomId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room id was supplied.\"}")); return; }
|
||||
}
|
||||
SomfyRoom* room = somfy.getRoomById(roomId);
|
||||
if(!room) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room with the specified id not found.\"}"));
|
||||
else {
|
||||
somfy.deleteRoom(roomId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Room deleted.\"}"));
|
||||
}
|
||||
}));
|
||||
asyncServer.addHandler(editHandler("/linkRemote", model_edit_type_t::link_remote));
|
||||
asyncServer.addHandler(editHandler("/unlinkRemote", model_edit_type_t::unlink_remote));
|
||||
asyncServer.addHandler(editHandler("/linkToGroup", model_edit_type_t::link_to_group));
|
||||
asyncServer.addHandler(editHandler("/unlinkFromGroup", model_edit_type_t::unlink_from_group));
|
||||
asyncServer.addHandler(editHandler("/deleteRoom", model_edit_type_t::delete_room));
|
||||
asyncServer.on("/deleteRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t roomId = 0;
|
||||
if(asyncHasParam(request, "roomId")) {
|
||||
roomId = atoi(asyncParam(request, "roomId").c_str());
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No room object supplied.\"}")); return; }
|
||||
SomfyRoom* room = somfy.getRoomById(roomId);
|
||||
if(!room) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Room with the specified id not found.\"}"));
|
||||
else {
|
||||
somfy.deleteRoom(roomId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Room deleted.\"}"));
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_room, { "roomId" });
|
||||
});
|
||||
|
||||
// deleteShade - supports GET with query params and POST/PUT with JSON body
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteShade",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t shadeId = 255;
|
||||
if(!json.isNull()) {
|
||||
ESP_LOGD(TAG, "Deleting a shade");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; }
|
||||
}
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
else if(shade->isInGroup()) {
|
||||
request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This shade is a member of a group and cannot be deleted.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.deleteShade(shadeId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Shade deleted.\"}"));
|
||||
}
|
||||
}));
|
||||
asyncServer.addHandler(editHandler("/deleteShade", model_edit_type_t::delete_shade));
|
||||
asyncServer.on("/deleteShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t shadeId = 255;
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
shadeId = atoi(asyncParam(request, "shadeId").c_str());
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade with the specified id not found.\"}"));
|
||||
else if(shade->isInGroup()) {
|
||||
request->send(400, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"This shade is a member of a group and cannot be deleted.\"}"));
|
||||
}
|
||||
else {
|
||||
somfy.deleteShade(shadeId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Shade deleted.\"}"));
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_shade, { "shadeId" });
|
||||
});
|
||||
|
||||
// deleteGroup - supports GET with query params and POST/PUT with JSON body
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/deleteGroup",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
uint8_t groupId = 255;
|
||||
if(!json.isNull()) {
|
||||
ESP_LOGD(TAG, "Deleting a group");
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); return; }
|
||||
}
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}"));
|
||||
else {
|
||||
somfy.deleteGroup(groupId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Group deleted.\"}"));
|
||||
}
|
||||
}));
|
||||
asyncServer.addHandler(editHandler("/deleteGroup", model_edit_type_t::delete_group));
|
||||
asyncServer.on("/deleteGroup", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint8_t groupId = 255;
|
||||
if(asyncHasParam(request, "groupId")) {
|
||||
groupId = atoi(asyncParam(request, "groupId").c_str());
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; }
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group with the specified id not found.\"}"));
|
||||
else {
|
||||
somfy.deleteGroup(groupId);
|
||||
request->send(200, _encoding_json, F("{\"status\":\"SUCCESS\",\"desc\":\"Group deleted.\"}"));
|
||||
}
|
||||
queueModelEditFromParams(request, model_edit_type_t::delete_group, { "groupId" });
|
||||
});
|
||||
// The repeaters, from a snapshot like the other reads.
|
||||
asyncServer.on("/repeaters", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::repeaters); });
|
||||
|
||||
|
||||
// updateFirmware - file upload
|
||||
asyncServer.on("/updateFirmware", HTTP_POST,
|
||||
|
|
@ -2195,62 +1727,10 @@ void Web::begin() {
|
|||
request->send(200, _encoding_json, g_async_content);
|
||||
});
|
||||
|
||||
// roomSortOrder
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/roomSortOrder",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonArray arr = json.as<JsonArray>();
|
||||
uint8_t order = 0;
|
||||
for(JsonVariant v : arr) {
|
||||
uint8_t roomId = v.as<uint8_t>();
|
||||
if(roomId != 0) {
|
||||
SomfyRoom *room = somfy.getRoomById(roomId);
|
||||
if(room) room->sortOrder = order++;
|
||||
}
|
||||
}
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set room order\"}");
|
||||
}));
|
||||
|
||||
// shadeSortOrder
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/shadeSortOrder",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonArray arr = json.as<JsonArray>();
|
||||
uint8_t order = 0;
|
||||
for(JsonVariant v : arr) {
|
||||
uint8_t shadeId = v.as<uint8_t>();
|
||||
if(shadeId != 255) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(shade) shade->sortOrder = order++;
|
||||
}
|
||||
}
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set shade order\"}");
|
||||
}));
|
||||
|
||||
// groupSortOrder
|
||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/groupSortOrder",
|
||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(json.isNull()) {
|
||||
request->send(500, "application/json", "{\"status\":\"ERROR\",\"desc\":\"JSON parse error\"}");
|
||||
return;
|
||||
}
|
||||
JsonArray arr = json.as<JsonArray>();
|
||||
uint8_t order = 0;
|
||||
for(JsonVariant v : arr) {
|
||||
uint8_t groupId = v.as<uint8_t>();
|
||||
if(groupId != 255) {
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(group) group->sortOrder = order++;
|
||||
}
|
||||
}
|
||||
request->send(200, "application/json", "{\"status\":\"OK\",\"desc\":\"Successfully set group order\"}");
|
||||
}));
|
||||
// Sort orders: the ids in the order the client lists them.
|
||||
asyncServer.addHandler(editHandler("/roomSortOrder", model_edit_type_t::room_order));
|
||||
asyncServer.addHandler(editHandler("/shadeSortOrder", model_edit_type_t::shade_order));
|
||||
asyncServer.addHandler(editHandler("/groupSortOrder", model_edit_type_t::group_order));
|
||||
|
||||
asyncServer.on("/beginFrequencyScan", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
somfy.transceiver.beginFrequencyScan();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include <esp_task_wdt.h>
|
||||
#include "Somfy.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Sockets.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
|
||||
|
|
@ -89,6 +90,18 @@ void SomfyGroup::emitState(uint8_t num, const char *evt) {
|
|||
sockEmit.endEmit(num);
|
||||
this->publish();
|
||||
}
|
||||
// To every client: the sender picks out its own by requestId.
|
||||
void emitRequestResult(uint32_t requestId, const edit_result_t &result) {
|
||||
JsonSockEvent *json = sockEmit.beginEmit("requestResult");
|
||||
json->beginObject();
|
||||
json->addElem("requestId", requestId);
|
||||
json->addElem("ok", result.ok);
|
||||
json->addElem("id", result.id);
|
||||
json->addElem("code", result.code);
|
||||
json->addElem("desc", result.desc);
|
||||
json->endObject();
|
||||
sockEmit.endEmit();
|
||||
}
|
||||
void SomfyShadeController::emitState(uint8_t num) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||
SomfyShade *shade = &this->shades[i];
|
||||
|
|
|
|||
161
test/test_modeleditor/test_modeleditor.cpp
Normal file
161
test/test_modeleditor/test_modeleditor.cpp
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
#include <unity.h>
|
||||
#include <set>
|
||||
#include "ModelEditor.h"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
/*********************************************************************
|
||||
* Ids, sort orders and remote addresses
|
||||
********************************************************************/
|
||||
static void test_first_free_id_fills_gaps() {
|
||||
const uint8_t ids[] = { 1, 2, 4, 255, 255 };
|
||||
TEST_ASSERT_EQUAL_UINT8(3, firstFreeId(ids, 5, 30));
|
||||
}
|
||||
static void test_first_free_id_of_an_empty_list_is_1() {
|
||||
TEST_ASSERT_EQUAL_UINT8(1, firstFreeId(nullptr, 0, 30));
|
||||
}
|
||||
static void test_first_free_id_is_0_when_every_one_is_taken() {
|
||||
const uint8_t ids[] = { 1, 2, 3 };
|
||||
TEST_ASSERT_EQUAL_UINT8(0, firstFreeId(ids, 3, 3));
|
||||
TEST_ASSERT_EQUAL_UINT8(4, firstFreeId(ids, 3, 4));
|
||||
}
|
||||
static void test_next_sort_order_goes_after_the_last() {
|
||||
const int8_t orders[] = { 3, 0, 7, 2 };
|
||||
TEST_ASSERT_EQUAL_INT8(8, nextSortOrder(orders, 4));
|
||||
TEST_ASSERT_EQUAL_INT8(0, nextSortOrder(nullptr, 0));
|
||||
}
|
||||
static void test_free_remote_address_skips_every_taken_one() {
|
||||
// 101 is taken, so 102 is tried, which is taken too, and so on.
|
||||
const uint32_t used[] = { 103, 101, 102, 500 };
|
||||
TEST_ASSERT_EQUAL_UINT32(104, freeRemoteAddress(101, used, 4));
|
||||
TEST_ASSERT_EQUAL_UINT32(100, freeRemoteAddress(100, used, 4));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Lists
|
||||
********************************************************************/
|
||||
static void test_add_goes_into_the_first_empty_slot() {
|
||||
uint8_t list[4] = { 5, 7, 0, 0 };
|
||||
TEST_ASSERT_TRUE(addToList(list, 4, 9));
|
||||
const uint8_t expected[] = { 5, 7, 9, 0 };
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, list, 4);
|
||||
}
|
||||
static void test_adding_one_already_there_changes_nothing() {
|
||||
uint8_t list[4] = { 5, 7, 0, 0 };
|
||||
TEST_ASSERT_TRUE(addToList(list, 4, 7));
|
||||
const uint8_t expected[] = { 5, 7, 0, 0 };
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, list, 4);
|
||||
}
|
||||
static void test_adding_to_a_full_list_fails() {
|
||||
uint8_t list[2] = { 5, 7 };
|
||||
TEST_ASSERT_FALSE(addToList(list, 2, 9));
|
||||
}
|
||||
static void test_remove_packs_the_rest_in_order() {
|
||||
uint8_t list[5] = { 1, 4, 3, 4, 0 };
|
||||
TEST_ASSERT_TRUE(removeFromList(list, 5, 4));
|
||||
const uint8_t expected[] = { 1, 3, 0, 0, 0 };
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, list, 5);
|
||||
}
|
||||
static void test_removing_one_not_there_reports_it() {
|
||||
uint8_t list[3] = { 1, 2, 0 };
|
||||
TEST_ASSERT_FALSE(removeFromList(list, 3, 9));
|
||||
}
|
||||
static void test_pack_closes_gaps() {
|
||||
uint8_t list[7] = { 1, 0, 4, 3, 0, 0, 6 };
|
||||
packList(list, 7);
|
||||
const uint8_t expected[] = { 1, 4, 3, 6, 0, 0, 0 };
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, list, 7);
|
||||
}
|
||||
static void test_repeater_addresses_use_the_same_rules() {
|
||||
uint32_t list[3] = { 0, 0, 0 };
|
||||
TEST_ASSERT_TRUE(addToList(list, 3, 0xABCDEFu));
|
||||
TEST_ASSERT_TRUE(addToList(list, 3, 0x123456u));
|
||||
TEST_ASSERT_TRUE(addToList(list, 3, 0xABCDEFu));
|
||||
TEST_ASSERT_TRUE(removeFromList(list, 3, 0xABCDEFu));
|
||||
TEST_ASSERT_EQUAL_UINT32(0x123456u, list[0]);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, list[1]);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* GPIO pins
|
||||
********************************************************************/
|
||||
static std::set<uint8_t> radioPins, ethernetPins, shadePins;
|
||||
static bool inSet(uint8_t pin, void *ctx) { return static_cast<std::set<uint8_t> *>(ctx)->count(pin) > 0; }
|
||||
static pin_users_t users(std::set<uint8_t> &pins) {
|
||||
pin_users_t u;
|
||||
u.uses = inSet;
|
||||
u.ctx = &pins;
|
||||
return u;
|
||||
}
|
||||
static int8_t conflict(shade_types type, radio_proto proto, uint8_t up, uint8_t down, uint8_t my) {
|
||||
return gpioPinConflict(type, proto, up, down, my, users(radioPins), users(ethernetPins), users(shadePins));
|
||||
}
|
||||
|
||||
static void test_a_radio_shade_uses_no_pins() {
|
||||
radioPins = { 4 };
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::roller, radio_proto::RTS, 4, 4, 4));
|
||||
}
|
||||
static void test_free_pins_are_fine() {
|
||||
radioPins = { 4 }; ethernetPins = { 18 }; shadePins = { 21 };
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::roller, radio_proto::GP_Remote, 25, 26, 27));
|
||||
}
|
||||
static void test_a_pin_the_radio_uses() {
|
||||
radioPins = { 26 }; ethernetPins = {}; shadePins = {};
|
||||
TEST_ASSERT_EQUAL_INT8(PIN_CONFLICT_RADIO, conflict(shade_types::roller, radio_proto::GP_Remote, 25, 26, 27));
|
||||
}
|
||||
static void test_every_pin_is_checked_against_ethernet() {
|
||||
// It used to check only the up pin against Ethernet.
|
||||
radioPins = {}; shadePins = {};
|
||||
ethernetPins = { 26 };
|
||||
TEST_ASSERT_EQUAL_INT8(PIN_CONFLICT_ETHERNET, conflict(shade_types::roller, radio_proto::GP_Remote, 25, 26, 27));
|
||||
ethernetPins = { 27 };
|
||||
TEST_ASSERT_EQUAL_INT8(PIN_CONFLICT_ETHERNET, conflict(shade_types::roller, radio_proto::GP_Remote, 25, 26, 27));
|
||||
}
|
||||
static void test_a_pin_another_shade_uses() {
|
||||
radioPins = {}; ethernetPins = {};
|
||||
shadePins = { 25 };
|
||||
TEST_ASSERT_EQUAL_INT8(PIN_CONFLICT_SHADE, conflict(shade_types::roller, radio_proto::GP_Remote, 25, 26, 27));
|
||||
}
|
||||
static void test_only_the_pins_a_shade_drives_are_checked() {
|
||||
radioPins = {}; ethernetPins = {};
|
||||
shadePins = { 25, 27 };
|
||||
// A relay never drives My.
|
||||
TEST_ASSERT_EQUAL_INT8(PIN_CONFLICT_SHADE, conflict(shade_types::roller, radio_proto::GP_Relay, 25, 26, 27));
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::roller, radio_proto::GP_Relay, 24, 26, 27));
|
||||
// A dry contact only drives down; a two way dry contact up and down.
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::drycontact, radio_proto::GP_Remote, 25, 26, 27));
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::drycontact2, radio_proto::GP_Remote, 24, 26, 27));
|
||||
// A single button gate on a remote only drives down.
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::garage1, radio_proto::GP_Remote, 25, 26, 27));
|
||||
}
|
||||
static void test_255_means_no_pin() {
|
||||
radioPins = { 255 }; ethernetPins = { 255 }; shadePins = { 255 };
|
||||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::roller, radio_proto::GP_Remote, 255, 26, 255));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_first_free_id_fills_gaps);
|
||||
RUN_TEST(test_first_free_id_of_an_empty_list_is_1);
|
||||
RUN_TEST(test_first_free_id_is_0_when_every_one_is_taken);
|
||||
RUN_TEST(test_next_sort_order_goes_after_the_last);
|
||||
RUN_TEST(test_free_remote_address_skips_every_taken_one);
|
||||
RUN_TEST(test_add_goes_into_the_first_empty_slot);
|
||||
RUN_TEST(test_adding_one_already_there_changes_nothing);
|
||||
RUN_TEST(test_adding_to_a_full_list_fails);
|
||||
RUN_TEST(test_remove_packs_the_rest_in_order);
|
||||
RUN_TEST(test_removing_one_not_there_reports_it);
|
||||
RUN_TEST(test_pack_closes_gaps);
|
||||
RUN_TEST(test_repeater_addresses_use_the_same_rules);
|
||||
RUN_TEST(test_a_radio_shade_uses_no_pins);
|
||||
RUN_TEST(test_free_pins_are_fine);
|
||||
RUN_TEST(test_a_pin_the_radio_uses);
|
||||
RUN_TEST(test_every_pin_is_checked_against_ethernet);
|
||||
RUN_TEST(test_a_pin_another_shade_uses);
|
||||
RUN_TEST(test_only_the_pins_a_shade_drives_are_checked);
|
||||
RUN_TEST(test_255_means_no_pin);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -217,6 +217,47 @@ static void test_a_left_out_item_does_not_leave_a_stray_comma() {
|
|||
TEST_ASSERT_EQUAL_STRING(("[" + shadeJson(4) + "]").c_str(), document(snapshot_doc_t::shades, 0, 7, fits).c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Repeaters, what a new item would get, and group options
|
||||
********************************************************************/
|
||||
static void test_repeaters_list() {
|
||||
TEST_ASSERT_EQUAL_STRING("[12345,678]", document(snapshot_doc_t::repeaters).c_str());
|
||||
}
|
||||
static void test_next_room_fills_the_lowest_gap() {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"roomId\":1}", document(snapshot_doc_t::nextRoom).c_str());
|
||||
}
|
||||
static void test_next_shade() {
|
||||
// Ids 1 and 4 are taken, so 2; its address is startingAddress + id unless taken.
|
||||
snap.radioBitLength = 80;
|
||||
snap.radioProto = 1;
|
||||
TEST_ASSERT_EQUAL_STRING("{\"shadeId\":2,\"remoteAddress\":79,\"bitLength\":80,\"stepSize\":100,\"proto\":1}",
|
||||
document(snapshot_doc_t::nextShade).c_str());
|
||||
}
|
||||
static void test_next_address_skips_one_already_used() {
|
||||
snap.shades[0].remoteAddress = 79; // what shade 2 would get
|
||||
snap.shades[1].remoteAddress = 80;
|
||||
TEST_ASSERT_EQUAL_UINT32(81, nextRemoteAddress(snap, 2));
|
||||
}
|
||||
static void test_next_group() {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"groupId\":1,\"remoteAddress\":78,\"bitLength\":0,\"proto\":0}",
|
||||
document(snapshot_doc_t::nextGroup).c_str());
|
||||
}
|
||||
static void test_no_room_for_another_shade() {
|
||||
snap.shadeCount = 0;
|
||||
for(uint8_t id = 1; id <= SOMFY_MAX_SHADES - 2; id++) snap.shades[snap.shadeCount++] = shade(id, "S");
|
||||
TEST_ASSERT_EQUAL_UINT8(255, nextShadeId(snap));
|
||||
}
|
||||
static void test_group_options_lists_the_shades_not_in_it() {
|
||||
snap.shades[snap.shadeCount++] = shade(7, "Spare");
|
||||
const std::string expected = groupWithLinked().substr(0, groupWithLinked().length() - 1) +
|
||||
",\"availShades\":[" + shadeRefJson(7) + "]}";
|
||||
TEST_ASSERT_EQUAL_STRING(expected.c_str(), document(snapshot_doc_t::groupOptions, 6).c_str());
|
||||
}
|
||||
static void test_group_options_of_a_group_gone_by_the_snapshot() {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}",
|
||||
document(snapshot_doc_t::groupOptions, 9).c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Slots
|
||||
********************************************************************/
|
||||
|
|
@ -288,6 +329,14 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_a_single_item_gone_by_the_snapshot_is_an_error);
|
||||
RUN_TEST(test_an_item_too_big_for_the_unit_is_left_out);
|
||||
RUN_TEST(test_a_left_out_item_does_not_leave_a_stray_comma);
|
||||
RUN_TEST(test_repeaters_list);
|
||||
RUN_TEST(test_next_room_fills_the_lowest_gap);
|
||||
RUN_TEST(test_next_shade);
|
||||
RUN_TEST(test_next_address_skips_one_already_used);
|
||||
RUN_TEST(test_next_group);
|
||||
RUN_TEST(test_no_room_for_another_shade);
|
||||
RUN_TEST(test_group_options_lists_the_shades_not_in_it);
|
||||
RUN_TEST(test_group_options_of_a_group_gone_by_the_snapshot);
|
||||
RUN_TEST(test_two_reads_at_once_and_no_more);
|
||||
RUN_TEST(test_a_read_is_filled_then_freed);
|
||||
RUN_TEST(test_a_read_gone_before_its_fill_is_not_filled);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue