Add shades, rooms, and groups to the first empty slot

Shades, rooms, and groups are kept in fixed-size arrays, and each record
has an id that the API, MQTT, and the Home Assistant integration use to
refer to it. `addShade`, `addRoom`, and `addGroup` give a new record the
lowest unused id and store it at index `id - 1`, which assumes a
record's index always matches its id.

That stops being true once a record has been deleted and the controller
restarts. The configuration file only holds the records that exist, and
loading it fills each array from the start, so every record after a gap
moves down a slot. Index `id - 1` for the lowest unused id then belongs
to the record that used to be one id higher, and adding a record
overwrites it. Each overwrite frees up another id, so every later add
replaces another record.

For example, with shades 1 through 5, deleting shade 3 and restarting
loads shades 4 and 5 into slots 2 and 3. Adding a shade then takes id 3
and replaces shade 4 in slot 2.

Store new records in the first empty slot instead. Lookups by id
already scan every slot, and restoring a backup fills the arrays the
same way booting does, so nothing else relies on a record's index
matching its id.

Fixes #494.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Evan Broder 2026-09-26 13:50:25 -07:00
parent eb75868adb
commit 4aad5e5a2e

View file

@ -3751,12 +3751,18 @@ SomfyShade *SomfyShadeController::addShade(JsonObject &obj) {
}
SomfyShade *SomfyShadeController::addShade() {
uint8_t shadeId = this->getNextShadeId();
// So the next shade id will be the first one we run into with an id of 255 so
// if it gets deleted in the middle then it will get the first slot that is empty.
// There is no apparent way around this. In the future we might actually add an indexer
// Shades are packed into the lowest slots when they are loaded, so once one has been
// deleted a shade's slot no longer matches its id.
// In the future we might actually add an indexer
// to it for sorting later. The time has come so the sort order is set below.
if(shadeId == 255) return nullptr;
SomfyShade *shade = &this->shades[shadeId - 1];
SomfyShade *shade = nullptr;
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++) {
if(this->shades[i].getShadeId() == 255) {
shade = &this->shades[i];
break;
}
}
if(shade) {
shade->setShadeId(shadeId);
shade->sortOrder = this->getMaxShadeOrder() + 1;
@ -3848,9 +3854,16 @@ SomfyRoom *SomfyShadeController::addRoom(JsonObject &obj) {
}
SomfyRoom *SomfyShadeController::addRoom() {
uint8_t roomId = this->getNextRoomId();
// So the next room id will be the first one we run into with an id of 0 so
// Rooms are packed into the lowest slots when they are loaded, so once one has been
// deleted a room's slot no longer matches its id.
if(roomId == 0) return nullptr;
SomfyRoom *room = &this->rooms[roomId - 1];
SomfyRoom *room = nullptr;
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
if(this->rooms[i].roomId == 0) {
room = &this->rooms[i];
break;
}
}
if(room) {
room->roomId = roomId;
room->sortOrder = this->getMaxRoomOrder() + 1;
@ -3870,12 +3883,18 @@ SomfyGroup *SomfyShadeController::addGroup(JsonObject &obj) {
}
SomfyGroup *SomfyShadeController::addGroup() {
uint8_t groupId = this->getNextGroupId();
// So the next shade id will be the first one we run into with an id of 255 so
// if it gets deleted in the middle then it will get the first slot that is empty.
// There is no apparent way around this. In the future we might actually add an indexer
// Groups are packed into the lowest slots when they are loaded, so once one has been
// deleted a group's slot no longer matches its id.
// In the future we might actually add an indexer
// to it for sorting later.
if(groupId == 255) return nullptr;
SomfyGroup *group = &this->groups[groupId - 1];
SomfyGroup *group = nullptr;
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
if(this->groups[i].getGroupId() == 255) {
group = &this->groups[i];
break;
}
}
if(group) {
group->setGroupId(groupId);
group->sortOrder = this->getMaxGroupOrder() + 1;