mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-11-01 22:32:13 +01:00
Merge pull request #42 from cjkas/refactor/model-editor
refactor: apply shade model edits on loop() through ModelEditor
This commit is contained in:
commit
e39ec13884
20 changed files with 1035 additions and 918 deletions
|
|
@ -245,7 +245,7 @@ var httpStatusText = {
|
||||||
'504': 'Gateway Timeout',
|
'504': 'Gateway Timeout',
|
||||||
'505': 'HTTP Version Not Supported'
|
'505': 'HTTP Version Not Supported'
|
||||||
};
|
};
|
||||||
function getJSON(url, cb) {
|
function getJSON(url, cb, attempt) {
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
console.log({ get: url });
|
console.log({ get: url });
|
||||||
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}${url}` : url, true);
|
xhr.open('GET', baseUrl.length > 0 ? `${baseUrl}${url}` : url, true);
|
||||||
|
|
@ -253,6 +253,10 @@ function getJSON(url, cb) {
|
||||||
xhr.responseType = 'json';
|
xhr.responseType = 'json';
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
let status = xhr.status;
|
let status = xhr.status;
|
||||||
|
if (isBusy(status, xhr.response) && (attempt || 0) < 10) {
|
||||||
|
setTimeout(() => getJSON(url, cb, (attempt || 0) + 1), 250);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
let err = xhr.response || {};
|
let err = xhr.response || {};
|
||||||
err.htmlError = status;
|
err.htmlError = status;
|
||||||
|
|
@ -274,12 +278,17 @@ function getJSON(url, cb) {
|
||||||
};
|
};
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
function getJSONSync(url, cb) {
|
function getJSONSync(url, cb, attempt) {
|
||||||
let overlay = ui.waitMessage(document.getElementById('divContainer'));
|
let overlay = ui.waitMessage(document.getElementById('divContainer'));
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = 'json';
|
xhr.responseType = 'json';
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
let status = xhr.status;
|
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) {
|
if (status !== 200) {
|
||||||
let err = xhr.response || {};
|
let err = xhr.response || {};
|
||||||
err.htmlError = status;
|
err.htmlError = status;
|
||||||
|
|
@ -453,6 +462,79 @@ function putJSONSync(url, data, cb) {
|
||||||
xhr.send(JSON.stringify(data));
|
xhr.send(JSON.stringify(data));
|
||||||
} catch (err) { ui.serviceError(document.getElementById('divContainer'), err); }
|
} 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 socket;
|
||||||
var tConnect = null;
|
var tConnect = null;
|
||||||
var sockIsOpen = false;
|
var sockIsOpen = false;
|
||||||
|
|
@ -515,15 +597,24 @@ async function initSockets() {
|
||||||
case 'shadeCommand':
|
case 'shadeCommand':
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
break;
|
break;
|
||||||
|
case 'requestResult':
|
||||||
|
procRequestResult(msg);
|
||||||
|
break;
|
||||||
case 'roomRemoved':
|
case 'roomRemoved':
|
||||||
somfy.procRoomRemoved(msg);
|
somfy.procRoomRemoved(msg);
|
||||||
break;
|
break;
|
||||||
case 'roomAdded':
|
case 'roomAdded':
|
||||||
somfy.procRoomAdded(msg);
|
somfy.procRoomAdded(msg);
|
||||||
break;
|
break;
|
||||||
|
// Another client changed the lists. This one's own edits
|
||||||
|
// refresh them when their result comes in.
|
||||||
case 'shadeRemoved':
|
case 'shadeRemoved':
|
||||||
break;
|
|
||||||
case 'shadeAdded':
|
case 'shadeAdded':
|
||||||
|
if (!editsPending()) somfy.updateShadeList();
|
||||||
|
break;
|
||||||
|
case 'groupRemoved':
|
||||||
|
case 'groupAdded':
|
||||||
|
if (!editsPending()) somfy.updateGroupList();
|
||||||
break;
|
break;
|
||||||
case 'ethernet':
|
case 'ethernet':
|
||||||
wifi.procEthernet(msg);
|
wifi.procEthernet(msg);
|
||||||
|
|
@ -2181,7 +2272,7 @@ class Somfy {
|
||||||
if (room.roomId === 0) return;
|
if (room.roomId === 0) return;
|
||||||
let r = _rooms.find(x => x.roomId === room.roomId);
|
let r = _rooms.find(x => x.roomId === room.roomId);
|
||||||
if (typeof r !== 'undefined' && r.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 });
|
_rooms.sort((a, b) => { return a.sortOrder - b.sortOrder });
|
||||||
this.setRoomsList(_rooms);
|
this.setRoomsList(_rooms);
|
||||||
let rs = document.getElementById('divRoomSelector');
|
let rs = document.getElementById('divRoomSelector');
|
||||||
|
|
@ -2259,7 +2350,7 @@ class Somfy {
|
||||||
order.push(parseInt(items[i].getAttribute('data-roomid'), 10));
|
order.push(parseInt(items[i].getAttribute('data-roomid'), 10));
|
||||||
// Reorder the shades on the main page.
|
// Reorder the shades on the main page.
|
||||||
}
|
}
|
||||||
putJSONSync('/roomSortOrder', order, (err) => {
|
editJSON('/roomSortOrder', order, null, (err) => {
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else this.updateRoomsList();
|
else this.updateRoomsList();
|
||||||
});
|
});
|
||||||
|
|
@ -2455,7 +2546,7 @@ class Somfy {
|
||||||
order.push(parseInt(items[i].getAttribute('data-shadeid'), 10));
|
order.push(parseInt(items[i].getAttribute('data-shadeid'), 10));
|
||||||
// Reorder the shades on the main page.
|
// 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--) {
|
for (let i = order.length - 1; i >= 0; i--) {
|
||||||
let el = shadeControls.querySelector(`.somfyShadeCtl[data-shadeid="${order[i]}"`);
|
let el = shadeControls.querySelector(`.somfyShadeCtl[data-shadeid="${order[i]}"`);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|
@ -2700,7 +2791,7 @@ class Somfy {
|
||||||
order.push(parseInt(items[i].getAttribute('data-groupid'), 10));
|
order.push(parseInt(items[i].getAttribute('data-groupid'), 10));
|
||||||
// Reorder the shades on the main page.
|
// 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--) {
|
for (let i = order.length - 1; i >= 0; i--) {
|
||||||
let el = groupControls.querySelector(`.somfyGroupCtl[data-groupid="${order[i]}"`);
|
let el = groupControls.querySelector(`.somfyGroupCtl[data-groupid="${order[i]}"`);
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|
@ -2909,18 +3000,17 @@ class Somfy {
|
||||||
remoteAddress: frame.address,
|
remoteAddress: frame.address,
|
||||||
rollingCode: frame.rcode
|
rollingCode: frame.rcode
|
||||||
};
|
};
|
||||||
let overlay = ui.waitMessage(document.getElementById('divLinking'));
|
editJSON('/linkRemote', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||||
putJSON('/linkRemote', obj, (err, shade) => {
|
|
||||||
console.log(shade);
|
console.log(shade);
|
||||||
overlay.remove();
|
|
||||||
lnk.remove();
|
lnk.remove();
|
||||||
this.setLinkedRemotesList(shade);
|
if (err) ui.serviceError(err);
|
||||||
|
else this.setLinkedRemotesList(shade);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lnk = document.getElementById('divLinkRepeater');
|
lnk = document.getElementById('divLinkRepeater');
|
||||||
if (lnk) {
|
if (lnk) {
|
||||||
putJSONSync(`/linkRepeater`, {address:frame.address}, (err, repeaters) => {
|
editJSON(`/linkRepeater`, {address:frame.address}, () => '/repeaters', (err, repeaters) => {
|
||||||
lnk.remove();
|
lnk.remove();
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else this.setRepeaterList(repeaters);
|
else this.setRepeaterList(repeaters);
|
||||||
|
|
@ -3251,7 +3341,7 @@ class Somfy {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (isNaN(roomId) || roomId === 0) {
|
if (isNaN(roomId) || roomId === 0) {
|
||||||
// We are adding.
|
// We are adding.
|
||||||
putJSONSync('/addRoom', obj, (err, room) => {
|
editJSON('/addRoom', obj, (id) => `/room?roomId=${id}`, (err, room) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
ui.serviceError(err);
|
ui.serviceError(err);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|
@ -3267,7 +3357,7 @@ class Somfy {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
obj.roomId = roomId;
|
obj.roomId = roomId;
|
||||||
putJSONSync('/saveRoom', obj, (err, room) => {
|
editJSON('/saveRoom', obj, (id) => `/room?roomId=${id}`, (err, room) => {
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else this.updateRoomsList();
|
else this.updateRoomsList();
|
||||||
console.log(room);
|
console.log(room);
|
||||||
|
|
@ -3324,7 +3414,7 @@ class Somfy {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (isNaN(shadeId) || shadeId >= 255) {
|
if (isNaN(shadeId) || shadeId >= 255) {
|
||||||
// We are adding.
|
// We are adding.
|
||||||
putJSONSync('/addShade', obj, (err, shade) => {
|
editJSON('/addShade', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
ui.serviceError(err);
|
ui.serviceError(err);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|
@ -3343,9 +3433,12 @@ class Somfy {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
obj.shadeId = shadeId;
|
obj.shadeId = shadeId;
|
||||||
putJSONSync('/saveShade', obj, (err, shade) => {
|
editJSON('/saveShade', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||||
if (err) ui.serviceError(err);
|
if (err) {
|
||||||
else this.updateShadeList();
|
ui.serviceError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updateShadeList();
|
||||||
console.log(shade);
|
console.log(shade);
|
||||||
let ico = document.getElementById('icoShade');
|
let ico = document.getElementById('icoShade');
|
||||||
let tilt = ico.parentElement.querySelector('i.icss-window-tilt');
|
let tilt = ico.parentElement.querySelector('i.icss-window-tilt');
|
||||||
|
|
@ -3376,7 +3469,7 @@ class Somfy {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (isNaN(groupId) || groupId >= 255) {
|
if (isNaN(groupId) || groupId >= 255) {
|
||||||
// We are adding.
|
// We are adding.
|
||||||
putJSONSync('/addGroup', obj, (err, group) => {
|
editJSON('/addGroup', obj, (id) => `/group?groupId=${id}`, (err, group) => {
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else {
|
else {
|
||||||
console.log(group);
|
console.log(group);
|
||||||
|
|
@ -3391,7 +3484,7 @@ class Somfy {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
obj.groupId = groupId;
|
obj.groupId = groupId;
|
||||||
putJSONSync('/saveGroup', obj, (err, shade) => {
|
editJSON('/saveGroup', obj, (id) => `/group?groupId=${id}`, (err, shade) => {
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else this.updateGroupList();
|
else this.updateGroupList();
|
||||||
console.log(shade);
|
console.log(shade);
|
||||||
|
|
@ -3458,7 +3551,7 @@ class Somfy {
|
||||||
else {
|
else {
|
||||||
let prompt = ui.promptMessage(`Are you sure you want to delete this room?`, () => {
|
let prompt = ui.promptMessage(`Are you sure you want to delete this room?`, () => {
|
||||||
ui.clearErrors();
|
ui.clearErrors();
|
||||||
putJSONSync('/deleteRoom', { roomId: roomId }, (err, room) => {
|
editJSON('/deleteRoom', { roomId: roomId }, null, (err, room) => {
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else
|
else
|
||||||
|
|
@ -3483,9 +3576,10 @@ class Somfy {
|
||||||
else {
|
else {
|
||||||
let prompt = ui.promptMessage(`Are you sure you want to delete this shade?`, () => {
|
let prompt = ui.promptMessage(`Are you sure you want to delete this shade?`, () => {
|
||||||
ui.clearErrors();
|
ui.clearErrors();
|
||||||
putJSONSync('/deleteShade', { shadeId: shadeId }, (err, shade) => {
|
editJSON('/deleteShade', { shadeId: shadeId }, null, (err, shade) => {
|
||||||
|
prompt.remove();
|
||||||
|
if (err) ui.serviceError(err);
|
||||||
this.updateShadeList();
|
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>`;
|
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 {
|
else {
|
||||||
let prompt = ui.promptMessage(`Are you sure you want to delete this group?`, () => {
|
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);
|
if (err) ui.serviceError(err);
|
||||||
this.updateGroupList();
|
this.updateGroupList();
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
|
|
@ -3631,7 +3725,7 @@ class Somfy {
|
||||||
let obj = { shadeId: shadeId, paired: paired || false };
|
let obj = { shadeId: shadeId, paired: paired || false };
|
||||||
let div = document.getElementById('divPairing');
|
let div = document.getElementById('divPairing');
|
||||||
let overlay = typeof div === 'undefined' ? undefined : ui.waitMessage(div);
|
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 (overlay) overlay.remove();
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
|
@ -4004,9 +4098,10 @@ class Somfy {
|
||||||
mouseDown = false;
|
mouseDown = false;
|
||||||
let obj = ui.fromElement(div);
|
let obj = ui.fromElement(div);
|
||||||
let prompt = ui.promptMessage('Confirm Motor Response', () => {
|
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);
|
console.log(group);
|
||||||
somfy.setLinkedShadesList(group);
|
if (err) ui.serviceError(err);
|
||||||
|
else somfy.setLinkedShadesList(group);
|
||||||
this.updateGroupList();
|
this.updateGroupList();
|
||||||
});
|
});
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
|
|
@ -4098,9 +4193,10 @@ class Somfy {
|
||||||
if (err) ui.serviceError(err);
|
if (err) ui.serviceError(err);
|
||||||
else {
|
else {
|
||||||
let prompt = ui.promptMessage('Confirm Motor Response', () => {
|
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);
|
console.log(group);
|
||||||
somfy.setLinkedShadesList(group);
|
if (err) ui.serviceError(err);
|
||||||
|
else somfy.setLinkedShadesList(group);
|
||||||
this.updateGroupList();
|
this.updateGroupList();
|
||||||
});
|
});
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
|
|
@ -4136,7 +4232,7 @@ class Somfy {
|
||||||
}
|
}
|
||||||
unlinkRepeater(address) {
|
unlinkRepeater(address) {
|
||||||
let prompt = ui.promptMessage('Are you sure you want to stop repeating frames from this 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);
|
if (err) ui.serviceError(err);
|
||||||
else this.setRepeaterList(repeaters);
|
else this.setRepeaterList(repeaters);
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
|
|
@ -4151,11 +4247,11 @@ class Somfy {
|
||||||
shadeId: shadeId,
|
shadeId: shadeId,
|
||||||
remoteAddress: remoteAddress
|
remoteAddress: remoteAddress
|
||||||
};
|
};
|
||||||
putJSONSync('/unlinkRemote', obj, (err, shade) => {
|
editJSON('/unlinkRemote', obj, (id) => `/shade?shadeId=${id}`, (err, shade) => {
|
||||||
|
|
||||||
console.log(shade);
|
console.log(shade);
|
||||||
prompt.remove();
|
prompt.remove();
|
||||||
this.setLinkedRemotesList(shade);
|
if (err) ui.serviceError(err);
|
||||||
|
else this.setLinkedRemotesList(shade);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Concurrent mutation during chunked `/controller` response
|
## 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`).
|
**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
|
### 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.
|
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
|
### 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.
|
- 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.
|
||||||
- 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()`.
|
|
||||||
- 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).
|
- 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
|
## Oversized read items are left out
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Plan: domain modules behind the LoopQueue
|
# 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).
|
**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
|
## 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.
|
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
|
### 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:
|
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`.
|
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
|
## 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.
|
- 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)).
|
- 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.
|
- `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.
|
- The Home Assistant integration sends no `apikey` header on shade, group and tilt commands.
|
||||||
|
- New ids stop two short of the maximum: 30 shades, 14 groups and 14 rooms instead of 32, 16 and 16 (`getNext*Id` stopped at `MAX - 1`). ModelEditor keeps those limits for now.
|
||||||
|
- `SomfyGroup::fromJSON` reads `linkedShades` into a local array and drops it, so a group's members only change through `/linkToGroup` and `/unlinkFromGroup`.
|
||||||
|
|
||||||
|
Fixed by step 3:
|
||||||
|
- The Ethernet pin conflict was only checked for the up pin, and the check used the shade's current protocol instead of the one being saved.
|
||||||
|
- `linkRepeater` never set `isDirty`, so a newly linked repeater wasn't saved. The sort orders weren't saved either.
|
||||||
|
- `/linkRemote` and `/unlinkRemote` sent a 500 and then a second response.
|
||||||
|
- The web UI's `procRoomRemoved` kept the removed room and dropped the others, and `deleteShade` never closed its prompt (`prompt.remove;`).
|
||||||
|
- The web UI called `/repeaters`, which didn't exist.
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ lib_deps =
|
||||||
platform = native
|
platform = native
|
||||||
test_framework = unity
|
test_framework = unity
|
||||||
test_build_src = yes
|
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 =
|
build_flags =
|
||||||
-std=gnu++17
|
-std=gnu++17
|
||||||
-Isrc
|
-Isrc
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "ConfigFile.h"
|
#include "ConfigFile.h"
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
#include "SomfySnapshot.h"
|
#include "SomfySnapshot.h"
|
||||||
|
#include "SomfyEditor.h"
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
|
|
||||||
static const char *TAG = "LoopQueue";
|
static const char *TAG = "LoopQueue";
|
||||||
|
|
@ -66,6 +67,9 @@ void LoopQueue::drain() {
|
||||||
case loop_item_type_t::snapshot_fill:
|
case loop_item_type_t::snapshot_fill:
|
||||||
snapshotSlots.fill(item.slot, fillSnapshot);
|
snapshotSlots.fill(item.slot, fillSnapshot);
|
||||||
break;
|
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
|
shade_request, // run by ShadeRequests
|
||||||
restore_shades, // restore from /shades.tmp, then reboot
|
restore_shades, // restore from /shades.tmp, then reboot
|
||||||
load_shades, // replace the shades with /shades.tmp
|
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 {
|
struct loop_item_t {
|
||||||
loop_item_type_t type = loop_item_type_t::shade_request;
|
loop_item_type_t type = loop_item_type_t::shade_request;
|
||||||
shade_request_t request; // shade_request
|
shade_request_t request; // shade_request
|
||||||
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
||||||
uint8_t slot = 0; // snapshot_fill: the SnapshotSlots slot
|
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.
|
// 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 <string.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "ModelSnapshot.h"
|
#include "ModelSnapshot.h"
|
||||||
|
#include "ModelEditor.h"
|
||||||
|
|
||||||
static const char *TAG = "Snapshot";
|
static const char *TAG = "Snapshot";
|
||||||
|
|
||||||
|
|
@ -118,6 +119,35 @@ void writeGroup(JsonFormatter &json, const group_snapshot_t &group, shade_lookup
|
||||||
json.endArray();
|
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
|
* Documents
|
||||||
********************************************************************/
|
********************************************************************/
|
||||||
|
|
@ -136,6 +166,11 @@ enum : uint8_t {
|
||||||
ST_TO_GROUPS, // ],"groups":[
|
ST_TO_GROUPS, // ],"groups":[
|
||||||
ST_TO_REPEATERS, // ],"repeaters":[
|
ST_TO_REPEATERS, // ],"repeaters":[
|
||||||
ST_CLOSE_DOC, // ]}
|
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
|
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 };
|
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 ROOM_STEPS[] = { ST_ROOM, ST_END };
|
||||||
static const uint8_t SHADE_STEPS[] = { ST_SHADE, 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 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) {
|
static const uint8_t *stepsFor(snapshot_doc_t doc) {
|
||||||
switch(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::room: return ROOM_STEPS;
|
||||||
case snapshot_doc_t::shade: return SHADE_STEPS;
|
case snapshot_doc_t::shade: return SHADE_STEPS;
|
||||||
case snapshot_doc_t::group: return GROUP_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;
|
return SHADES_STEPS;
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +244,11 @@ void SnapshotWriter::literal(const char *text) {
|
||||||
this->unitLen = strlen(this->unit);
|
this->unitLen = strlen(this->unit);
|
||||||
this->nextStep();
|
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) {
|
void SnapshotWriter::resetFmt(size_t offset) {
|
||||||
this->unit[offset] = '\0';
|
this->unit[offset] = '\0';
|
||||||
this->fmt.setBuffer(this->unit + offset, this->unitSize - offset);
|
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.
|
// 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.
|
// 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]() {
|
auto done = [this, inList]() {
|
||||||
this->groupOpen = false;
|
this->groupOpen = false;
|
||||||
if(inList) this->idx++;
|
if(inList) this->idx++;
|
||||||
|
|
@ -241,8 +291,8 @@ void SnapshotWriter::groupUnit(const group_snapshot_t &group, bool inList) {
|
||||||
const shade_snapshot_t *shade = nullptr;
|
const shade_snapshot_t *shade = nullptr;
|
||||||
while(this->lsIdx < group.linkedShadeCount && !(shade = this->snap.shade(group.linkedShades[this->lsIdx]))) this->lsIdx++;
|
while(this->lsIdx < group.linkedShadeCount && !(shade = this->snap.shade(group.linkedShades[this->lsIdx]))) this->lsIdx++;
|
||||||
if(!shade) {
|
if(!shade) {
|
||||||
strcpy(this->unit, "]}");
|
strcpy(this->unit, close);
|
||||||
this->unitLen = 2;
|
this->unitLen = strlen(close);
|
||||||
done();
|
done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -335,6 +385,52 @@ void SnapshotWriter::produceUnit() {
|
||||||
this->groupUnit(*group, false);
|
this->groupUnit(*group, false);
|
||||||
return;
|
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_OPEN_ARRAY: this->literal("["); return;
|
||||||
case ST_CLOSE_ARRAY: this->literal("]"); return;
|
case ST_CLOSE_ARRAY: this->literal("]"); return;
|
||||||
case ST_TO_SHADES: this->literal("],\"shades\":["); 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];
|
model_snapshot_t &snap = *this->snaps[slot];
|
||||||
snap.startingAddress = 0;
|
snap.startingAddress = 0;
|
||||||
|
snap.radioBitLength = snap.radioProto = 0;
|
||||||
snap.roomCount = snap.shadeCount = snap.groupCount = snap.repeaterCount = 0;
|
snap.roomCount = snap.shadeCount = snap.groupCount = snap.repeaterCount = 0;
|
||||||
fill(snap);
|
fill(snap);
|
||||||
expected = FILLING;
|
expected = FILLING;
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ struct group_snapshot_t {
|
||||||
// Only the rooms, shades, groups and repeaters that exist, in model order.
|
// Only the rooms, shades, groups and repeaters that exist, in model order.
|
||||||
struct model_snapshot_t {
|
struct model_snapshot_t {
|
||||||
uint32_t startingAddress;
|
uint32_t startingAddress;
|
||||||
|
uint8_t radioBitLength; // what a new shade or group gets
|
||||||
|
uint8_t radioProto;
|
||||||
uint8_t roomCount;
|
uint8_t roomCount;
|
||||||
uint8_t shadeCount;
|
uint8_t shadeCount;
|
||||||
uint8_t groupCount;
|
uint8_t groupCount;
|
||||||
|
|
@ -115,7 +117,12 @@ enum class snapshot_doc_t : uint8_t {
|
||||||
groups, // /groups
|
groups, // /groups
|
||||||
room, // /room?roomId=
|
room, // /room?roomId=
|
||||||
shade, // /shade?shadeId=
|
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
|
// Writes the fields of /controller or /discovery that aren't in the snapshot
|
||||||
// (device, radio and firmware details) into the open top-level object.
|
// (device, radio and firmware details) into the open top-level object.
|
||||||
|
|
@ -154,8 +161,15 @@ class SnapshotWriter {
|
||||||
void resetFmt(size_t offset);
|
void resetFmt(size_t offset);
|
||||||
bool overflowed(const char *what, uint8_t itemId);
|
bool overflowed(const char *what, uint8_t itemId);
|
||||||
size_t itemStart();
|
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
|
// 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.
|
// task and queues a fill; loop() fills it; the read streams it and lets it go.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
#include "ConfigFile.h"
|
#include "ConfigFile.h"
|
||||||
#include "GitOTA.h"
|
#include "GitOTA.h"
|
||||||
|
#include "ModelEditor.h"
|
||||||
|
|
||||||
static const char *TAG = "Somfy";
|
static const char *TAG = "Somfy";
|
||||||
|
|
||||||
|
|
@ -117,17 +118,6 @@ void SomfyRoom::clear() {
|
||||||
this->roomId = 0;
|
this->roomId = 0;
|
||||||
strcpy(this->name, "");
|
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; }
|
bool SomfyRoom::save() { somfy.commit(); return true; }
|
||||||
void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) {
|
void SomfyShadeController::processFrame(somfy_frame_t &frame, bool internal) {
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
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++)
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||||
if(this->shades[i].getShadeId() != 255) this->shades[i].processWaitingFrame();
|
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() {
|
uint8_t SomfyShadeController::getNextShadeId() {
|
||||||
// There is no shortcut for this since the deletion of
|
uint8_t ids[SOMFY_MAX_SHADES];
|
||||||
// a shade in the middle makes all of this very difficult.
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) ids[i] = this->shades[i].getShadeId();
|
||||||
for(uint8_t i = 1; i < SOMFY_MAX_SHADES - 1; i++) {
|
const uint8_t id = firstFreeId(ids, SOMFY_MAX_SHADES, SOMFY_MAX_SHADES - 2);
|
||||||
bool id_exists = false;
|
return id == 0 ? 255 : id;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
int8_t SomfyShadeController::getMaxShadeOrder() {
|
int8_t SomfyShadeController::getMaxShadeOrder() {
|
||||||
int8_t order = -1;
|
int8_t orders[SOMFY_MAX_SHADES];
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
size_t count = 0;
|
||||||
SomfyShade *shade = &this->shades[i];
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||||
if(shade->getShadeId() == 255) continue;
|
if(this->shades[i].getShadeId() != 255) orders[count++] = this->shades[i].sortOrder;
|
||||||
if(order < shade->sortOrder) order = shade->sortOrder;
|
return nextSortOrder(orders, count) - 1;
|
||||||
}
|
|
||||||
return order;
|
|
||||||
}
|
}
|
||||||
int8_t SomfyShadeController::getMaxGroupOrder() {
|
int8_t SomfyShadeController::getMaxGroupOrder() {
|
||||||
int8_t order = -1;
|
int8_t orders[SOMFY_MAX_GROUPS];
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
size_t count = 0;
|
||||||
SomfyGroup *group = &this->groups[i];
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++)
|
||||||
if(group->getGroupId() == 255) continue;
|
if(this->groups[i].getGroupId() != 255) orders[count++] = this->groups[i].sortOrder;
|
||||||
if(order < group->sortOrder) order = group->sortOrder;
|
return nextSortOrder(orders, count) - 1;
|
||||||
}
|
|
||||||
return order;
|
|
||||||
}
|
}
|
||||||
uint8_t SomfyShadeController::getNextGroupId() {
|
uint8_t SomfyShadeController::getNextGroupId() {
|
||||||
// There is no shortcut for this since the deletion of
|
uint8_t ids[SOMFY_MAX_GROUPS];
|
||||||
// a group in the middle makes all of this very difficult.
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) ids[i] = this->groups[i].getGroupId();
|
||||||
for(uint8_t i = 1; i < SOMFY_MAX_GROUPS - 1; i++) {
|
const uint8_t id = firstFreeId(ids, SOMFY_MAX_GROUPS, SOMFY_MAX_GROUPS - 2);
|
||||||
bool id_exists = false;
|
return id == 0 ? 255 : id;
|
||||||
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 SomfyShadeController::getNextRoomId() {
|
uint8_t SomfyShadeController::getNextRoomId() {
|
||||||
// There is no shortcut for this since the deletion of
|
uint8_t ids[SOMFY_MAX_ROOMS];
|
||||||
// a room in the middle makes all of this very difficult.
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) ids[i] = this->rooms[i].roomId;
|
||||||
for(uint8_t i = 1; i < SOMFY_MAX_ROOMS - 1; i++) {
|
return firstFreeId(ids, SOMFY_MAX_ROOMS, SOMFY_MAX_ROOMS - 2);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
int8_t SomfyShadeController::getMaxRoomOrder() {
|
int8_t SomfyShadeController::getMaxRoomOrder() {
|
||||||
int8_t order = -1;
|
int8_t orders[SOMFY_MAX_ROOMS];
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
size_t count = 0;
|
||||||
SomfyRoom *room = &this->rooms[i];
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++)
|
||||||
if(room->roomId == 0) continue;
|
if(this->rooms[i].roomId != 0) orders[count++] = this->rooms[i].sortOrder;
|
||||||
if(order < room->sortOrder) order = room->sortOrder;
|
return nextSortOrder(orders, count) - 1;
|
||||||
}
|
|
||||||
return order;
|
|
||||||
}
|
}
|
||||||
uint8_t SomfyShadeController::repeaterCount() {
|
uint8_t SomfyShadeController::repeaterCount() {
|
||||||
uint8_t count = 0;
|
uint8_t count = 0;
|
||||||
|
|
@ -270,31 +216,6 @@ uint8_t SomfyShadeController::groupCount() {
|
||||||
}
|
}
|
||||||
return count;
|
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() {
|
SomfyShade *SomfyShadeController::addShade() {
|
||||||
Preferences pref;
|
Preferences pref;
|
||||||
uint8_t shadeId = this->getNextShadeId();
|
uint8_t shadeId = this->getNextShadeId();
|
||||||
|
|
@ -313,37 +234,15 @@ SomfyShade *SomfyShadeController::addShade() {
|
||||||
return shade;
|
return shade;
|
||||||
}
|
}
|
||||||
bool SomfyShadeController::unlinkRepeater(uint32_t address) {
|
bool SomfyShadeController::unlinkRepeater(uint32_t address) {
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) {
|
removeFromList(this->repeaters, SOMFY_MAX_REPEATERS, address);
|
||||||
if(this->repeaters[i] == address) this->repeaters[i] = 0;
|
|
||||||
}
|
|
||||||
this->compressRepeaters();
|
|
||||||
this->isDirty = true;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
SomfyRoom *SomfyShadeController::addRoom(JsonObject &obj) {
|
// False when every repeater slot is taken.
|
||||||
SomfyRoom *room = this->addRoom();
|
bool SomfyShadeController::linkRepeater(uint32_t address) {
|
||||||
if(room) {
|
if(!addToList(this->repeaters, SOMFY_MAX_REPEATERS, address)) return false;
|
||||||
room->fromJSON(obj);
|
this->isDirty = true;
|
||||||
room->save();
|
return true;
|
||||||
room->emitState("roomAdded");
|
|
||||||
}
|
|
||||||
return room;
|
|
||||||
}
|
}
|
||||||
SomfyRoom *SomfyShadeController::addRoom() {
|
SomfyRoom *SomfyShadeController::addRoom() {
|
||||||
uint8_t roomId = this->getNextRoomId();
|
uint8_t roomId = this->getNextRoomId();
|
||||||
|
|
@ -357,15 +256,6 @@ SomfyRoom *SomfyShadeController::addRoom() {
|
||||||
}
|
}
|
||||||
return room;
|
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() {
|
SomfyGroup *SomfyShadeController::addGroup() {
|
||||||
uint8_t groupId = this->getNextGroupId();
|
uint8_t groupId = this->getNextGroupId();
|
||||||
// So the next shade id will be the first one we run into with an id of 255 so
|
// 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 getMaxRoomOrder();
|
||||||
int8_t getMaxShadeOrder();
|
int8_t getMaxShadeOrder();
|
||||||
int8_t getMaxGroupOrder();
|
int8_t getMaxGroupOrder();
|
||||||
uint32_t getNextRemoteAddress(uint8_t shadeId);
|
|
||||||
SomfyShadeController();
|
SomfyShadeController();
|
||||||
Transceiver transceiver;
|
Transceiver transceiver;
|
||||||
SomfyRoom *addRoom();
|
SomfyRoom *addRoom();
|
||||||
SomfyRoom *addRoom(JsonObject &obj);
|
|
||||||
SomfyShade *addShade();
|
SomfyShade *addShade();
|
||||||
SomfyShade *addShade(JsonObject &obj);
|
|
||||||
SomfyGroup *addGroup();
|
SomfyGroup *addGroup();
|
||||||
SomfyGroup *addGroup(JsonObject &obj);
|
|
||||||
bool deleteRoom(uint8_t roomId);
|
bool deleteRoom(uint8_t roomId);
|
||||||
bool deleteShade(uint8_t shadeId);
|
bool deleteShade(uint8_t shadeId);
|
||||||
bool deleteGroup(uint8_t groupId);
|
bool deleteGroup(uint8_t groupId);
|
||||||
bool begin();
|
bool begin();
|
||||||
void loop();
|
void loop();
|
||||||
void end();
|
void end();
|
||||||
void compressRepeaters();
|
|
||||||
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
|
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0};
|
||||||
SomfyRoom rooms[SOMFY_MAX_ROOMS];
|
SomfyRoom rooms[SOMFY_MAX_ROOMS];
|
||||||
SomfyShade shades[SOMFY_MAX_SHADES];
|
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 "esp_log.h"
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
|
#include "ModelEditor.h"
|
||||||
|
|
||||||
static const char *TAG = "Somfy";
|
static const char *TAG = "Somfy";
|
||||||
|
|
||||||
|
|
@ -13,60 +14,12 @@ void SomfyGroup::clear() {
|
||||||
this->name[0] = 0x00;
|
this->name[0] = 0x00;
|
||||||
memset(&this->linkedShades, 0x00, sizeof(this->linkedShades));
|
memset(&this->linkedShades, 0x00, sizeof(this->linkedShades));
|
||||||
}
|
}
|
||||||
bool SomfyGroup::linkShade(uint8_t shadeId) {
|
// False when the group is full. The caller saves the change.
|
||||||
// Check to see if the shade is already linked. If it is just return true
|
bool SomfyGroup::linkShade(uint8_t shadeId) { return addToList(this->linkedShades, SOMFY_MAX_GROUPED_SHADES, shadeId); }
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
// False when the shade wasn't in the group. The caller saves the change.
|
||||||
if(this->linkedShades[i] == shadeId) {
|
bool SomfyGroup::unlinkShade(uint8_t shadeId) { return removeFromList(this->linkedShades, SOMFY_MAX_GROUPED_SHADES, shadeId); }
|
||||||
return true;
|
// 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); }
|
||||||
}
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bool SomfyGroup::hasShadeId(uint8_t shadeId) {
|
bool SomfyGroup::hasShadeId(uint8_t shadeId) {
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
for(uint8_t i = 0; i < SOMFY_MAX_GROUPED_SHADES; i++) {
|
||||||
if(this->linkedShades[i] == 0) break;
|
if(this->linkedShades[i] == 0) break;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "ConfigSettings.h"
|
#include "ConfigSettings.h"
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include "ModelEditor.h"
|
||||||
|
#include "SomfyEditor.h"
|
||||||
|
|
||||||
static const char *TAG = "Somfy";
|
static const char *TAG = "Somfy";
|
||||||
|
|
||||||
|
|
@ -36,50 +39,32 @@ int8_t SomfyShade::validateJSON(JsonObject &obj) {
|
||||||
type = shade_types::drycontact;
|
type = shade_types::drycontact;
|
||||||
}
|
}
|
||||||
else {
|
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()) {
|
// The pins and protocol the shade is about to have.
|
||||||
radio_proto proto = this->proto;
|
const radio_proto proto = !obj["proto"].isNull() ? static_cast<radio_proto>(obj["proto"].as<uint8_t>()) : this->proto;
|
||||||
if(proto == radio_proto::GP_Relay || proto == radio_proto::GP_Remote) {
|
const uint8_t upPin = !obj["gpioUp"].isNull() ? obj["gpioUp"].as<uint8_t>() : this->gpioUp;
|
||||||
// Check to see if we are using the up and or down
|
const uint8_t downPin = !obj["gpioDown"].isNull() ? obj["gpioDown"].as<uint8_t>() : this->gpioDown;
|
||||||
// GPIOs anywhere else.
|
const uint8_t myPin = !obj["gpioMy"].isNull() ? obj["gpioMy"].as<uint8_t>() : this->gpioMy;
|
||||||
uint8_t upPin = !obj["gpioUp"].isNull() ? obj["gpioUp"].as<uint8_t>() : this->gpioUp;
|
pin_users_t radio, ethernet, others;
|
||||||
uint8_t downPin = !obj["gpioDown"].isNull() ? obj["gpioDown"].as<uint8_t>() : this->gpioDown;
|
if(somfy.transceiver.config.enabled)
|
||||||
uint8_t myPin = !obj["gpioMy"].isNull() ? obj["gpioMy"].as<uint8_t>() : this->gpioMy;
|
radio.uses = [](uint8_t pin, void *) { return somfy.transceiver.usesPin(pin); };
|
||||||
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;
|
|
||||||
}
|
|
||||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||||
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref) {
|
if(settings.connType == conn_types_t::ethernet || settings.connType == conn_types_t::ethernetpref)
|
||||||
if((upPin != 255 && settings.Ethernet.usesPin(upPin)) ||
|
ethernet.uses = [](uint8_t pin, void *) { return settings.Ethernet.usesPin(pin); };
|
||||||
(downPin != 255 && somfy.transceiver.usesPin(downPin)) ||
|
|
||||||
(myPin != 255 && somfy.transceiver.usesPin(myPin)))
|
|
||||||
ret = -11;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
if(ret == 0) {
|
others.ctx = this;
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
others.uses = [](uint8_t pin, void *ctx) {
|
||||||
SomfyShade *shade = &somfy.shades[i];
|
const uint8_t self = static_cast<SomfyShade *>(ctx)->getShadeId();
|
||||||
if(shade->getShadeId() == this->getShadeId() || shade->getShadeId() == 255) continue;
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||||
if((upPin != 255 && shade->usesPin(upPin)) ||
|
SomfyShade &shade = somfy.shades[i];
|
||||||
(downPin != 255 && shade->usesPin(downPin)) ||
|
if(shade.getShadeId() == self || shade.getShadeId() == 255) continue;
|
||||||
(myPin != 255 && shade->usesPin(myPin))){
|
if(shade.usesPin(pin)) return true;
|
||||||
ret = -12;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
|
};
|
||||||
|
ret = gpioPinConflict(type, proto, upPin, downPin, myPin, radio, ethernet, others);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
int8_t SomfyShade::fromJSON(JsonObject &obj) {
|
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"];
|
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);
|
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) {
|
void fillSnapshot(model_snapshot_t &snap) {
|
||||||
snap.startingAddress = somfy.startingAddress;
|
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++)
|
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++)
|
||||||
if(somfy.rooms[i].roomId != 0) snapshotRoom(somfy.rooms[i], snap.rooms[snap.roomCount++]);
|
if(somfy.rooms[i].roomId != 0) snapshotRoom(somfy.rooms[i], snap.rooms[snap.roomCount++]);
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include "LoopQueue.h"
|
#include "LoopQueue.h"
|
||||||
#include "ModelSnapshot.h"
|
#include "ModelSnapshot.h"
|
||||||
#include "SomfySnapshot.h"
|
#include "SomfySnapshot.h"
|
||||||
|
#include "SomfyEditor.h"
|
||||||
#include "Web.h"
|
#include "Web.h"
|
||||||
#include "MQTT.h"
|
#include "MQTT.h"
|
||||||
#include "GitOTA.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
|
// 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
|
// read routes do. The read routes themselves write from a ModelSnapshot that
|
||||||
// loop() fills (sendSnapshot).
|
// 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) {
|
static void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) {
|
||||||
shade_snapshot_t s;
|
shade_snapshot_t s;
|
||||||
snapshotShade(*shade, s);
|
snapshotShade(*shade, s);
|
||||||
|
|
@ -240,11 +236,6 @@ static void serializeGroup(SomfyGroup *group, JsonFormatter &json) {
|
||||||
shade_snapshot_t linked;
|
shade_snapshot_t linked;
|
||||||
writeGroup(json, s, liveShade, &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) {
|
static void serializeTransceiverConfig(JsonFormatter &json) {
|
||||||
auto &cfg = somfy.transceiver.config;
|
auto &cfg = somfy.transceiver.config;
|
||||||
json.addElem("type", cfg.type);
|
json.addElem("type", cfg.type);
|
||||||
|
|
@ -385,6 +376,58 @@ static void sendSnapshot(AsyncWebServerRequest *request, snapshot_doc_t doc, uin
|
||||||
request->send(response);
|
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 --
|
// -- Async handler implementations --
|
||||||
void Web::handleDiscovery(AsyncWebServerRequest *request) {
|
void Web::handleDiscovery(AsyncWebServerRequest *request) {
|
||||||
if(request->method() == HTTP_POST || request->method() == HTTP_GET) {
|
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("/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("/group", WebRequestMethodComposite(HTTP_GET) | HTTP_POST, [](AsyncWebServerRequest *request) { webServer.handleGroup(request); });
|
||||||
|
|
||||||
asyncServer.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
// What a new room, shade or group would get, from a snapshot.
|
||||||
AsyncJsonResp resp;
|
asyncServer.on("/getNextRoom", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextRoom); });
|
||||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
asyncServer.on("/getNextShade", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextShade); });
|
||||||
resp.beginObject();
|
asyncServer.on("/getNextGroup", HTTP_GET, [](AsyncWebServerRequest *request) { sendSnapshot(request, snapshot_doc_t::nextGroup); });
|
||||||
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.\"}"));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
asyncServer.on("/groupOptions", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/groupOptions", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
if(asyncHasParam(request, "groupId")) {
|
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 {
|
|
||||||
request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"You must supply a valid group id.\"}"));
|
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
|
// setMyPosition - supports both GET with query params and POST/PUT with JSON body
|
||||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setMyPosition",
|
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/setMyPosition",
|
||||||
|
|
@ -1374,54 +1226,9 @@ void Web::begin() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// setPaired - supports both query params and JSON body
|
asyncServer.addHandler(editHandler("/setPaired", model_edit_type_t::set_paired));
|
||||||
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.on("/setPaired", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/setPaired", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint8_t shadeId = 255;
|
queueModelEditFromParams(request, model_edit_type_t::set_paired, { "shadeId", "paired" });
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// unpairShade
|
// unpairShade
|
||||||
|
|
@ -1467,310 +1274,35 @@ void Web::begin() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// linkRepeater
|
// Linking repeaters, remotes and group members, and deleting. Edits of the
|
||||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/linkRepeater",
|
// shade model: loop() applies them and sends requestResult.
|
||||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
asyncServer.addHandler(editHandler("/linkRepeater", model_edit_type_t::link_repeater));
|
||||||
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();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
asyncServer.on("/linkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/linkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint32_t address = 0;
|
queueModelEditFromParams(request, model_edit_type_t::link_repeater, { "address" });
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
asyncServer.addHandler(editHandler("/unlinkRepeater", model_edit_type_t::unlink_repeater));
|
||||||
// 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.on("/unlinkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/unlinkRepeater", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint32_t address = 0;
|
queueModelEditFromParams(request, model_edit_type_t::unlink_repeater, { "address" });
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
asyncServer.addHandler(editHandler("/linkRemote", model_edit_type_t::link_remote));
|
||||||
// unlinkRemote
|
asyncServer.addHandler(editHandler("/unlinkRemote", model_edit_type_t::unlink_remote));
|
||||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/unlinkRemote",
|
asyncServer.addHandler(editHandler("/linkToGroup", model_edit_type_t::link_to_group));
|
||||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
asyncServer.addHandler(editHandler("/unlinkFromGroup", model_edit_type_t::unlink_from_group));
|
||||||
if(json.isNull()) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No remote object supplied.\"}")); return; }
|
asyncServer.addHandler(editHandler("/deleteRoom", model_edit_type_t::delete_room));
|
||||||
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.on("/deleteRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/deleteRoom", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint8_t roomId = 0;
|
queueModelEditFromParams(request, model_edit_type_t::delete_room, { "roomId" });
|
||||||
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.\"}"));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
asyncServer.addHandler(editHandler("/deleteShade", model_edit_type_t::delete_shade));
|
||||||
// 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.on("/deleteShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/deleteShade", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint8_t shadeId = 255;
|
queueModelEditFromParams(request, model_edit_type_t::delete_shade, { "shadeId" });
|
||||||
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.\"}"));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
asyncServer.addHandler(editHandler("/deleteGroup", model_edit_type_t::delete_group));
|
||||||
// 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.on("/deleteGroup", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/deleteGroup", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
uint8_t groupId = 255;
|
queueModelEditFromParams(request, model_edit_type_t::delete_group, { "groupId" });
|
||||||
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.\"}"));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// 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
|
// updateFirmware - file upload
|
||||||
asyncServer.on("/updateFirmware", HTTP_POST,
|
asyncServer.on("/updateFirmware", HTTP_POST,
|
||||||
|
|
@ -2195,62 +1727,10 @@ void Web::begin() {
|
||||||
request->send(200, _encoding_json, g_async_content);
|
request->send(200, _encoding_json, g_async_content);
|
||||||
});
|
});
|
||||||
|
|
||||||
// roomSortOrder
|
// Sort orders: the ids in the order the client lists them.
|
||||||
asyncServer.addHandler(new AsyncCallbackJsonWebHandler("/roomSortOrder",
|
asyncServer.addHandler(editHandler("/roomSortOrder", model_edit_type_t::room_order));
|
||||||
[](AsyncWebServerRequest *request, JsonVariant &json) {
|
asyncServer.addHandler(editHandler("/shadeSortOrder", model_edit_type_t::shade_order));
|
||||||
if(json.isNull()) {
|
asyncServer.addHandler(editHandler("/groupSortOrder", model_edit_type_t::group_order));
|
||||||
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\"}");
|
|
||||||
}));
|
|
||||||
|
|
||||||
asyncServer.on("/beginFrequencyScan", HTTP_GET, [](AsyncWebServerRequest *request) {
|
asyncServer.on("/beginFrequencyScan", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||||
somfy.transceiver.beginFrequencyScan();
|
somfy.transceiver.beginFrequencyScan();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
#include "Somfy.h"
|
#include "Somfy.h"
|
||||||
|
#include "SomfyEditor.h"
|
||||||
#include "Sockets.h"
|
#include "Sockets.h"
|
||||||
#include "entryPoints/MQTT.h"
|
#include "entryPoints/MQTT.h"
|
||||||
|
|
||||||
|
|
@ -89,6 +90,18 @@ void SomfyGroup::emitState(uint8_t num, const char *evt) {
|
||||||
sockEmit.endEmit(num);
|
sockEmit.endEmit(num);
|
||||||
this->publish();
|
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) {
|
void SomfyShadeController::emitState(uint8_t num) {
|
||||||
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
|
||||||
SomfyShade *shade = &this->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());
|
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
|
* 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_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_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_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_two_reads_at_once_and_no_more);
|
||||||
RUN_TEST(test_a_read_is_filled_then_freed);
|
RUN_TEST(test_a_read_is_filled_then_freed);
|
||||||
RUN_TEST(test_a_read_gone_before_its_fill_is_not_filled);
|
RUN_TEST(test_a_read_gone_before_its_fill_is_not_filled);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue