mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
refactor: apply shade model edits on loop() through ModelEditor
The 19 routes that changed rooms, shades, groups and repeaters on the web
server task (add, save, delete, link and unlink, setPaired, the sort
orders) now copy their request into a model_edit_t, queue it on the
LoopQueue and reply {status:accepted,requestId:N}. loop() applies it
(SomfyJson.cpp, next to fromJSON) and broadcasts requestResult {requestId,
ok, id, code, desc}. One edit at a time; a second gets 503, a body over
4 KB gets 413.
ModelEditor.h/.cpp holds the rules, tested natively: the lowest free id,
the next sort order, a free remote address, adding to and removing from
the group-member and repeater lists, and the GPIO pin-conflict check. They
replace the getNext*Id/getMax*Order copies, the remote-address search,
the list code and the inline pin checks. The pin check now uses the
protocol being saved and checks every pin against Ethernet, not only up.
/getNextRoom, /getNextShade, /getNextGroup, /groupOptions and a new
/repeaters (the UI already called it) are ModelSnapshot reads.
The web UI's editJSON() sends an edit, waits for its requestResult, then
reads the item back from /shade, /group, /room or /repeaters, so the
callbacks that used the old reply bodies are unchanged. No result within
5 s offers a reload; reads and edits told Busy are retried. It now also
refreshes its lists on shadeAdded/shadeRemoved/groupAdded/groupRemoved
from other clients, and procRoomRemoved keeps the right rooms.
Also fixed: a shade that fails validation is no longer added; linked
repeaters and sort orders are saved; linking reports a full list.
27 new native tests, 319 in all. Flash: esp32devdbg -11,484 bytes,
esp32dev -7,180 bytes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
77703a9855
commit
e70e499d75
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.
|
||||
|
|
|
|||
|
|
@ -109,7 +109,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