mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
Merge pull request #119 from cjkas/test/config-records
test: shades.cfg, backups, rolling codes, firmware update and MQTT discovery
This commit is contained in:
commit
ddbe106d8c
20 changed files with 2455 additions and 1084 deletions
|
|
@ -132,7 +132,7 @@ build_flags =
|
|||
platform = native
|
||||
test_framework = unity
|
||||
test_build_src = yes
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<ShadeBehaviour.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ItemSnapshot.cpp> +<ModelEditor.cpp> +<ShadeChanges.cpp> +<ProtoSchema.cpp> +<ProtoFrame.cpp> +<ProtoSync.cpp> +<ProtoParse.cpp> +<ProtoSlots.cpp> +<ProtoOps.cpp> +<ProtoEdits.cpp> +<ProtoSettings.cpp> +<ProtoAssembly.cpp>
|
||||
build_src_filter = -<*> +<Utils.cpp> +<ConfigRecords.cpp> +<MqttDisco.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<ShadeBehaviour.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp> +<ItemSnapshot.cpp> +<ModelEditor.cpp> +<ShadeChanges.cpp> +<ProtoSchema.cpp> +<ProtoFrame.cpp> +<ProtoSync.cpp> +<ProtoParse.cpp> +<ProtoSlots.cpp> +<ProtoOps.cpp> +<ProtoEdits.cpp> +<ProtoSettings.cpp> +<ProtoAssembly.cpp>
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@^7.2.2
|
||||
build_flags =
|
||||
|
|
|
|||
1184
src/ConfigFile.cpp
1184
src/ConfigFile.cpp
File diff suppressed because it is too large
Load diff
|
|
@ -3,72 +3,41 @@
|
|||
#include "Somfy.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "ConfigHeader.h"
|
||||
#include "ConfigRecords.h"
|
||||
|
||||
#ifndef configfile_h
|
||||
#define configfile_h
|
||||
|
||||
#define CFG_VALUE_SEP ','
|
||||
#define CFG_REC_END '\n'
|
||||
#define CFG_TOK_NONE 0x00
|
||||
#define CFG_TOK_QUOTE '"'
|
||||
|
||||
|
||||
class ConfigFile {
|
||||
// Opens shades.cfg or a backup and writes or reads its values
|
||||
// (ConfigRecords.h), from the file or into a String in RAM.
|
||||
class ConfigFile : public ConfigReader, public ConfigWriter {
|
||||
protected:
|
||||
File file;
|
||||
String *_ramBuf = nullptr;
|
||||
bool readOnly = false;
|
||||
bool begin(const char *filename, bool readOnly = false);
|
||||
uint32_t startRecPos = 0;
|
||||
bool _opened = false;
|
||||
public:
|
||||
config_header_t header;
|
||||
bool beginRAM(String *buf);
|
||||
void end();
|
||||
bool isOpen();
|
||||
bool seekRecordByIndex(uint16_t ndx);
|
||||
bool readHeader();
|
||||
bool seekChar(const char val);
|
||||
bool writeHeader(const config_header_t &header);
|
||||
bool writeHeader();
|
||||
bool writeSeparator();
|
||||
bool writeRecordEnd();
|
||||
bool writeChar(const char val);
|
||||
bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeBool(const bool val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP);
|
||||
bool readString(char *buff, size_t len);
|
||||
bool readVarString(char *buff, size_t len);
|
||||
bool skipValue(size_t len);
|
||||
bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP);
|
||||
bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP);
|
||||
char readChar(const char defVal = '\0');
|
||||
int8_t readInt8(const int8_t defVal = 0);
|
||||
uint8_t readUInt8(const uint8_t defVal = 0);
|
||||
uint16_t readUInt16(const uint16_t defVal = 0);
|
||||
uint32_t readUInt32(const uint32_t defVal = 0);
|
||||
bool readBool(const bool defVal = false);
|
||||
float readFloat(const float defVal = 0.00);
|
||||
bool readByte(uint8_t &val) override;
|
||||
uint32_t position() override;
|
||||
bool writeBytes(const uint8_t *buf, size_t len) override;
|
||||
};
|
||||
class ShadeConfigFile : public ConfigFile {
|
||||
protected:
|
||||
bool writeRepeaterRecord(SomfyShadeController *s);
|
||||
bool writeRoomRecord(SomfyRoom *room);
|
||||
bool writeShadeRecord(SomfyShade *shade);
|
||||
bool writeGroupRecord(SomfyGroup *group);
|
||||
bool writeSettingsRecord();
|
||||
bool writeNetRecord();
|
||||
bool writeTransRecord(transceiver_config_t &cfg);
|
||||
bool readRepeaterRecord(SomfyShadeController *s);
|
||||
bool readRoomRecord(SomfyRoom *room);
|
||||
bool readShadeRecord(SomfyShade *shade);
|
||||
bool readGroupRecord(SomfyGroup *group);
|
||||
bool readSettingsRecord();
|
||||
bool readNetRecord(restore_options_t &opts);
|
||||
bool readTransRecord(transceiver_config_t &cfg);
|
||||
// A restore takes the rolling codes in the file (rollingCodeAfterLoad).
|
||||
bool restoring = false;
|
||||
bool writeModel(SomfyShadeController *s);
|
||||
void readModel(SomfyShadeController *s);
|
||||
void readRoom(SomfyRoom *room);
|
||||
void readShade(SomfyShade *shade);
|
||||
void readGroup(SomfyGroup *group);
|
||||
void readRepeaters(SomfyShadeController *s);
|
||||
public:
|
||||
static bool exists();
|
||||
static bool load(SomfyShadeController *somfy, const char *filename = "/shades.cfg");
|
||||
|
|
|
|||
490
src/ConfigRecords.cpp
Normal file
490
src/ConfigRecords.cpp
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
#include "ConfigRecords.h"
|
||||
#include "Utils.h"
|
||||
|
||||
bool ConfigReader::next(uint8_t &val) {
|
||||
if(!this->readByte(val)) return false;
|
||||
this->_last = val;
|
||||
return true;
|
||||
}
|
||||
void ConfigReader::endRecord() {
|
||||
if(!this->atRecordEnd()) this->seekChar(CFG_REC_END);
|
||||
}
|
||||
bool ConfigReader::seekChar(const char val) {
|
||||
uint8_t ch;
|
||||
do {
|
||||
if(!this->next(ch)) return false;
|
||||
} while(ch != static_cast<uint8_t>(val));
|
||||
return true;
|
||||
}
|
||||
bool ConfigReader::readString(char *buff, size_t len) {
|
||||
memset(buff, 0x00, len);
|
||||
if(this->atRecordEnd()) return false;
|
||||
size_t i = 0;
|
||||
uint8_t val;
|
||||
while(this->next(val)) {
|
||||
if(val == CFG_REC_END || val == CFG_VALUE_SEP) {
|
||||
_rtrim(buff);
|
||||
return true;
|
||||
}
|
||||
if(i < len - 1) buff[i++] = val;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool ConfigReader::readFixedString(char *buff, size_t len) {
|
||||
memset(buff, 0x00, len);
|
||||
if(this->atRecordEnd()) return false;
|
||||
uint8_t val;
|
||||
for(size_t i = 0; i < len - 1; i++) {
|
||||
if(!this->next(val)) return false;
|
||||
if(val == CFG_REC_END) {
|
||||
_rtrim(buff);
|
||||
return true;
|
||||
}
|
||||
buff[i] = val;
|
||||
}
|
||||
// The separator after the padding.
|
||||
if(!this->next(val)) return false;
|
||||
_rtrim(buff);
|
||||
return true;
|
||||
}
|
||||
bool ConfigReader::readVarString(char *buff, size_t len) {
|
||||
memset(buff, 0x00, len);
|
||||
if(this->atRecordEnd()) return false;
|
||||
uint8_t quotes = 0;
|
||||
size_t i = 0;
|
||||
uint8_t val;
|
||||
while(this->next(val)) {
|
||||
if(val == CFG_REC_END || (val == CFG_VALUE_SEP && quotes >= 2)) {
|
||||
_rtrim(buff);
|
||||
return true;
|
||||
}
|
||||
if(val == CFG_TOK_QUOTE) {
|
||||
quotes++;
|
||||
continue;
|
||||
}
|
||||
if(i < len - 1) buff[i++] = val;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
int8_t ConfigReader::readInt8(const int8_t defVal) {
|
||||
char buff[5];
|
||||
if(this->readString(buff, sizeof(buff))) return static_cast<int8_t>(atoi(buff));
|
||||
return defVal;
|
||||
}
|
||||
uint8_t ConfigReader::readUInt8(const uint8_t defVal) {
|
||||
char buff[4];
|
||||
if(this->readString(buff, sizeof(buff))) return static_cast<uint8_t>(atoi(buff));
|
||||
return defVal;
|
||||
}
|
||||
uint16_t ConfigReader::readUInt16(const uint16_t defVal) {
|
||||
char buff[6];
|
||||
if(this->readString(buff, sizeof(buff))) return static_cast<uint16_t>(atoi(buff));
|
||||
return defVal;
|
||||
}
|
||||
uint32_t ConfigReader::readUInt32(const uint32_t defVal) {
|
||||
char buff[11];
|
||||
if(this->readString(buff, sizeof(buff))) return static_cast<uint32_t>(strtoul(buff, nullptr, 10));
|
||||
return defVal;
|
||||
}
|
||||
float ConfigReader::readFloat(const float defVal) {
|
||||
char buff[25];
|
||||
if(this->readString(buff, sizeof(buff))) return atof(buff);
|
||||
return defVal;
|
||||
}
|
||||
bool ConfigReader::readBool(const bool defVal) {
|
||||
char buff[6];
|
||||
if(this->readString(buff, sizeof(buff))) {
|
||||
switch(buff[0]) {
|
||||
case 't':
|
||||
case 'T':
|
||||
case '1':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return defVal;
|
||||
}
|
||||
|
||||
bool ConfigWriter::writeChar(const char val) {
|
||||
const uint8_t b = static_cast<uint8_t>(val);
|
||||
return this->writeBytes(&b, 1);
|
||||
}
|
||||
bool ConfigWriter::writeString(const char *val, size_t len, const char tok) {
|
||||
size_t slen = strlen(val);
|
||||
if(slen > 0 && !this->writeBytes(reinterpret_cast<const uint8_t *>(val), slen)) return false;
|
||||
// Pad the value to its fixed width.
|
||||
while(slen + 1 < len) {
|
||||
if(!this->writeChar(' ')) return false;
|
||||
slen++;
|
||||
}
|
||||
if(tok != CFG_TOK_NONE) return this->writeChar(tok);
|
||||
return true;
|
||||
}
|
||||
bool ConfigWriter::writeVarString(const char *val, const char tok) {
|
||||
this->writeChar(CFG_TOK_QUOTE);
|
||||
const size_t slen = strlen(val);
|
||||
if(slen > 0 && !this->writeBytes(reinterpret_cast<const uint8_t *>(val), slen)) return false;
|
||||
this->writeChar(CFG_TOK_QUOTE);
|
||||
if(tok != CFG_TOK_NONE) return this->writeChar(tok);
|
||||
return true;
|
||||
}
|
||||
bool ConfigWriter::writeInt8(const int8_t val, const char tok) {
|
||||
char buff[5];
|
||||
snprintf(buff, sizeof(buff), "%4d", val);
|
||||
return this->writeString(buff, sizeof(buff), tok);
|
||||
}
|
||||
bool ConfigWriter::writeUInt8(const uint8_t val, const char tok) {
|
||||
char buff[4];
|
||||
snprintf(buff, sizeof(buff), "%3u", val);
|
||||
return this->writeString(buff, sizeof(buff), tok);
|
||||
}
|
||||
bool ConfigWriter::writeUInt16(const uint16_t val, const char tok) {
|
||||
char buff[6];
|
||||
snprintf(buff, sizeof(buff), "%5u", val);
|
||||
return this->writeString(buff, sizeof(buff), tok);
|
||||
}
|
||||
bool ConfigWriter::writeUInt32(const uint32_t val, const char tok) {
|
||||
char buff[11];
|
||||
snprintf(buff, sizeof(buff), "%10u", static_cast<unsigned>(val));
|
||||
return this->writeString(buff, sizeof(buff), tok);
|
||||
}
|
||||
bool ConfigWriter::writeFloat(const float val, const uint8_t prec, const char tok) {
|
||||
char buff[20];
|
||||
snprintf(buff, sizeof(buff), "%*.*f", 7 + prec, prec, val);
|
||||
return this->writeString(buff, 8 + prec, tok);
|
||||
}
|
||||
bool ConfigWriter::writeBool(const bool val, const char tok) {
|
||||
return this->writeString(val ? "true" : "false", 6, tok);
|
||||
}
|
||||
|
||||
bool writeConfigHeader(ConfigWriter &w, const config_header_t &header) {
|
||||
w.writeUInt8(header.version);
|
||||
w.writeUInt8(header.length);
|
||||
w.writeUInt16(header.roomRecordSize);
|
||||
w.writeUInt8(header.roomRecords);
|
||||
w.writeUInt16(header.shadeRecordSize);
|
||||
w.writeUInt8(header.shadeRecords);
|
||||
w.writeUInt16(header.groupRecordSize);
|
||||
w.writeUInt8(header.groupRecords);
|
||||
w.writeUInt16(header.repeaterRecordSize);
|
||||
w.writeUInt8(header.repeaterRecords);
|
||||
w.writeUInt16(header.settingsRecordSize);
|
||||
w.writeUInt16(header.netRecordSize);
|
||||
w.writeUInt16(header.transRecordSize);
|
||||
return w.writeString(header.serverId, sizeof(header.serverId), CFG_REC_END);
|
||||
}
|
||||
bool readConfigHeader(ConfigReader &r, config_header_t &header) {
|
||||
r.beginRecord();
|
||||
header.version = r.readUInt8(header.version);
|
||||
header.length = r.readUInt8(0);
|
||||
if(header.version >= 19) {
|
||||
header.roomRecordSize = r.readUInt16(header.roomRecordSize);
|
||||
header.roomRecords = r.readUInt8(header.roomRecords);
|
||||
}
|
||||
if(header.version >= 13) header.shadeRecordSize = r.readUInt16(header.shadeRecordSize);
|
||||
else header.shadeRecordSize = r.readUInt8(static_cast<uint8_t>(header.shadeRecordSize));
|
||||
header.shadeRecords = r.readUInt8(header.shadeRecords);
|
||||
if(header.version > 10) {
|
||||
if(header.version >= 13) header.groupRecordSize = r.readUInt16(header.groupRecordSize);
|
||||
else header.groupRecordSize = r.readUInt8(header.groupRecordSize);
|
||||
header.groupRecords = r.readUInt8(header.groupRecords);
|
||||
}
|
||||
if(header.version >= 21) {
|
||||
header.repeaterRecordSize = r.readUInt16(header.repeaterRecordSize);
|
||||
header.repeaterRecords = r.readUInt8(header.repeaterRecords);
|
||||
}
|
||||
if(header.version > 13) {
|
||||
header.settingsRecordSize = r.readUInt16(header.settingsRecordSize);
|
||||
header.netRecordSize = r.readUInt16(header.netRecordSize);
|
||||
header.transRecordSize = r.readUInt16(header.transRecordSize);
|
||||
r.readString(header.serverId, sizeof(header.serverId));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeRoomRecord(ConfigWriter &w, const room_record_t &rec) {
|
||||
w.writeUInt8(rec.roomId);
|
||||
w.writeString(rec.name, sizeof(rec.name));
|
||||
return w.writeUInt8(static_cast<uint8_t>(rec.sortOrder), CFG_REC_END);
|
||||
}
|
||||
bool readRoomRecord(ConfigReader &r, room_record_t &rec) {
|
||||
r.beginRecord();
|
||||
rec.roomId = r.readUInt8(0);
|
||||
r.readFixedString(rec.name, sizeof(rec.name));
|
||||
rec.sortOrder = static_cast<int8_t>(r.readUInt8(rec.roomId - 1));
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeShadeRecord(ConfigWriter &w, const shade_record_t &rec) {
|
||||
w.writeUInt8(rec.shadeId);
|
||||
w.writeBool(rec.paired);
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.shadeType));
|
||||
w.writeUInt32(rec.remoteAddress);
|
||||
w.writeString(rec.name, sizeof(rec.name));
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.tiltType));
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.proto));
|
||||
w.writeUInt8(rec.bitLength);
|
||||
w.writeUInt32(rec.upTime);
|
||||
w.writeUInt32(rec.downTime);
|
||||
w.writeUInt32(rec.tiltTime);
|
||||
w.writeUInt16(rec.stepSize);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) w.writeUInt32(rec.linkedRemotes[j]);
|
||||
w.writeUInt16(rec.lastRollingCode);
|
||||
if(rec.shadeId != 255) {
|
||||
w.writeUInt8(rec.flags);
|
||||
w.writeFloat(rec.myPos, 5);
|
||||
w.writeFloat(rec.myTiltPos, 5);
|
||||
w.writeFloat(rec.currentPos, 5);
|
||||
w.writeFloat(rec.currentTiltPos, 5);
|
||||
}
|
||||
else {
|
||||
// A deleted shade is written cleared.
|
||||
w.writeUInt8(0);
|
||||
w.writeFloat(-1.0f, 5);
|
||||
w.writeFloat(-1.0f, 5);
|
||||
w.writeFloat(0.0f, 5);
|
||||
w.writeFloat(0.0f, 5);
|
||||
}
|
||||
w.writeBool(rec.flipCommands);
|
||||
w.writeBool(rec.flipPosition);
|
||||
w.writeUInt8(rec.repeats);
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.sortOrder));
|
||||
w.writeUInt8(rec.gpioUp);
|
||||
w.writeUInt8(rec.gpioDown);
|
||||
w.writeUInt8(rec.gpioMy);
|
||||
w.writeUInt8(rec.gpioFlags);
|
||||
return w.writeUInt8(rec.roomId, CFG_REC_END);
|
||||
}
|
||||
bool readShadeRecord(ConfigReader &r, const config_header_t &header, shade_record_t &rec) {
|
||||
const uint8_t ver = header.version;
|
||||
r.beginRecord();
|
||||
rec.shadeId = r.readUInt8(255);
|
||||
rec.paired = r.readBool(false);
|
||||
rec.shadeType = static_cast<shade_types>(r.readUInt8(0));
|
||||
rec.remoteAddress = r.readUInt32(0);
|
||||
r.readFixedString(rec.name, sizeof(rec.name));
|
||||
if(ver < 3) rec.tiltType = r.readBool(false) ? tilt_types::none : tilt_types::tiltmotor;
|
||||
else rec.tiltType = static_cast<tilt_types>(r.readUInt8(0));
|
||||
if(ver > 6) rec.proto = static_cast<radio_proto>(r.readUInt8(0));
|
||||
if(ver > 1) rec.bitLength = r.readUInt8(56);
|
||||
rec.upTime = r.readUInt32(rec.upTime);
|
||||
rec.downTime = r.readUInt32(rec.downTime);
|
||||
rec.tiltTime = r.readUInt32(rec.tiltTime);
|
||||
if(ver > 5) rec.stepSize = r.readUInt16(100);
|
||||
// Before version 5 a shade had 5 linked remotes.
|
||||
const uint8_t remotes = ver < 5 ? 5 : SOMFY_MAX_LINKED_REMOTES;
|
||||
for(uint8_t j = 0; j < remotes; j++) rec.linkedRemotes[j] = r.readUInt32(0);
|
||||
rec.lastRollingCode = r.readUInt16(0);
|
||||
if(ver > 7) rec.flags = r.readUInt8(0);
|
||||
if(ver < 4) rec.myPos = static_cast<float>(r.readUInt8(255));
|
||||
else {
|
||||
rec.myPos = r.readFloat(-1);
|
||||
rec.myTiltPos = r.readFloat(-1);
|
||||
}
|
||||
if(rec.myPos > 100 || rec.myPos < 0) rec.myPos = -1;
|
||||
if(rec.myTiltPos > 100 || rec.myTiltPos < 0) rec.myTiltPos = -1;
|
||||
rec.currentPos = r.readFloat(0);
|
||||
rec.currentTiltPos = r.readFloat(0);
|
||||
if(rec.tiltType == tilt_types::none || rec.shadeType != shade_types::blind) {
|
||||
rec.myTiltPos = -1;
|
||||
rec.currentTiltPos = 0;
|
||||
rec.tiltType = tilt_types::none;
|
||||
}
|
||||
if(ver < 3) {
|
||||
// Positions were fractions.
|
||||
rec.currentPos = rec.currentPos * 100;
|
||||
rec.currentTiltPos = rec.currentTiltPos * 100;
|
||||
}
|
||||
if(ver >= 9) rec.flipCommands = r.readBool(false);
|
||||
if(ver >= 10) rec.flipPosition = r.readBool(false);
|
||||
if(ver >= 12) rec.repeats = r.readUInt8(1);
|
||||
if(ver >= 13) rec.sortOrder = static_cast<int8_t>(r.readUInt8(rec.shadeId - 1));
|
||||
else rec.sortOrder = static_cast<int8_t>(rec.shadeId - 1);
|
||||
if(ver > 14) {
|
||||
rec.gpioUp = r.readUInt8(rec.gpioUp);
|
||||
rec.gpioDown = r.readUInt8(rec.gpioDown);
|
||||
}
|
||||
if(ver > 15) rec.gpioMy = r.readUInt8(rec.gpioMy);
|
||||
if(ver > 16) rec.gpioFlags = r.readUInt8(rec.gpioFlags);
|
||||
if(rec.shadeId != 255 && rec.tiltType == tilt_types::tiltonly) {
|
||||
// A tilt only shade is always all the way down.
|
||||
rec.currentPos = 100.0f;
|
||||
rec.myPos = 100.0f;
|
||||
}
|
||||
if(ver >= 19) rec.roomId = r.readUInt8(0);
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeGroupRecord(ConfigWriter &w, const group_record_t &rec) {
|
||||
w.writeUInt8(rec.groupId);
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.groupType));
|
||||
w.writeUInt32(rec.remoteAddress);
|
||||
w.writeString(rec.name, sizeof(rec.name));
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.proto));
|
||||
w.writeUInt8(rec.bitLength);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(rec.linkedShades[j]);
|
||||
w.writeUInt8(rec.repeats);
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.sortOrder));
|
||||
w.writeBool(rec.flipCommands);
|
||||
w.writeUInt8(rec.roomId);
|
||||
return w.writeUInt16(rec.lastRollingCode, CFG_REC_END);
|
||||
}
|
||||
bool readGroupRecord(ConfigReader &r, const config_header_t &header, group_record_t &rec) {
|
||||
const uint8_t ver = header.version;
|
||||
r.beginRecord();
|
||||
rec.groupId = r.readUInt8(255);
|
||||
rec.groupType = static_cast<group_types>(r.readUInt8(0));
|
||||
rec.remoteAddress = r.readUInt32(0);
|
||||
r.readFixedString(rec.name, sizeof(rec.name));
|
||||
rec.proto = static_cast<radio_proto>(r.readUInt8(0));
|
||||
rec.bitLength = r.readUInt8(56);
|
||||
// Version 23 put the rolling code here, version 24 at the end.
|
||||
if(ver == 23) rec.lastRollingCode = r.readUInt16(0);
|
||||
rec.hasRollingCode = ver >= 23;
|
||||
memset(rec.linkedShades, 0x00, sizeof(rec.linkedShades));
|
||||
uint8_t linked = 0;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) {
|
||||
const uint8_t shadeId = r.readUInt8(0);
|
||||
if(shadeId > 0) rec.linkedShades[linked++] = shadeId;
|
||||
}
|
||||
if(ver >= 12) rec.repeats = r.readUInt8(1);
|
||||
if(ver >= 13) rec.sortOrder = static_cast<int8_t>(r.readUInt8(rec.groupId - 1));
|
||||
else rec.sortOrder = static_cast<int8_t>(rec.groupId - 1);
|
||||
if(ver >= 18) rec.flipCommands = r.readBool(false);
|
||||
if(ver >= 19) rec.roomId = r.readUInt8(0);
|
||||
if(ver >= 24) rec.lastRollingCode = r.readUInt16(0);
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeRepeaterRecord(ConfigWriter &w, const uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++)
|
||||
w.writeUInt32(repeaters[i], i == SOMFY_MAX_REPEATERS - 1 ? CFG_REC_END : CFG_VALUE_SEP);
|
||||
return true;
|
||||
}
|
||||
bool readRepeaterRecord(ConfigReader &r, uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]) {
|
||||
r.beginRecord();
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_REPEATERS; i++) repeaters[i] = r.readUInt32(0);
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSettingsRecord(ConfigWriter &w, const settings_record_t &rec) {
|
||||
w.writeVarString(rec.fwVersion);
|
||||
w.writeVarString(rec.hostname);
|
||||
w.writeVarString(rec.ntpServer);
|
||||
w.writeVarString(rec.posixZone);
|
||||
w.writeBool(rec.ssdpBroadcast);
|
||||
return w.writeBool(rec.checkForUpdate, CFG_REC_END);
|
||||
}
|
||||
bool readSettingsRecord(ConfigReader &r, const config_header_t &header, settings_record_t &rec) {
|
||||
r.beginRecord();
|
||||
r.readVarString(rec.fwVersion, sizeof(rec.fwVersion));
|
||||
r.readVarString(rec.hostname, sizeof(rec.hostname));
|
||||
r.readVarString(rec.ntpServer, sizeof(rec.ntpServer));
|
||||
r.readVarString(rec.posixZone, sizeof(rec.posixZone));
|
||||
rec.ssdpBroadcast = r.readBool(false);
|
||||
if(header.version >= 20) rec.checkForUpdate = r.readBool(true);
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeNetRecord(ConfigWriter &w, const net_record_t &rec, bool withEthernet) {
|
||||
w.writeUInt8(rec.connType);
|
||||
w.writeBool(rec.dhcp);
|
||||
w.writeVarString(rec.ip);
|
||||
w.writeVarString(rec.gateway);
|
||||
w.writeVarString(rec.subnet);
|
||||
w.writeVarString(rec.dns1);
|
||||
w.writeVarString(rec.dns2);
|
||||
w.writeVarString(rec.mqttProtocol);
|
||||
w.writeVarString(rec.mqttHostname);
|
||||
w.writeUInt16(rec.mqttPort);
|
||||
w.writeBool(rec.mqttPubDisco);
|
||||
w.writeVarString(rec.mqttRootTopic);
|
||||
w.writeVarString(rec.mqttDiscoTopic);
|
||||
if(!withEthernet) return w.writeUInt8(0, CFG_REC_END);
|
||||
w.writeUInt8(rec.ethBoardType);
|
||||
w.writeUInt8(rec.ethPhyType);
|
||||
w.writeUInt8(rec.ethClkMode);
|
||||
w.writeInt8(rec.ethPhyAddress);
|
||||
w.writeInt8(rec.ethPWRPin);
|
||||
w.writeInt8(rec.ethMDCPin);
|
||||
return w.writeInt8(rec.ethMDIOPin, CFG_REC_END);
|
||||
}
|
||||
bool readNetRecord(ConfigReader &r, const config_header_t &header, net_record_t &rec, bool withEthernet) {
|
||||
r.beginRecord();
|
||||
rec.connType = r.readUInt8(0);
|
||||
rec.dhcp = r.readBool(true);
|
||||
r.readVarString(rec.ip, sizeof(rec.ip));
|
||||
r.readVarString(rec.gateway, sizeof(rec.gateway));
|
||||
r.readVarString(rec.subnet, sizeof(rec.subnet));
|
||||
r.readVarString(rec.dns1, sizeof(rec.dns1));
|
||||
r.readVarString(rec.dns2, sizeof(rec.dns2));
|
||||
if(header.version >= 22) {
|
||||
r.readVarString(rec.mqttProtocol, sizeof(rec.mqttProtocol));
|
||||
r.readVarString(rec.mqttHostname, sizeof(rec.mqttHostname));
|
||||
rec.mqttPort = r.readUInt16(1883);
|
||||
rec.mqttPubDisco = r.readBool(false);
|
||||
r.readVarString(rec.mqttRootTopic, sizeof(rec.mqttRootTopic));
|
||||
r.readVarString(rec.mqttDiscoTopic, sizeof(rec.mqttDiscoTopic));
|
||||
}
|
||||
if(withEthernet) {
|
||||
rec.ethBoardType = r.readUInt8(1);
|
||||
rec.ethPhyType = r.readUInt8(0);
|
||||
rec.ethClkMode = r.readUInt8(0);
|
||||
rec.ethPhyAddress = r.readInt8(1);
|
||||
rec.ethPWRPin = r.readInt8(1);
|
||||
rec.ethMDCPin = r.readInt8(16);
|
||||
rec.ethMDIOPin = r.readInt8(23);
|
||||
}
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeTransRecord(ConfigWriter &w, const trans_record_t &rec) {
|
||||
w.writeBool(rec.enabled);
|
||||
w.writeUInt8(static_cast<uint8_t>(rec.proto));
|
||||
w.writeUInt8(rec.type);
|
||||
w.writeUInt8(rec.SCKPin);
|
||||
w.writeUInt8(rec.CSNPin);
|
||||
w.writeUInt8(rec.MOSIPin);
|
||||
w.writeUInt8(rec.MISOPin);
|
||||
w.writeUInt8(rec.TXPin);
|
||||
w.writeUInt8(rec.RXPin);
|
||||
w.writeFloat(rec.frequency, 3);
|
||||
w.writeFloat(rec.rxBandwidth, 2);
|
||||
w.writeFloat(rec.deviation, 2);
|
||||
w.writeInt8(rec.txPower);
|
||||
return w.writeBool(rec.noiseDetection, CFG_REC_END);
|
||||
}
|
||||
bool readTransRecord(ConfigReader &r, trans_record_t &rec) {
|
||||
r.beginRecord();
|
||||
rec.enabled = r.readBool(false);
|
||||
rec.proto = static_cast<radio_proto>(r.readUInt8(0));
|
||||
rec.type = r.readUInt8(56);
|
||||
rec.SCKPin = r.readUInt8(rec.SCKPin);
|
||||
rec.CSNPin = r.readUInt8(rec.CSNPin);
|
||||
rec.MOSIPin = r.readUInt8(rec.MOSIPin);
|
||||
rec.MISOPin = r.readUInt8(rec.MISOPin);
|
||||
rec.TXPin = r.readUInt8(rec.TXPin);
|
||||
rec.RXPin = r.readUInt8(rec.RXPin);
|
||||
rec.frequency = r.readFloat(rec.frequency);
|
||||
rec.rxBandwidth = r.readFloat(rec.rxBandwidth);
|
||||
rec.deviation = r.readFloat(rec.deviation);
|
||||
rec.txPower = r.readInt8(rec.txPower);
|
||||
rec.noiseDetection = r.readBool(false);
|
||||
r.endRecord();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool skipRecords(ConfigReader &r, uint32_t count) {
|
||||
for(uint32_t i = 0; i < count; i++) {
|
||||
r.beginRecord();
|
||||
if(!r.seekChar(CFG_REC_END)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
216
src/ConfigRecords.h
Normal file
216
src/ConfigRecords.h
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#ifndef CONFIG_RECORDS_H
|
||||
#define CONFIG_RECORDS_H
|
||||
#include <Arduino.h>
|
||||
#include "ConfigHeader.h"
|
||||
#include "ShadeTypes.h"
|
||||
#include "SomfyFrame.h"
|
||||
|
||||
// The records of shades.cfg and of a backup, and how they are written and
|
||||
// read. The file is text: each value is padded to a fixed width and followed
|
||||
// by a comma, strings that vary in length are quoted, and each record ends
|
||||
// with a newline. Older versions of the file lack the values added since,
|
||||
// and read as their defaults. Kept free of the file system and the shade
|
||||
// model so every version can be unit tested on the PC (pio test -e native).
|
||||
|
||||
#define CFG_VALUE_SEP ','
|
||||
#define CFG_REC_END '\n'
|
||||
#define CFG_TOK_NONE 0x00
|
||||
#define CFG_TOK_QUOTE '"'
|
||||
|
||||
#define SHADE_HDR_VER 24
|
||||
#define SHADE_HDR_SIZE 76
|
||||
#define SHADE_REC_SIZE 276
|
||||
#define GROUP_REC_SIZE 200
|
||||
#define ROOM_REC_SIZE 29
|
||||
#define REPEATER_REC_SIZE 77
|
||||
// Files written before 4.0 declare the transceiver record 74 bytes: it was
|
||||
// left there when noiseDetection was added.
|
||||
#define TRANS_REC_SIZE 80
|
||||
|
||||
// The sizes of the strings, which are those of the fields they are read into.
|
||||
#define CFG_NAME_LEN 21
|
||||
#define CFG_VERSION_LEN 24
|
||||
#define CFG_HOSTNAME_LEN 32
|
||||
#define CFG_NTP_SERVER_LEN 65
|
||||
#define CFG_POSIX_ZONE_LEN 64
|
||||
#define CFG_IP_LEN 24
|
||||
#define CFG_MQTT_PROTOCOL_LEN 10
|
||||
#define CFG_MQTT_HOSTNAME_LEN 65
|
||||
#define CFG_MQTT_TOPIC_LEN 65
|
||||
|
||||
// Reads the values of a file. A record's values past its end read as their
|
||||
// defaults, so a record shorter than expected never reads into the next one.
|
||||
class ConfigReader {
|
||||
private:
|
||||
uint8_t _last = 0;
|
||||
bool next(uint8_t &val);
|
||||
public:
|
||||
virtual ~ConfigReader() {}
|
||||
// The next byte of the file; false at its end.
|
||||
virtual bool readByte(uint8_t &val) = 0;
|
||||
virtual uint32_t position() = 0;
|
||||
void beginRecord() { _last = 0; }
|
||||
// Moves past the rest of the record: the values a newer version added.
|
||||
void endRecord();
|
||||
bool atRecordEnd() const { return _last == CFG_REC_END; }
|
||||
bool seekChar(const char val);
|
||||
// A value up to the next separator, cut to fit buff.
|
||||
bool readString(char *buff, size_t len);
|
||||
// A value padded to len - 1 bytes, read whole so it may hold commas: a
|
||||
// name.
|
||||
bool readFixedString(char *buff, size_t len);
|
||||
// A quoted value of any length, cut to fit buff.
|
||||
bool readVarString(char *buff, size_t len);
|
||||
int8_t readInt8(const int8_t defVal = 0);
|
||||
uint8_t readUInt8(const uint8_t defVal = 0);
|
||||
uint16_t readUInt16(const uint16_t defVal = 0);
|
||||
uint32_t readUInt32(const uint32_t defVal = 0);
|
||||
bool readBool(const bool defVal = false);
|
||||
float readFloat(const float defVal = 0.00);
|
||||
};
|
||||
class ConfigWriter {
|
||||
public:
|
||||
virtual ~ConfigWriter() {}
|
||||
virtual bool writeBytes(const uint8_t *buf, size_t len) = 0;
|
||||
bool writeChar(const char val);
|
||||
bool writeString(const char *val, size_t len, const char tok = CFG_VALUE_SEP);
|
||||
bool writeVarString(const char *val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeInt8(const int8_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt8(const uint8_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt16(const uint16_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeUInt32(const uint32_t val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeBool(const bool val, const char tok = CFG_VALUE_SEP);
|
||||
bool writeFloat(const float val, const uint8_t prec, const char tok = CFG_VALUE_SEP);
|
||||
};
|
||||
// Counts what would be written: the size of a record that varies in length.
|
||||
class ConfigCounter : public ConfigWriter {
|
||||
public:
|
||||
uint32_t bytes = 0;
|
||||
bool writeBytes(const uint8_t *buf, size_t len) override { (void)buf; bytes += len; return true; }
|
||||
};
|
||||
|
||||
struct room_record_t {
|
||||
uint8_t roomId = 0;
|
||||
char name[CFG_NAME_LEN] = "";
|
||||
int8_t sortOrder = 0;
|
||||
};
|
||||
struct shade_record_t {
|
||||
uint8_t shadeId = 255;
|
||||
bool paired = false;
|
||||
shade_types shadeType = shade_types::roller;
|
||||
uint32_t remoteAddress = 0;
|
||||
char name[CFG_NAME_LEN] = "";
|
||||
tilt_types tiltType = tilt_types::none;
|
||||
radio_proto proto = radio_proto::RTS;
|
||||
uint8_t bitLength = 56;
|
||||
uint32_t upTime = 10000;
|
||||
uint32_t downTime = 10000;
|
||||
uint32_t tiltTime = 7000;
|
||||
uint16_t stepSize = 100;
|
||||
uint32_t linkedRemotes[SOMFY_MAX_LINKED_REMOTES] = {};
|
||||
uint16_t lastRollingCode = 0;
|
||||
uint8_t flags = 0;
|
||||
float myPos = -1;
|
||||
float myTiltPos = -1;
|
||||
float currentPos = 0;
|
||||
float currentTiltPos = 0;
|
||||
bool flipCommands = false;
|
||||
bool flipPosition = false;
|
||||
uint8_t repeats = 1;
|
||||
int8_t sortOrder = 0;
|
||||
uint8_t gpioUp = 0;
|
||||
uint8_t gpioDown = 0;
|
||||
uint8_t gpioMy = 0;
|
||||
uint8_t gpioFlags = 0;
|
||||
uint8_t roomId = 0;
|
||||
};
|
||||
struct group_record_t {
|
||||
uint8_t groupId = 255;
|
||||
group_types groupType = group_types::channel;
|
||||
uint32_t remoteAddress = 0;
|
||||
char name[CFG_NAME_LEN] = "";
|
||||
radio_proto proto = radio_proto::RTS;
|
||||
uint8_t bitLength = 56;
|
||||
// Read without the gaps the file may have.
|
||||
uint8_t linkedShades[SOMFY_MAX_GROUPED_SHADES] = {};
|
||||
uint8_t repeats = 1;
|
||||
int8_t sortOrder = 0;
|
||||
bool flipCommands = false;
|
||||
uint8_t roomId = 0;
|
||||
uint16_t lastRollingCode = 0;
|
||||
// Versions before 23 have none.
|
||||
bool hasRollingCode = false;
|
||||
};
|
||||
struct settings_record_t {
|
||||
// The firmware that wrote the backup. Not restored.
|
||||
char fwVersion[CFG_VERSION_LEN] = "";
|
||||
char hostname[CFG_HOSTNAME_LEN] = "";
|
||||
char ntpServer[CFG_NTP_SERVER_LEN] = "";
|
||||
char posixZone[CFG_POSIX_ZONE_LEN] = "";
|
||||
bool ssdpBroadcast = false;
|
||||
bool checkForUpdate = true;
|
||||
};
|
||||
struct net_record_t {
|
||||
uint8_t connType = 0;
|
||||
bool dhcp = true;
|
||||
char ip[CFG_IP_LEN] = "";
|
||||
char gateway[CFG_IP_LEN] = "";
|
||||
char subnet[CFG_IP_LEN] = "";
|
||||
char dns1[CFG_IP_LEN] = "";
|
||||
char dns2[CFG_IP_LEN] = "";
|
||||
char mqttProtocol[CFG_MQTT_PROTOCOL_LEN] = "";
|
||||
char mqttHostname[CFG_MQTT_HOSTNAME_LEN] = "";
|
||||
uint16_t mqttPort = 1883;
|
||||
bool mqttPubDisco = false;
|
||||
char mqttRootTopic[CFG_MQTT_TOPIC_LEN] = "";
|
||||
char mqttDiscoTopic[CFG_MQTT_TOPIC_LEN] = "";
|
||||
// Only boards with Ethernet write these, and only the board that wrote
|
||||
// them reads them back.
|
||||
uint8_t ethBoardType = 1;
|
||||
uint8_t ethPhyType = 0;
|
||||
uint8_t ethClkMode = 0;
|
||||
int8_t ethPhyAddress = 1;
|
||||
int8_t ethPWRPin = 1;
|
||||
int8_t ethMDCPin = 16;
|
||||
int8_t ethMDIOPin = 23;
|
||||
};
|
||||
struct trans_record_t {
|
||||
bool enabled = false;
|
||||
radio_proto proto = radio_proto::RTS;
|
||||
uint8_t type = 56;
|
||||
uint8_t SCKPin = 18;
|
||||
uint8_t CSNPin = 5;
|
||||
uint8_t MOSIPin = 23;
|
||||
uint8_t MISOPin = 19;
|
||||
uint8_t TXPin = 13;
|
||||
uint8_t RXPin = 12;
|
||||
float frequency = 433.42;
|
||||
float rxBandwidth = 99.97;
|
||||
float deviation = 47.60;
|
||||
int8_t txPower = 10;
|
||||
bool noiseDetection = false;
|
||||
};
|
||||
|
||||
// The readers leave alone what the record's version lacks, so a record that
|
||||
// starts as the shade, group or settings it is read into keeps their values.
|
||||
bool writeConfigHeader(ConfigWriter &w, const config_header_t &header);
|
||||
bool readConfigHeader(ConfigReader &r, config_header_t &header);
|
||||
bool writeRoomRecord(ConfigWriter &w, const room_record_t &rec);
|
||||
bool readRoomRecord(ConfigReader &r, room_record_t &rec);
|
||||
bool writeShadeRecord(ConfigWriter &w, const shade_record_t &rec);
|
||||
bool readShadeRecord(ConfigReader &r, const config_header_t &header, shade_record_t &rec);
|
||||
bool writeGroupRecord(ConfigWriter &w, const group_record_t &rec);
|
||||
bool readGroupRecord(ConfigReader &r, const config_header_t &header, group_record_t &rec);
|
||||
bool writeRepeaterRecord(ConfigWriter &w, const uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]);
|
||||
bool readRepeaterRecord(ConfigReader &r, uint32_t (&repeaters)[SOMFY_MAX_REPEATERS]);
|
||||
bool writeSettingsRecord(ConfigWriter &w, const settings_record_t &rec);
|
||||
bool readSettingsRecord(ConfigReader &r, const config_header_t &header, settings_record_t &rec);
|
||||
// withEthernet: the Ethernet settings, which a board without Ethernet leaves out.
|
||||
bool writeNetRecord(ConfigWriter &w, const net_record_t &rec, bool withEthernet);
|
||||
bool readNetRecord(ConfigReader &r, const config_header_t &header, net_record_t &rec, bool withEthernet);
|
||||
bool writeTransRecord(ConfigWriter &w, const trans_record_t &rec);
|
||||
bool readTransRecord(ConfigReader &r, trans_record_t &rec);
|
||||
// Moves past count records without reading them.
|
||||
bool skipRecords(ConfigReader &r, uint32_t count);
|
||||
|
||||
#endif
|
||||
|
|
@ -233,36 +233,6 @@ void ConfigSettings::print() {
|
|||
if(this->connType == conn_types_t::ethernet || this->connType == conn_types_t::ethernetpref) this->Ethernet.print();
|
||||
#endif
|
||||
}
|
||||
uint16_t ConfigSettings::calcSettingsRecSize() {
|
||||
return strlen(this->fwVersion.name) + 3
|
||||
+ strlen(this->hostname) + 3
|
||||
+ strlen(this->NTP.ntpServer) + 3
|
||||
+ strlen(this->NTP.posixZone) + 3
|
||||
+ 6 // ssdpbroadcast
|
||||
+ 6; // updateCheck
|
||||
}
|
||||
uint16_t ConfigSettings::calcNetRecSize() {
|
||||
return 4 // connType
|
||||
+ 6 // dhcp
|
||||
+ this->IP.ip.toString().length() + 3
|
||||
+ this->IP.gateway.toString().length() + 3
|
||||
+ this->IP.subnet.toString().length() + 3
|
||||
+ this->IP.dns1.toString().length() + 3
|
||||
+ this->IP.dns2.toString().length() + 3
|
||||
+ strlen(this->MQTT.protocol) + 3
|
||||
+ strlen(this->MQTT.hostname) + 3
|
||||
+ 6 // MQTT Port
|
||||
+ 6 // PubDisco
|
||||
+ strlen(this->MQTT.rootTopic) + 3
|
||||
+ strlen(this->MQTT.discoTopic) + 3
|
||||
+ 4 // ETH.boardType
|
||||
+ 4 // ETH.phyType
|
||||
+ 4 // ETH.clkMode
|
||||
+ 5 // ETH.phyAddress
|
||||
+ 5 // ETH.PWRPin
|
||||
+ 5 // ETH.MDCPin
|
||||
+ 5; // ETH.MDIOPin
|
||||
}
|
||||
bool MQTTSettings::begin() {
|
||||
this->load();
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -189,8 +189,6 @@ class ConfigSettings: BaseSettings {
|
|||
bool save();
|
||||
bool load();
|
||||
void print();
|
||||
uint16_t calcSettingsRecSize();
|
||||
uint16_t calcNetRecSize();
|
||||
bool getAppVersion();
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
91
src/MqttDisco.cpp
Normal file
91
src/MqttDisco.cpp
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#include "MqttDisco.h"
|
||||
|
||||
bool isDiscoSwitch(shade_types type) { return type == shade_types::drycontact || type == shade_types::drycontact2; }
|
||||
void shadeDiscoTopic(const char *discoTopic, uint8_t shadeId, bool isSwitch, char *topic, size_t len) {
|
||||
snprintf(topic, len, "%s/%s/%d/config", discoTopic, isSwitch ? "switch" : "cover", shadeId);
|
||||
}
|
||||
static const char *deviceClass(shade_types type) {
|
||||
switch(type) {
|
||||
case shade_types::blind:
|
||||
return "blind";
|
||||
case shade_types::lgate:
|
||||
case shade_types::cgate:
|
||||
case shade_types::rgate:
|
||||
case shade_types::lgate1:
|
||||
case shade_types::cgate1:
|
||||
case shade_types::rgate1:
|
||||
case shade_types::ldrapery:
|
||||
case shade_types::rdrapery:
|
||||
case shade_types::cdrapery:
|
||||
return "curtain";
|
||||
case shade_types::garage1:
|
||||
case shade_types::garage3:
|
||||
return "garage";
|
||||
case shade_types::awning:
|
||||
return "awning";
|
||||
case shade_types::shutter:
|
||||
return "shutter";
|
||||
default:
|
||||
return "shade";
|
||||
}
|
||||
}
|
||||
void shadeDisco(const shade_disco_t &shade, JsonObject obj, char *topic, size_t len) {
|
||||
snprintf(topic, len, "%s/shades/%d", shade.rootTopic, shade.shadeId);
|
||||
obj["~"] = topic;
|
||||
JsonObject dobj = obj["device"].to<JsonObject>();
|
||||
dobj["hw_version"] = shade.fwVersion;
|
||||
dobj["name"] = shade.hostname;
|
||||
dobj["mf"] = "rstrouse";
|
||||
JsonArray arrids = dobj["identifiers"].to<JsonArray>();
|
||||
snprintf(topic, len, "mqtt_espsomfyrts_%s", shade.serverId);
|
||||
arrids.add(topic);
|
||||
dobj["model"] = "ESPSomfy-RTS MQTT";
|
||||
snprintf(topic, len, "%s/status", shade.rootTopic);
|
||||
obj["availability_topic"] = topic;
|
||||
obj["payload_available"] = "online";
|
||||
obj["payload_not_available"] = "offline";
|
||||
obj["name"] = shade.name;
|
||||
snprintf(topic, len, "mqtt_%s_shade%d", shade.serverId, shade.shadeId);
|
||||
obj["unique_id"] = topic;
|
||||
const bool isSwitch = isDiscoSwitch(shade.shadeType);
|
||||
if(!isSwitch) {
|
||||
obj["device_class"] = deviceClass(shade.shadeType);
|
||||
// Positions count down from open: 0 is open and 100 closed, so an
|
||||
// upward move (-1) opens. An awning, which opens downward, and a
|
||||
// flipped position turn that around.
|
||||
const bool reversed = shade.flipPosition != (shade.shadeType == shade_types::awning);
|
||||
obj["payload_close"] = reversed ? "-1" : "1";
|
||||
obj["payload_open"] = reversed ? "1" : "-1";
|
||||
obj["position_open"] = reversed ? 100 : 0;
|
||||
obj["position_closed"] = reversed ? 0 : 100;
|
||||
obj["state_closing"] = reversed ? "-1" : "1";
|
||||
obj["state_opening"] = reversed ? "1" : "-1";
|
||||
if(shade.tiltType != tilt_types::tiltonly) {
|
||||
obj["command_topic"] = "~/direction/set";
|
||||
obj["position_topic"] = "~/position";
|
||||
obj["set_position_topic"] = "~/target/set";
|
||||
obj["state_topic"] = "~/direction";
|
||||
obj["payload_stop"] = "0";
|
||||
obj["state_stopped"] = "0";
|
||||
}
|
||||
else {
|
||||
obj["payload_close"] = nullptr;
|
||||
obj["payload_open"] = nullptr;
|
||||
obj["payload_stop"] = nullptr;
|
||||
}
|
||||
if(shade.tiltType != tilt_types::none) {
|
||||
obj["tilt_command_topic"] = "~/tiltTarget/set";
|
||||
obj["tilt_status_topic"] = "~/tiltPosition";
|
||||
}
|
||||
}
|
||||
else {
|
||||
obj["payload_on"] = 100;
|
||||
obj["payload_off"] = 0;
|
||||
obj["state_off"] = 0;
|
||||
obj["state_on"] = 100;
|
||||
obj["state_topic"] = "~/position";
|
||||
obj["command_topic"] = "~/target/set";
|
||||
}
|
||||
obj["enabled_by_default"] = true;
|
||||
shadeDiscoTopic(shade.discoTopic, shade.shadeId, isSwitch, topic, len);
|
||||
}
|
||||
29
src/MqttDisco.h
Normal file
29
src/MqttDisco.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#ifndef MQTT_DISCO_H
|
||||
#define MQTT_DISCO_H
|
||||
#include <ArduinoJson.h>
|
||||
#include "ShadeTypes.h"
|
||||
|
||||
// What Home Assistant's MQTT discovery needs to know about a shade. Kept
|
||||
// free of the shade model and MQTT so the config it gets can be unit tested
|
||||
// on the PC (pio test -e native).
|
||||
struct shade_disco_t {
|
||||
uint8_t shadeId = 0;
|
||||
const char *name = "";
|
||||
shade_types shadeType = shade_types::roller;
|
||||
tilt_types tiltType = tilt_types::none;
|
||||
bool flipPosition = false;
|
||||
// From the MQTT and general settings.
|
||||
const char *rootTopic = "";
|
||||
const char *discoTopic = "";
|
||||
const char *serverId = "";
|
||||
const char *hostname = "";
|
||||
const char *fwVersion = "";
|
||||
};
|
||||
// Dry contacts are switches, the rest covers.
|
||||
bool isDiscoSwitch(shade_types type);
|
||||
// Where the shade's config goes: <discoTopic>/cover/<id>/config or
|
||||
// <discoTopic>/switch/<id>/config.
|
||||
void shadeDiscoTopic(const char *discoTopic, uint8_t shadeId, bool isSwitch, char *topic, size_t len);
|
||||
// Fills obj with the shade's config and topic with where it goes.
|
||||
void shadeDisco(const shade_disco_t &shade, JsonObject obj, char *topic, size_t len);
|
||||
#endif
|
||||
19
src/MqttTopics.h
Normal file
19
src/MqttTopics.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef MQTT_TOPICS_H
|
||||
#define MQTT_TOPICS_H
|
||||
#include <stddef.h>
|
||||
|
||||
// The topics the broker keeps for a shade (shades/<id>/) and a group
|
||||
// (groups/<id>/), which deleting one clears. Kept free of MQTT so the lists
|
||||
// can be checked against what is published in the unit tests on the PC
|
||||
// (pio test -e native).
|
||||
inline constexpr const char *SHADE_TOPICS[] = {
|
||||
"shadeId", "name", "remoteAddress", "shadeType", "tiltType", "flags", "flipCommands", "flipPosition",
|
||||
"position", "direction", "target", "lastRollingCode", "mypos", "myTiltPos",
|
||||
"tiltDirection", "tiltPosition", "tiltTarget", "windy", "sunny"
|
||||
};
|
||||
inline constexpr const char *GROUP_TOPICS[] = {
|
||||
"groupId", "name", "remoteAddress", "groupType", "direction", "lastRollingCode", "flags", "sunSensor", "flipCommands"
|
||||
};
|
||||
inline constexpr size_t SHADE_TOPIC_COUNT = sizeof(SHADE_TOPICS) / sizeof(SHADE_TOPICS[0]);
|
||||
inline constexpr size_t GROUP_TOPIC_COUNT = sizeof(GROUP_TOPICS) / sizeof(GROUP_TOPICS[0]);
|
||||
#endif
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
#define ROLLING_CODE_H
|
||||
#include <stdint.h>
|
||||
|
||||
// Working out a remote's next rolling code (src/SomfyRemote.cpp). Kept free of NVS
|
||||
// Working out a remote's next rolling code (src/SomfyRemote.cpp) and the one
|
||||
// it goes on with after a load (src/ConfigFile.cpp). Kept free of NVS
|
||||
// so it can be unit tested on the PC (pio test -e native).
|
||||
|
||||
// The code the next frame uses. It is only read here and consumed once the
|
||||
|
|
@ -13,4 +14,14 @@ template<typename LoadStored>
|
|||
inline uint16_t nextRollingCode(bool synced, uint16_t last, LoadStored loadStored) {
|
||||
return (uint16_t)((synced ? last : loadStored()) + 1);
|
||||
}
|
||||
// The rolling code a remote goes on with after shades.cfg or a backup is
|
||||
// read. NVS holds the last code sent and shades.cfg the code when it was
|
||||
// saved, so a load keeps the higher. A restore takes the backup's code even
|
||||
// when it is lower. A motor ignores codes below the last one it heard, so
|
||||
// after restoring an older backup to the same device it ignores the remote
|
||||
// until the codes pass that one.
|
||||
inline uint16_t rollingCodeAfterLoad(uint16_t stored, uint16_t inFile, bool restoring) {
|
||||
if(restoring) return inFile;
|
||||
return stored > inFile ? stored : inFile;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include "Somfy.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "MqttConnection.h"
|
||||
#include "MqttDisco.h"
|
||||
#include "MqttTopics.h"
|
||||
|
||||
extern SomfyShadeController somfy;
|
||||
extern ConfigSettings settings;
|
||||
|
|
@ -48,140 +50,30 @@ void SomfyShade::publishState() {
|
|||
snapshotShade(*this, s);
|
||||
publishShadeTopics(shadeStateTopics(s), s, publishShadeTopic, this);
|
||||
}
|
||||
// The shade's config for Home Assistant's MQTT discovery.
|
||||
void SomfyShade::publishDisco() {
|
||||
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
||||
shade_disco_t disco;
|
||||
disco.shadeId = this->shadeId;
|
||||
disco.name = this->name;
|
||||
disco.shadeType = this->shadeType;
|
||||
disco.tiltType = this->tiltType;
|
||||
disco.flipPosition = this->flipPosition;
|
||||
disco.rootTopic = settings.MQTT.rootTopic;
|
||||
disco.discoTopic = settings.MQTT.discoTopic;
|
||||
disco.serverId = settings.serverId;
|
||||
disco.hostname = settings.hostname;
|
||||
disco.fwVersion = settings.fwVersion.name;
|
||||
char topic[128] = "";
|
||||
JsonDocument doc;
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
snprintf(topic, sizeof(topic), "%s/shades/%d", settings.MQTT.rootTopic, this->shadeId);
|
||||
obj["~"] = topic;
|
||||
JsonObject dobj = obj.createNestedObject("device");
|
||||
dobj["hw_version"] = settings.fwVersion.name;
|
||||
dobj["name"] = settings.hostname;
|
||||
dobj["mf"] = "rstrouse";
|
||||
JsonArray arrids = dobj.createNestedArray("identifiers");
|
||||
//snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s_shade%d", settings.serverId, this->shadeId);
|
||||
snprintf(topic, sizeof(topic), "mqtt_espsomfyrts_%s", settings.serverId);
|
||||
arrids.add(topic);
|
||||
//snprintf(topic, sizeof(topic), "ESPSomfy-RTS_%s", settings.serverId);
|
||||
dobj["model"] = "ESPSomfy-RTS MQTT";
|
||||
snprintf(topic, sizeof(topic), "%s/status", settings.MQTT.rootTopic);
|
||||
obj["availability_topic"] = topic;
|
||||
obj["payload_available"] = "online";
|
||||
obj["payload_not_available"] = "offline";
|
||||
obj["name"] = this->name;
|
||||
snprintf(topic, sizeof(topic), "mqtt_%s_shade%d", settings.serverId, this->shadeId);
|
||||
obj["unique_id"] = topic;
|
||||
switch(this->shadeType) {
|
||||
case shade_types::blind:
|
||||
obj["device_class"] = "blind";
|
||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
||||
break;
|
||||
case shade_types::lgate:
|
||||
case shade_types::cgate:
|
||||
case shade_types::rgate:
|
||||
case shade_types::lgate1:
|
||||
case shade_types::cgate1:
|
||||
case shade_types::rgate1:
|
||||
case shade_types::ldrapery:
|
||||
case shade_types::rdrapery:
|
||||
case shade_types::cdrapery:
|
||||
obj["device_class"] = "curtain";
|
||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
||||
break;
|
||||
case shade_types::garage1:
|
||||
case shade_types::garage3:
|
||||
obj["device_class"] = "garage";
|
||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
||||
break;
|
||||
case shade_types::awning:
|
||||
obj["device_class"] = "awning";
|
||||
obj["payload_close"] = this->flipPosition ? "1" : "-1";
|
||||
obj["payload_open"] = this->flipPosition ? "-1" : "1";
|
||||
obj["position_open"] = this->flipPosition ? 0 : 100;
|
||||
obj["position_closed"] = this->flipPosition ? 100 : 0;
|
||||
obj["state_closing"] = this->flipPosition ? "1" : "-1";
|
||||
obj["state_opening"] = this->flipPosition ? "-1" : "1";
|
||||
break;
|
||||
case shade_types::shutter:
|
||||
obj["device_class"] = "shutter";
|
||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
||||
break;
|
||||
case shade_types::drycontact2:
|
||||
case shade_types::drycontact:
|
||||
break;
|
||||
default:
|
||||
obj["device_class"] = "shade";
|
||||
obj["payload_close"] = this->flipPosition ? "-1" : "1";
|
||||
obj["payload_open"] = this->flipPosition ? "1" : "-1";
|
||||
obj["position_open"] = this->flipPosition ? 100 : 0;
|
||||
obj["position_closed"] = this->flipPosition ? 0 : 100;
|
||||
obj["state_closing"] = this->flipPosition ? "-1" : "1";
|
||||
obj["state_opening"] = this->flipPosition ? "1" : "-1";
|
||||
break;
|
||||
}
|
||||
if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::drycontact2) {
|
||||
if(this->tiltType != tilt_types::tiltonly) {
|
||||
obj["command_topic"] = "~/direction/set";
|
||||
obj["position_topic"] = "~/position";
|
||||
obj["set_position_topic"] = "~/target/set";
|
||||
obj["state_topic"] = "~/direction";
|
||||
obj["payload_stop"] = "0";
|
||||
obj["state_stopped"] = "0";
|
||||
}
|
||||
else {
|
||||
obj["payload_close"] = nullptr;
|
||||
obj["payload_open"] = nullptr;
|
||||
obj["payload_stop"] = nullptr;
|
||||
}
|
||||
|
||||
if(this->tiltType != tilt_types::none) {
|
||||
obj["tilt_command_topic"] = "~/tiltTarget/set";
|
||||
obj["tilt_status_topic"] = "~/tiltPosition";
|
||||
}
|
||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
||||
}
|
||||
else {
|
||||
obj["payload_on"] = 100;
|
||||
obj["payload_off"] = 0;
|
||||
obj["state_off"] = 0;
|
||||
obj["state_on"] = 100;
|
||||
obj["state_topic"] = "~/position";
|
||||
obj["command_topic"] = "~/target/set";
|
||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
||||
}
|
||||
|
||||
obj["enabled_by_default"] = true;
|
||||
shadeDisco(disco, obj, topic, sizeof(topic));
|
||||
mqtt.publishDisco(topic, obj, true);
|
||||
}
|
||||
void SomfyShade::unpublishDisco() {
|
||||
if(!mqtt.connected() || !settings.MQTT.pubDisco) return;
|
||||
char topic[128] = "";
|
||||
if(this->shadeType != shade_types::drycontact && this->shadeType != shade_types::drycontact2) {
|
||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
||||
}
|
||||
else
|
||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, this->shadeId);
|
||||
shadeDiscoTopic(settings.MQTT.discoTopic, this->shadeId, isDiscoSwitch(this->shadeType), topic, sizeof(topic));
|
||||
mqtt.unpublish(topic);
|
||||
}
|
||||
void SomfyShade::publish() {
|
||||
|
|
@ -227,45 +119,19 @@ void SomfyGroup::unpublish() { SomfyGroup::unpublish(this->groupId); }
|
|||
void SomfyShade::unpublish() { SomfyShade::unpublish(this->shadeId); }
|
||||
void SomfyShade::unpublish(uint8_t id) {
|
||||
if(mqtt.connected()) {
|
||||
SomfyShade::unpublish(id, "shadeId");
|
||||
SomfyShade::unpublish(id, "name");
|
||||
SomfyShade::unpublish(id, "remoteAddress");
|
||||
SomfyShade::unpublish(id, "shadeType");
|
||||
SomfyShade::unpublish(id, "tiltType");
|
||||
SomfyShade::unpublish(id, "flags");
|
||||
SomfyShade::unpublish(id, "flipCommands");
|
||||
SomfyShade::unpublish(id, "flipPosition");
|
||||
SomfyShade::unpublish(id, "position");
|
||||
SomfyShade::unpublish(id, "direction");
|
||||
SomfyShade::unpublish(id, "target");
|
||||
SomfyShade::unpublish(id, "lastRollingCode");
|
||||
SomfyShade::unpublish(id, "mypos");
|
||||
SomfyShade::unpublish(id, "myTiltPos");
|
||||
SomfyShade::unpublish(id, "tiltDirection");
|
||||
SomfyShade::unpublish(id, "tiltPosition");
|
||||
SomfyShade::unpublish(id, "tiltTarget");
|
||||
SomfyShade::unpublish(id, "windy");
|
||||
SomfyShade::unpublish(id, "sunny");
|
||||
for(const char *topic : SHADE_TOPICS) SomfyShade::unpublish(id, topic);
|
||||
if(settings.MQTT.pubDisco) {
|
||||
char topic[128] = "";
|
||||
snprintf(topic, sizeof(topic), "%s/cover/%d/config", settings.MQTT.discoTopic, id);
|
||||
shadeDiscoTopic(settings.MQTT.discoTopic, id, false, topic, sizeof(topic));
|
||||
mqtt.unpublish(topic);
|
||||
snprintf(topic, sizeof(topic), "%s/switch/%d/config", settings.MQTT.discoTopic, id);
|
||||
shadeDiscoTopic(settings.MQTT.discoTopic, id, true, topic, sizeof(topic));
|
||||
mqtt.unpublish(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
void SomfyGroup::unpublish(uint8_t id) {
|
||||
if(mqtt.connected()) {
|
||||
SomfyGroup::unpublish(id, "groupId");
|
||||
SomfyGroup::unpublish(id, "name");
|
||||
SomfyGroup::unpublish(id, "remoteAddress");
|
||||
SomfyGroup::unpublish(id, "groupType");
|
||||
SomfyGroup::unpublish(id, "direction");
|
||||
SomfyGroup::unpublish(id, "lastRollingCode");
|
||||
SomfyGroup::unpublish(id, "flags");
|
||||
SomfyGroup::unpublish(id, "SunSensor");
|
||||
SomfyGroup::unpublish(id, "flipCommands");
|
||||
for(const char *topic : GROUP_TOPICS) SomfyGroup::unpublish(id, topic);
|
||||
}
|
||||
}
|
||||
void SomfyGroup::unpublish(uint8_t id, const char *topic) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ Runs against a real device on your network, by hand, never in CI. It checks sock
|
|||
| `--password`, `--username` / `--pin` | the credentials of a Secured device |
|
||||
| `--radio --shade <id>` | also send real commands: that shade moves a little and back |
|
||||
| `--edit` | also add a room and delete it again |
|
||||
| `--reboot` | also reboot the device and wait for it to come back, and restore the settings from a backup taken a moment before (it changes the NTP server, which the restore puts back) |
|
||||
| `--reboot` | also reboot the device and wait for it to come back, and restore the settings from a backup taken a moment before (it changes the NTP server, which the restore puts back), and restore the radio settings alone |
|
||||
| `--firmware <firmware.bin>` | also update the device to that firmware, built for its chip, and check it is still running a minute later, after it marks the update good. Needs a build with an OTA slot |
|
||||
| `--lockout` | on a Secured device, also fail `/login` until this PC is refused, then wait out the 30 s lockout |
|
||||
| `--record` | save hello, the sync and the schema to `test/fixtures/proto2/`, for the web UI and Home Assistant tests |
|
||||
|
||||
|
|
@ -20,4 +21,11 @@ A plain run sends nothing on air, changes no setting (it saves only values the d
|
|||
|
||||
The web UI holds one of the device's 5 socket slots for each open tab, and Home Assistant holds one. The tests that fill every slot fill the ones left, so close the UI's tabs for the full load.
|
||||
|
||||
A rollback is checked by hand, on a release build with an OTA slot:
|
||||
|
||||
1. Note the version the web UI shows.
|
||||
2. Build firmware that restarts before it is marked good: add `abort();` at the end of `setup()` in `src/SomfyController.cpp`, and change `data-src/appversion` so the two versions differ.
|
||||
3. Update to it through the web UI, with the serial monitor open.
|
||||
4. The crash restarts the device, and the bootloader boots the old firmware ([ADR 0003](../../docs/adr/0003-pioarduino-idf55.md)). The web UI shows the version from step 1 again.
|
||||
|
||||
The tests run in whatever security mode the device is in; the suite never changes it. To test both modes, run the suite once in each.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ def pytest_addoption(parser):
|
|||
parser.addoption("--shade", type=int, default=None, help="with --radio: the id of the shade the commands may move")
|
||||
parser.addoption("--edit", action="store_true", help="also run the edits that change the shade model, then undo them")
|
||||
parser.addoption("--reboot", action="store_true", help="also run the tests that reboot the device")
|
||||
parser.addoption("--firmware", default=None, help="also update the device to this firmware.bin, built for its chip")
|
||||
parser.addoption("--lockout", action="store_true", help="also run the test that locks this PC out of /login for 30 s")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
from proto_client import run_sync
|
||||
from test_commands import is_ok, op, synced_model
|
||||
from test_routes import auth, http, restore_without_a_file, upload
|
||||
from test_settings import desc, settings, uptime, wait_for_restart
|
||||
from test_settings import boot_time, desc, settings, wait_for_restart
|
||||
|
||||
# Saved for the round trip and put back by the restore. .invalid never resolves.
|
||||
CHANGED_NTP_SERVER = "restore.e2e.invalid"
|
||||
|
|
@ -50,7 +50,7 @@ def test_a_restore_brings_back_the_parts_that_were_ticked(request, host, token,
|
|||
pytest.skip("restores a backup and reboots the device: pass --reboot")
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
uptime_before = uptime(schema, frames)
|
||||
booted = boot_time(schema, frames)
|
||||
general = settings(c, schema, "general")
|
||||
ntp = {name: general.get(name, schema.default(name)) for name in ("ntpServer", "posixZone")}
|
||||
assert ntp["ntpServer"] != CHANGED_NTP_SERVER
|
||||
|
|
@ -63,10 +63,50 @@ def test_a_restore_brings_back_the_parts_that_were_ticked(request, host, token,
|
|||
options = json.dumps({"shades": False, "settings": True})
|
||||
status, _, body = upload(host, "/restore", auth(token), file=backup, fields={"data": options})
|
||||
assert status == 200, body
|
||||
wait_for_restart(host, token, schema, uptime_before)
|
||||
wait_for_restart(host, token, schema, booted)
|
||||
finally:
|
||||
c, _, _ = synced_model(connect, schema)
|
||||
restored = settings(c, schema, "general").get("ntpServer", schema.default("ntpServer"))
|
||||
if restored != ntp["ntpServer"]:
|
||||
op(c, schema, "saveSettings", dict(ntp, section="general"))
|
||||
assert restored == ntp["ntpServer"]
|
||||
|
||||
|
||||
def test_a_restore_of_the_radio_alone_brings_it_back_as_it_was(request, host, token, connect, schema):
|
||||
"""4.0 boards without Ethernet declared their network record 28 bytes
|
||||
longer than they wrote, so a restore that skipped it read the radio from
|
||||
the middle of its record."""
|
||||
if not request.config.getoption("--reboot"):
|
||||
pytest.skip("restores a backup and reboots the device: pass --reboot")
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
booted = boot_time(schema, frames)
|
||||
radio = settings(c, schema, "radio")
|
||||
status, _, backup = http(host, "/backup", headers=auth(token))
|
||||
assert status == 200
|
||||
|
||||
options = json.dumps({"shades": False, "transceiver": True})
|
||||
status, _, body = upload(host, "/restore", auth(token), file=backup, fields={"data": options})
|
||||
assert status == 200, body
|
||||
wait_for_restart(host, token, schema, booted)
|
||||
c, _, _ = synced_model(connect, schema)
|
||||
assert settings(c, schema, "radio") == radio
|
||||
|
||||
|
||||
def test_a_restore_of_the_shades_brings_them_back_as_they_were(request, host, token, connect, schema):
|
||||
"""Every shade and group, their rolling codes included, from a backup taken a moment before."""
|
||||
if not request.config.getoption("--reboot"):
|
||||
pytest.skip("restores a backup and reboots the device: pass --reboot")
|
||||
_, shades, groups = synced_model(connect, schema)
|
||||
_, frames, _ = run_sync(connect(schema=schema.hash))
|
||||
booted = boot_time(schema, frames)
|
||||
status, _, backup = http(host, "/backup", headers=auth(token))
|
||||
assert status == 200
|
||||
|
||||
options = json.dumps({"shades": True})
|
||||
status, _, body = upload(host, "/restore", auth(token), file=backup, fields={"data": options})
|
||||
assert status == 200, body
|
||||
wait_for_restart(host, token, schema, booted)
|
||||
_, shades_after, groups_after = synced_model(connect, schema)
|
||||
assert shades_after == shades
|
||||
assert groups_after == groups
|
||||
|
|
|
|||
|
|
@ -119,20 +119,20 @@ def test_reboot(request, host, token, connect, schema):
|
|||
pytest.skip("reboots the device: pass --reboot")
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
uptime_before = uptime(schema, frames)
|
||||
booted = boot_time(schema, frames)
|
||||
assert is_ok(schema, op(c, schema, "reboot", {}))
|
||||
assert c.closed.wait(15), "the socket stayed open"
|
||||
wait_for_restart(host, token, schema, uptime_before)
|
||||
wait_for_restart(host, token, schema, booted)
|
||||
|
||||
|
||||
def wait_for_restart(host, token, schema, uptime_before):
|
||||
"""Waits until the device is back, and checks it restarted."""
|
||||
def wait_for_restart(host, token, schema, booted):
|
||||
"""Waits until the device has restarted since it booted at booted (boot_time)."""
|
||||
# The device closes its sockets just before it restarts, and may still
|
||||
# take a connection in between: that one never gets its hello.
|
||||
time.sleep(3)
|
||||
end = time.time() + 90
|
||||
while True:
|
||||
assert time.time() < end, "the device didn't come back"
|
||||
assert time.time() < end, "the device didn't restart"
|
||||
try:
|
||||
back = ProtoClient(host, params={"apikey": token} if token else None, timeout=3)
|
||||
except OSError:
|
||||
|
|
@ -142,7 +142,9 @@ def wait_for_restart(host, token, schema, uptime_before):
|
|||
if back.status == 101:
|
||||
hello, frames, _ = run_sync(back, timeout=10)
|
||||
assert hello["d"]["sh"] == schema.hash
|
||||
assert uptime(schema, frames) < uptime_before, "the device didn't restart"
|
||||
# Compared as boot times: a device that restarted a moment
|
||||
# ago may have had less uptime than it has now.
|
||||
if boot_time(schema, frames) > booted + 2:
|
||||
return
|
||||
except TimeoutError:
|
||||
pass # not back yet
|
||||
|
|
@ -151,6 +153,11 @@ def wait_for_restart(host, token, schema, uptime_before):
|
|||
time.sleep(2)
|
||||
|
||||
|
||||
def boot_time(schema, frames):
|
||||
"""When the device booted, as a time.time(), from the status a sync ends with."""
|
||||
return time.time() - uptime(schema, frames) / 1000
|
||||
|
||||
|
||||
def header(schema, frames):
|
||||
"""The header a sync starts with."""
|
||||
key = schema.keys["header"]
|
||||
|
|
|
|||
52
test/e2e/test_update.py
Normal file
52
test/e2e/test_update.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""A firmware update through /updateFirmware, as the web UI sends it.
|
||||
|
||||
Pass --firmware with a firmware.bin built for the device's chip, e.g.
|
||||
.pio/build/esp32s3/firmware.bin. The device must have an OTA slot: the
|
||||
release builds do, the C5, C6 and debug builds don't.
|
||||
|
||||
It can't tell the new firmware from the old when both have the same
|
||||
version, and it doesn't make the update fail. test/e2e/README.md has the
|
||||
rollback check by hand.
|
||||
"""
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from proto_client import run_sync
|
||||
from test_routes import auth, upload
|
||||
from test_settings import boot_time, wait_for_restart
|
||||
|
||||
# The new firmware is marked good once it has run this long with the network
|
||||
# up; a reset before then boots the old one (docs/adr/0003-pioarduino-idf55.md).
|
||||
PROVEN_AFTER_S = 60
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def firmware(request):
|
||||
path = request.config.getoption("--firmware")
|
||||
if not path:
|
||||
pytest.skip("updates the device's firmware: pass --firmware <firmware.bin>")
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_an_update_restarts_on_the_new_firmware_and_stays(host, token, connect, schema, firmware):
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
booted = boot_time(schema, frames)
|
||||
c.close()
|
||||
|
||||
status, _, body = upload(host, "/updateFirmware", auth(token), file=firmware)
|
||||
assert status == 200, body
|
||||
wait_for_restart(host, token, schema, booted)
|
||||
|
||||
# Past the point where it is marked good it is still running, and hasn't
|
||||
# restarted on the old firmware.
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
restarted = boot_time(schema, frames)
|
||||
c.close()
|
||||
time.sleep(PROVEN_AFTER_S + 10)
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
assert abs(boot_time(schema, frames) - restarted) < 5, "the device restarted again"
|
||||
850
test/test_configrecords/test_configrecords.cpp
Normal file
850
test/test_configrecords/test_configrecords.cpp
Normal file
|
|
@ -0,0 +1,850 @@
|
|||
#include <unity.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include "ConfigRecords.h"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
// A file in memory.
|
||||
class TextWriter : public ConfigWriter {
|
||||
public:
|
||||
std::string text;
|
||||
bool writeBytes(const uint8_t *buf, size_t len) override { text.append(reinterpret_cast<const char *>(buf), len); return true; }
|
||||
};
|
||||
class TextReader : public ConfigReader {
|
||||
public:
|
||||
std::string text;
|
||||
size_t pos = 0;
|
||||
explicit TextReader(const std::string &t) : text(t) {}
|
||||
bool readByte(uint8_t &val) override {
|
||||
if(pos >= text.size()) return false;
|
||||
val = static_cast<uint8_t>(text[pos++]);
|
||||
return true;
|
||||
}
|
||||
uint32_t position() override { return static_cast<uint32_t>(pos); }
|
||||
};
|
||||
|
||||
static config_header_t header(uint8_t version = SHADE_HDR_VER) {
|
||||
config_header_t h;
|
||||
h.version = version;
|
||||
return h;
|
||||
}
|
||||
static shade_record_t aShade() {
|
||||
shade_record_t rec;
|
||||
rec.shadeId = 7;
|
||||
rec.paired = true;
|
||||
rec.shadeType = shade_types::blind;
|
||||
rec.remoteAddress = 0x123456;
|
||||
strcpy(rec.name, "Kitchen");
|
||||
rec.tiltType = tilt_types::tiltmotor;
|
||||
rec.proto = radio_proto::GP_Remote;
|
||||
rec.bitLength = 80;
|
||||
rec.upTime = 21000;
|
||||
rec.downTime = 19500;
|
||||
rec.tiltTime = 3200;
|
||||
rec.stepSize = 250;
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) rec.linkedRemotes[j] = 0x200000 + j;
|
||||
rec.lastRollingCode = 65000;
|
||||
rec.flags = 0x21;
|
||||
rec.myPos = 42.5f;
|
||||
rec.myTiltPos = 12.25f;
|
||||
rec.currentPos = 66.0f;
|
||||
rec.currentTiltPos = 33.5f;
|
||||
rec.flipCommands = true;
|
||||
rec.flipPosition = true;
|
||||
rec.repeats = 4;
|
||||
rec.sortOrder = 3;
|
||||
rec.gpioUp = 25;
|
||||
rec.gpioDown = 26;
|
||||
rec.gpioMy = 27;
|
||||
rec.gpioFlags = 2;
|
||||
rec.roomId = 5;
|
||||
return rec;
|
||||
}
|
||||
static group_record_t aGroup() {
|
||||
group_record_t rec;
|
||||
rec.groupId = 3;
|
||||
rec.remoteAddress = 0x654321;
|
||||
strcpy(rec.name, "Upstairs");
|
||||
rec.proto = radio_proto::RTS;
|
||||
rec.bitLength = 80;
|
||||
rec.linkedShades[0] = 1;
|
||||
rec.linkedShades[1] = 7;
|
||||
rec.linkedShades[2] = 32;
|
||||
rec.repeats = 2;
|
||||
rec.sortOrder = 9;
|
||||
rec.flipCommands = true;
|
||||
rec.roomId = 4;
|
||||
rec.lastRollingCode = 1234;
|
||||
return rec;
|
||||
}
|
||||
static net_record_t aNet() {
|
||||
net_record_t rec;
|
||||
rec.connType = 2;
|
||||
rec.dhcp = false;
|
||||
strcpy(rec.ip, "192.168.100.200");
|
||||
strcpy(rec.gateway, "192.168.100.1");
|
||||
strcpy(rec.subnet, "255.255.255.0");
|
||||
strcpy(rec.dns1, "1.1.1.1");
|
||||
strcpy(rec.dns2, "8.8.8.8");
|
||||
strcpy(rec.mqttProtocol, "mqtts://");
|
||||
strcpy(rec.mqttHostname, "broker.local");
|
||||
rec.mqttPort = 8883;
|
||||
rec.mqttPubDisco = true;
|
||||
strcpy(rec.mqttRootTopic, "somfy");
|
||||
strcpy(rec.mqttDiscoTopic, "ha");
|
||||
rec.ethBoardType = 3;
|
||||
rec.ethPhyType = 1;
|
||||
rec.ethClkMode = 3;
|
||||
rec.ethPhyAddress = -1;
|
||||
rec.ethPWRPin = 12;
|
||||
rec.ethMDCPin = 23;
|
||||
rec.ethMDIOPin = 18;
|
||||
return rec;
|
||||
}
|
||||
static trans_record_t aTrans() {
|
||||
trans_record_t rec;
|
||||
rec.enabled = true;
|
||||
rec.type = 80;
|
||||
rec.SCKPin = 4;
|
||||
rec.CSNPin = 7;
|
||||
rec.MOSIPin = 6;
|
||||
rec.MISOPin = 5;
|
||||
rec.TXPin = 10;
|
||||
rec.RXPin = 3;
|
||||
rec.frequency = 433.25f;
|
||||
rec.rxBandwidth = 812.5f;
|
||||
rec.deviation = 1.58f;
|
||||
rec.txPower = -30;
|
||||
rec.noiseDetection = true;
|
||||
return rec;
|
||||
}
|
||||
static void assertShadesEqual(const shade_record_t &want, const shade_record_t &got) {
|
||||
TEST_ASSERT_EQUAL_UINT8(want.shadeId, got.shadeId);
|
||||
TEST_ASSERT_EQUAL(want.paired, got.paired);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.shadeType), static_cast<uint8_t>(got.shadeType));
|
||||
TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress);
|
||||
TEST_ASSERT_EQUAL_STRING(want.name, got.name);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.tiltType), static_cast<uint8_t>(got.tiltType));
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(want.proto), static_cast<uint8_t>(got.proto));
|
||||
TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength);
|
||||
TEST_ASSERT_EQUAL_UINT32(want.upTime, got.upTime);
|
||||
TEST_ASSERT_EQUAL_UINT32(want.downTime, got.downTime);
|
||||
TEST_ASSERT_EQUAL_UINT32(want.tiltTime, got.tiltTime);
|
||||
TEST_ASSERT_EQUAL_UINT16(want.stepSize, got.stepSize);
|
||||
TEST_ASSERT_EQUAL_HEX32_ARRAY(want.linkedRemotes, got.linkedRemotes, SOMFY_MAX_LINKED_REMOTES);
|
||||
TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode);
|
||||
TEST_ASSERT_EQUAL_HEX8(want.flags, got.flags);
|
||||
TEST_ASSERT_EQUAL_FLOAT(want.myPos, got.myPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(want.myTiltPos, got.myTiltPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(want.currentPos, got.currentPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(want.currentTiltPos, got.currentTiltPos);
|
||||
TEST_ASSERT_EQUAL(want.flipCommands, got.flipCommands);
|
||||
TEST_ASSERT_EQUAL(want.flipPosition, got.flipPosition);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats);
|
||||
TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.gpioUp, got.gpioUp);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.gpioDown, got.gpioDown);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.gpioMy, got.gpioMy);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.gpioFlags, got.gpioFlags);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId);
|
||||
}
|
||||
|
||||
// Every record is the size the header says, or validate() refuses the file.
|
||||
static void test_each_record_is_the_size_the_header_declares() {
|
||||
config_header_t h = header();
|
||||
h.length = SHADE_HDR_SIZE;
|
||||
strcpy(h.serverId, "a1b2c3d4");
|
||||
TextWriter w;
|
||||
writeConfigHeader(w, h);
|
||||
TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, w.text.size());
|
||||
|
||||
room_record_t room;
|
||||
room.roomId = 16;
|
||||
strcpy(room.name, "12345678901234567890");
|
||||
w.text.clear();
|
||||
writeRoomRecord(w, room);
|
||||
TEST_ASSERT_EQUAL(ROOM_REC_SIZE, w.text.size());
|
||||
|
||||
w.text.clear();
|
||||
writeShadeRecord(w, aShade());
|
||||
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
|
||||
|
||||
w.text.clear();
|
||||
writeGroupRecord(w, aGroup());
|
||||
TEST_ASSERT_EQUAL(GROUP_REC_SIZE, w.text.size());
|
||||
|
||||
uint32_t repeaters[SOMFY_MAX_REPEATERS] = {0xFFFFFF, 1};
|
||||
w.text.clear();
|
||||
writeRepeaterRecord(w, repeaters);
|
||||
TEST_ASSERT_EQUAL(REPEATER_REC_SIZE, w.text.size());
|
||||
|
||||
w.text.clear();
|
||||
writeTransRecord(w, aTrans());
|
||||
TEST_ASSERT_EQUAL(TRANS_REC_SIZE, w.text.size());
|
||||
}
|
||||
// backup() measures the records that vary in length with a ConfigCounter.
|
||||
static void test_a_counter_measures_what_a_writer_writes() {
|
||||
TextWriter w;
|
||||
ConfigCounter c;
|
||||
writeNetRecord(w, aNet(), false);
|
||||
writeNetRecord(c, aNet(), false);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), c.bytes);
|
||||
}
|
||||
static void test_a_deleted_shade_is_written_cleared() {
|
||||
shade_record_t rec = aShade();
|
||||
rec.shadeId = 255;
|
||||
TextWriter w;
|
||||
writeShadeRecord(w, rec);
|
||||
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
|
||||
TextReader r(w.text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(), got);
|
||||
TEST_ASSERT_EQUAL_FLOAT(-1, got.myPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(0, got.currentPos);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, got.flags);
|
||||
}
|
||||
|
||||
static void test_the_header_reads_back() {
|
||||
config_header_t h = header();
|
||||
h.length = SHADE_HDR_SIZE;
|
||||
h.roomRecordSize = ROOM_REC_SIZE;
|
||||
h.roomRecords = 3;
|
||||
h.shadeRecordSize = SHADE_REC_SIZE;
|
||||
h.shadeRecords = 32;
|
||||
h.groupRecordSize = GROUP_REC_SIZE;
|
||||
h.groupRecords = 16;
|
||||
h.repeaterRecordSize = REPEATER_REC_SIZE;
|
||||
h.repeaterRecords = 1;
|
||||
h.settingsRecordSize = 94;
|
||||
h.netRecordSize = 166;
|
||||
h.transRecordSize = TRANS_REC_SIZE;
|
||||
strcpy(h.serverId, "a1b2c3d4");
|
||||
TextWriter w;
|
||||
writeConfigHeader(w, h);
|
||||
TextReader r(w.text);
|
||||
config_header_t got;
|
||||
readConfigHeader(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(24, got.version);
|
||||
TEST_ASSERT_EQUAL_INT8(SHADE_HDR_SIZE, got.length);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.roomRecords);
|
||||
TEST_ASSERT_EQUAL_UINT8(32, got.shadeRecords);
|
||||
TEST_ASSERT_EQUAL_UINT8(16, got.groupRecords);
|
||||
TEST_ASSERT_EQUAL_UINT8(REPEATER_REC_SIZE, got.repeaterRecordSize);
|
||||
TEST_ASSERT_EQUAL_UINT16(94, got.settingsRecordSize);
|
||||
TEST_ASSERT_EQUAL_UINT16(166, got.netRecordSize);
|
||||
TEST_ASSERT_EQUAL_UINT16(TRANS_REC_SIZE, got.transRecordSize);
|
||||
TEST_ASSERT_EQUAL_STRING("a1b2c3d4", got.serverId);
|
||||
TEST_ASSERT_EQUAL(SHADE_HDR_SIZE, r.position());
|
||||
}
|
||||
static void test_a_version_12_header_has_no_rooms_repeaters_or_settings() {
|
||||
// version, length, shade size and count (one byte each then), group size and count.
|
||||
TextReader r("12, 20,200, 2,150, 1\n");
|
||||
config_header_t got;
|
||||
readConfigHeader(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(12, got.version);
|
||||
TEST_ASSERT_EQUAL_UINT16(200, got.shadeRecordSize);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.shadeRecords);
|
||||
TEST_ASSERT_EQUAL_UINT16(150, got.groupRecordSize);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.groupRecords);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.roomRecords);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.repeaterRecords);
|
||||
TEST_ASSERT_EQUAL_UINT16(0, got.settingsRecordSize);
|
||||
}
|
||||
|
||||
static void test_a_shade_reads_back() {
|
||||
TextWriter w;
|
||||
writeShadeRecord(w, aShade());
|
||||
TextReader r(w.text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(), got);
|
||||
assertShadesEqual(aShade(), got);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
static void test_a_room_reads_back() {
|
||||
room_record_t room;
|
||||
room.roomId = 2;
|
||||
strcpy(room.name, "Living room");
|
||||
room.sortOrder = 5;
|
||||
TextWriter w;
|
||||
writeRoomRecord(w, room);
|
||||
TextReader r(w.text);
|
||||
room_record_t got;
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
|
||||
TEST_ASSERT_EQUAL_STRING("Living room", got.name);
|
||||
TEST_ASSERT_EQUAL_INT8(5, got.sortOrder);
|
||||
}
|
||||
static void test_a_group_reads_back() {
|
||||
TextWriter w;
|
||||
writeGroupRecord(w, aGroup());
|
||||
TextReader r(w.text);
|
||||
group_record_t got;
|
||||
readGroupRecord(r, header(), got);
|
||||
const group_record_t want = aGroup();
|
||||
TEST_ASSERT_EQUAL_UINT8(want.groupId, got.groupId);
|
||||
TEST_ASSERT_EQUAL_HEX32(want.remoteAddress, got.remoteAddress);
|
||||
TEST_ASSERT_EQUAL_STRING(want.name, got.name);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.bitLength, got.bitLength);
|
||||
TEST_ASSERT_EQUAL_UINT8_ARRAY(want.linkedShades, got.linkedShades, SOMFY_MAX_GROUPED_SHADES);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.repeats, got.repeats);
|
||||
TEST_ASSERT_EQUAL_INT8(want.sortOrder, got.sortOrder);
|
||||
TEST_ASSERT_TRUE(got.flipCommands);
|
||||
TEST_ASSERT_EQUAL_UINT8(want.roomId, got.roomId);
|
||||
TEST_ASSERT_EQUAL_UINT16(want.lastRollingCode, got.lastRollingCode);
|
||||
}
|
||||
static void test_a_group_reads_its_shades_without_gaps() {
|
||||
group_record_t rec = aGroup();
|
||||
memset(rec.linkedShades, 0, sizeof(rec.linkedShades));
|
||||
rec.linkedShades[2] = 4;
|
||||
rec.linkedShades[10] = 9;
|
||||
TextWriter w;
|
||||
writeGroupRecord(w, rec);
|
||||
TextReader r(w.text);
|
||||
group_record_t got;
|
||||
readGroupRecord(r, header(), got);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(9, got.linkedShades[1]);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.linkedShades[2]);
|
||||
}
|
||||
static void test_the_settings_read_back() {
|
||||
settings_record_t rec;
|
||||
strcpy(rec.fwVersion, "v4.0.0");
|
||||
strcpy(rec.hostname, "somfy");
|
||||
strcpy(rec.ntpServer, "time.google.com");
|
||||
strcpy(rec.posixZone, "CET-1CEST,M3.5.0,M10.5.0/3");
|
||||
rec.ssdpBroadcast = true;
|
||||
rec.checkForUpdate = false;
|
||||
TextWriter w;
|
||||
writeSettingsRecord(w, rec);
|
||||
TextReader r(w.text);
|
||||
settings_record_t got;
|
||||
readSettingsRecord(r, header(), got);
|
||||
TEST_ASSERT_EQUAL_STRING("v4.0.0", got.fwVersion);
|
||||
TEST_ASSERT_EQUAL_STRING("somfy", got.hostname);
|
||||
TEST_ASSERT_EQUAL_STRING("time.google.com", got.ntpServer);
|
||||
// The commas are inside the quotes.
|
||||
TEST_ASSERT_EQUAL_STRING("CET-1CEST,M3.5.0,M10.5.0/3", got.posixZone);
|
||||
TEST_ASSERT_TRUE(got.ssdpBroadcast);
|
||||
TEST_ASSERT_FALSE(got.checkForUpdate);
|
||||
}
|
||||
static void test_the_network_reads_back_with_ethernet() {
|
||||
TextWriter w;
|
||||
writeNetRecord(w, aNet(), true);
|
||||
TextReader r(w.text);
|
||||
net_record_t got;
|
||||
readNetRecord(r, header(), got, true);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.connType);
|
||||
TEST_ASSERT_FALSE(got.dhcp);
|
||||
TEST_ASSERT_EQUAL_STRING("192.168.100.200", got.ip);
|
||||
TEST_ASSERT_EQUAL_STRING("192.168.100.1", got.gateway);
|
||||
TEST_ASSERT_EQUAL_STRING("255.255.255.0", got.subnet);
|
||||
TEST_ASSERT_EQUAL_STRING("1.1.1.1", got.dns1);
|
||||
TEST_ASSERT_EQUAL_STRING("8.8.8.8", got.dns2);
|
||||
TEST_ASSERT_EQUAL_STRING("mqtts://", got.mqttProtocol);
|
||||
TEST_ASSERT_EQUAL_STRING("broker.local", got.mqttHostname);
|
||||
TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort);
|
||||
TEST_ASSERT_TRUE(got.mqttPubDisco);
|
||||
TEST_ASSERT_EQUAL_STRING("somfy", got.mqttRootTopic);
|
||||
TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.ethBoardType);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.ethPhyType);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.ethClkMode);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, got.ethPhyAddress);
|
||||
TEST_ASSERT_EQUAL_INT8(12, got.ethPWRPin);
|
||||
TEST_ASSERT_EQUAL_INT8(23, got.ethMDCPin);
|
||||
TEST_ASSERT_EQUAL_INT8(18, got.ethMDIOPin);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
static void test_the_radio_reads_back() {
|
||||
TextWriter w;
|
||||
writeTransRecord(w, aTrans());
|
||||
TextReader r(w.text);
|
||||
trans_record_t got;
|
||||
readTransRecord(r, got);
|
||||
TEST_ASSERT_TRUE(got.enabled);
|
||||
TEST_ASSERT_EQUAL_UINT8(80, got.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(7, got.CSNPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(6, got.MOSIPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(5, got.MISOPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(10, got.TXPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.RXPin);
|
||||
TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency);
|
||||
TEST_ASSERT_EQUAL_FLOAT(812.5f, got.rxBandwidth);
|
||||
TEST_ASSERT_EQUAL_FLOAT(1.58f, got.deviation);
|
||||
TEST_ASSERT_EQUAL_INT8(-30, got.txPower);
|
||||
TEST_ASSERT_TRUE(got.noiseDetection);
|
||||
}
|
||||
|
||||
// The web UI takes any name, and the record would shift by a value at the
|
||||
// comma.
|
||||
static void test_a_name_with_a_comma_keeps_the_record_in_line() {
|
||||
shade_record_t rec = aShade();
|
||||
strcpy(rec.name, "Left, by the door");
|
||||
TextWriter w;
|
||||
writeShadeRecord(w, rec);
|
||||
TEST_ASSERT_EQUAL(SHADE_REC_SIZE, w.text.size());
|
||||
TextReader r(w.text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(), got);
|
||||
assertShadesEqual(rec, got);
|
||||
}
|
||||
static void test_the_longest_name_reads_back() {
|
||||
room_record_t room;
|
||||
room.roomId = 1;
|
||||
strcpy(room.name, "12345678901234567890");
|
||||
room.sortOrder = 7;
|
||||
TextWriter w;
|
||||
writeRoomRecord(w, room);
|
||||
TextReader r(w.text);
|
||||
room_record_t got;
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_STRING("12345678901234567890", got.name);
|
||||
TEST_ASSERT_EQUAL_INT8(7, got.sortOrder);
|
||||
}
|
||||
// A quoted value as long as its field, with its quotes and comma, is longer
|
||||
// than the field. Reading stopped at the field's size and left the rest to
|
||||
// the next value.
|
||||
static void test_the_longest_settings_read_back() {
|
||||
settings_record_t rec;
|
||||
strcpy(rec.fwVersion, "v4.0.0");
|
||||
memset(rec.hostname, 'h', sizeof(rec.hostname) - 1);
|
||||
memset(rec.ntpServer, 'n', sizeof(rec.ntpServer) - 1);
|
||||
memset(rec.posixZone, 'z', sizeof(rec.posixZone) - 1);
|
||||
rec.ssdpBroadcast = true;
|
||||
rec.checkForUpdate = false;
|
||||
TextWriter w;
|
||||
writeSettingsRecord(w, rec);
|
||||
TextReader r(w.text);
|
||||
settings_record_t got;
|
||||
readSettingsRecord(r, header(), got);
|
||||
TEST_ASSERT_EQUAL_STRING(rec.hostname, got.hostname);
|
||||
TEST_ASSERT_EQUAL_STRING(rec.ntpServer, got.ntpServer);
|
||||
TEST_ASSERT_EQUAL_STRING(rec.posixZone, got.posixZone);
|
||||
TEST_ASSERT_TRUE(got.ssdpBroadcast);
|
||||
TEST_ASSERT_FALSE(got.checkForUpdate);
|
||||
}
|
||||
static void test_the_longest_mqtt_settings_read_back() {
|
||||
net_record_t rec = aNet();
|
||||
memset(rec.mqttHostname, 'm', sizeof(rec.mqttHostname) - 1);
|
||||
memset(rec.mqttRootTopic, 'r', sizeof(rec.mqttRootTopic) - 1);
|
||||
TextWriter w;
|
||||
writeNetRecord(w, rec, false);
|
||||
TextReader r(w.text);
|
||||
net_record_t got;
|
||||
readNetRecord(r, header(), got, false);
|
||||
TEST_ASSERT_EQUAL_STRING(rec.mqttHostname, got.mqttHostname);
|
||||
TEST_ASSERT_EQUAL_UINT16(8883, got.mqttPort);
|
||||
TEST_ASSERT_EQUAL_STRING(rec.mqttRootTopic, got.mqttRootTopic);
|
||||
TEST_ASSERT_EQUAL_STRING("ha", got.mqttDiscoTopic);
|
||||
}
|
||||
// A value too long for its field is cut, and the next value still reads.
|
||||
static void test_a_value_too_long_is_cut() {
|
||||
TextReader r("\"0123456789012345678901234567890123456789\",\"next\"\n");
|
||||
char ip[CFG_IP_LEN];
|
||||
char next[CFG_IP_LEN];
|
||||
r.beginRecord();
|
||||
r.readVarString(ip, sizeof(ip));
|
||||
r.readVarString(next, sizeof(next));
|
||||
TEST_ASSERT_EQUAL_STRING("01234567890123456789012", ip);
|
||||
TEST_ASSERT_EQUAL_STRING("next", next);
|
||||
}
|
||||
|
||||
// The layout of a restore past the shades: the network record of a board
|
||||
// without Ethernet, then the radio record.
|
||||
static std::string backupTail(const net_record_t &net, const trans_record_t &trans) {
|
||||
TextWriter w;
|
||||
writeNetRecord(w, net, false);
|
||||
writeTransRecord(w, trans);
|
||||
return w.text;
|
||||
}
|
||||
// 4.0 boards without Ethernet declared the network record 28 bytes longer
|
||||
// than they wrote, and a restore that skipped it by its size read the radio
|
||||
// from the middle of its record.
|
||||
static void test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio() {
|
||||
TextReader r(backupTail(aNet(), aTrans()));
|
||||
skipRecords(r, 1);
|
||||
trans_record_t got;
|
||||
readTransRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.SCKPin);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.RXPin);
|
||||
TEST_ASSERT_EQUAL_FLOAT(433.25f, got.frequency);
|
||||
TEST_ASSERT_TRUE(got.noiseDetection);
|
||||
}
|
||||
// The board that wrote a backup reads its Ethernet settings back; a board
|
||||
// with Ethernet restoring a backup from one without finds none.
|
||||
static void test_a_network_record_without_ethernet_keeps_the_ethernet_defaults() {
|
||||
TextReader r(backupTail(aNet(), aTrans()));
|
||||
net_record_t got;
|
||||
readNetRecord(r, header(), got, true);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.ethBoardType);
|
||||
TEST_ASSERT_EQUAL_INT8(1, got.ethPhyAddress);
|
||||
TEST_ASSERT_EQUAL_INT8(23, got.ethMDIOPin);
|
||||
trans_record_t trans;
|
||||
readTransRecord(r, trans);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, trans.SCKPin);
|
||||
}
|
||||
// Before 4.0 the radio record was declared 74 bytes and is 80.
|
||||
static void test_the_radio_reads_whole_whatever_its_declared_size() {
|
||||
TextWriter w;
|
||||
writeTransRecord(w, aTrans());
|
||||
TEST_ASSERT_NOT_EQUAL(74, w.text.size());
|
||||
TextReader r(w.text);
|
||||
trans_record_t got;
|
||||
readTransRecord(r, got);
|
||||
TEST_ASSERT_TRUE(got.noiseDetection);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
// A record from a newer version has values this one doesn't know.
|
||||
static void test_values_a_newer_version_added_are_skipped() {
|
||||
TextWriter w;
|
||||
writeRoomRecord(w, room_record_t{1, "Hall", 0});
|
||||
w.text.insert(w.text.size() - 1, ", 9, 9");
|
||||
writeRoomRecord(w, room_record_t{2, "Attic", 1});
|
||||
TextReader r(w.text);
|
||||
room_record_t got;
|
||||
readRoomRecord(r, got);
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
|
||||
TEST_ASSERT_EQUAL_STRING("Attic", got.name);
|
||||
}
|
||||
// A record shorter than its version says reads defaults for the rest and
|
||||
// leaves the next record alone.
|
||||
static void test_a_short_record_never_reads_the_next() {
|
||||
TextWriter w;
|
||||
writeRoomRecord(w, room_record_t{1, "Hall", 0});
|
||||
// Ends after the name.
|
||||
std::string text = w.text.substr(0, 4 + CFG_NAME_LEN - 1) + CFG_REC_END;
|
||||
w.text.clear();
|
||||
writeRoomRecord(w, room_record_t{2, "Attic", 1});
|
||||
TextReader r(text + w.text);
|
||||
room_record_t got;
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.roomId);
|
||||
TEST_ASSERT_EQUAL_STRING("Hall", got.name);
|
||||
TEST_ASSERT_EQUAL_INT8(0, got.sortOrder);
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.roomId);
|
||||
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
|
||||
}
|
||||
static void test_skipping_records_passes_their_newlines() {
|
||||
TextWriter w;
|
||||
writeShadeRecord(w, aShade());
|
||||
writeGroupRecord(w, aGroup());
|
||||
writeRoomRecord(w, room_record_t{4, "Last", 2});
|
||||
TextReader r(w.text);
|
||||
TEST_ASSERT_TRUE(skipRecords(r, 2));
|
||||
room_record_t got;
|
||||
readRoomRecord(r, got);
|
||||
TEST_ASSERT_EQUAL_STRING("Last", got.name);
|
||||
TEST_ASSERT_FALSE(skipRecords(r, 1));
|
||||
}
|
||||
|
||||
// Older versions, written field by field in the layout they had.
|
||||
static void writeShadeStart(TextWriter &w, uint8_t id) {
|
||||
w.writeUInt8(id);
|
||||
w.writeBool(true);
|
||||
w.writeUInt8(static_cast<uint8_t>(shade_types::roller));
|
||||
w.writeUInt32(0x111111);
|
||||
w.writeString("Old", CFG_NAME_LEN);
|
||||
}
|
||||
static std::string version4Shade() {
|
||||
TextWriter w;
|
||||
writeShadeStart(w, 2);
|
||||
w.writeUInt8(0); // tiltType
|
||||
w.writeUInt8(56); // bitLength
|
||||
w.writeUInt32(15000); // upTime
|
||||
w.writeUInt32(14000); // downTime
|
||||
w.writeUInt32(0); // tiltTime
|
||||
for(uint8_t j = 0; j < 5; j++) w.writeUInt32(j == 0 ? 0x222222 : 0);
|
||||
w.writeUInt16(300); // lastRollingCode
|
||||
w.writeFloat(50, 5); // myPos
|
||||
w.writeFloat(-1, 5); // myTiltPos
|
||||
w.writeFloat(25, 5); // currentPos
|
||||
w.writeFloat(0, 5, CFG_REC_END);
|
||||
return w.text;
|
||||
}
|
||||
static void test_a_version_4_shade() {
|
||||
const std::string text = version4Shade();
|
||||
TextReader r(text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(4), got);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.shadeId);
|
||||
TEST_ASSERT_TRUE(got.paired);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(shade_types::roller), static_cast<uint8_t>(got.shadeType));
|
||||
TEST_ASSERT_EQUAL_HEX32(0x111111, got.remoteAddress);
|
||||
TEST_ASSERT_EQUAL_STRING("Old", got.name);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(tilt_types::none), static_cast<uint8_t>(got.tiltType));
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(radio_proto::RTS), static_cast<uint8_t>(got.proto));
|
||||
TEST_ASSERT_EQUAL_UINT8(56, got.bitLength);
|
||||
TEST_ASSERT_EQUAL_UINT32(15000, got.upTime);
|
||||
TEST_ASSERT_EQUAL_UINT32(14000, got.downTime);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, got.tiltTime);
|
||||
TEST_ASSERT_EQUAL_UINT16(100, got.stepSize);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x222222, got.linkedRemotes[0]);
|
||||
TEST_ASSERT_EQUAL_HEX32(0, got.linkedRemotes[4]);
|
||||
TEST_ASSERT_EQUAL_HEX32(0, got.linkedRemotes[5]);
|
||||
TEST_ASSERT_EQUAL_UINT16(300, got.lastRollingCode);
|
||||
TEST_ASSERT_EQUAL_HEX8(0, got.flags);
|
||||
TEST_ASSERT_EQUAL_FLOAT(50, got.myPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(-1, got.myTiltPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(25, got.currentPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(0, got.currentTiltPos);
|
||||
TEST_ASSERT_FALSE(got.flipCommands);
|
||||
TEST_ASSERT_FALSE(got.flipPosition);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.repeats);
|
||||
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.gpioUp);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.roomId);
|
||||
TEST_ASSERT_EQUAL(text.size(), r.position());
|
||||
}
|
||||
// A restore reads into a record that starts as the shade it replaces, and
|
||||
// what an older version lacks stays as it was.
|
||||
static void test_what_an_older_version_lacks_stays_as_it_was() {
|
||||
const std::string text = version4Shade();
|
||||
TextReader r(text);
|
||||
shade_record_t got = aShade();
|
||||
readShadeRecord(r, header(4), got);
|
||||
TEST_ASSERT_EQUAL_UINT32(15000, got.upTime);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x222222, got.linkedRemotes[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(radio_proto::GP_Remote), static_cast<uint8_t>(got.proto));
|
||||
TEST_ASSERT_EQUAL_UINT16(250, got.stepSize);
|
||||
TEST_ASSERT_EQUAL_HEX32(0x200005, got.linkedRemotes[5]);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x21, got.flags);
|
||||
TEST_ASSERT_TRUE(got.flipCommands);
|
||||
TEST_ASSERT_TRUE(got.flipPosition);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.repeats);
|
||||
TEST_ASSERT_EQUAL_UINT8(25, got.gpioUp);
|
||||
TEST_ASSERT_EQUAL_UINT8(27, got.gpioMy);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.gpioFlags);
|
||||
TEST_ASSERT_EQUAL_UINT8(5, got.roomId);
|
||||
}
|
||||
// Version 2 saved positions as fractions and the favorite as a byte.
|
||||
static void test_a_version_2_shade_has_fractions() {
|
||||
TextWriter w;
|
||||
writeShadeStart(w, 1);
|
||||
w.writeBool(false); // tilt
|
||||
w.writeUInt8(56);
|
||||
w.writeUInt32(10000);
|
||||
w.writeUInt32(10000);
|
||||
w.writeUInt32(0);
|
||||
for(uint8_t j = 0; j < 5; j++) w.writeUInt32(0);
|
||||
w.writeUInt16(12);
|
||||
w.writeUInt8(40); // myPos
|
||||
w.writeFloat(0.5f, 5); // currentPos
|
||||
w.writeFloat(0, 5, CFG_REC_END);
|
||||
TextReader r(w.text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(2), got);
|
||||
TEST_ASSERT_EQUAL_FLOAT(40, got.myPos);
|
||||
TEST_ASSERT_EQUAL_FLOAT(50, got.currentPos);
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(tilt_types::none), static_cast<uint8_t>(got.tiltType));
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
// Version 13 had the sort order and no GPIOs or room.
|
||||
static void test_a_version_13_shade() {
|
||||
TextWriter w;
|
||||
writeShadeStart(w, 5);
|
||||
w.writeUInt8(0);
|
||||
w.writeUInt8(static_cast<uint8_t>(radio_proto::RTS));
|
||||
w.writeUInt8(80);
|
||||
w.writeUInt32(10000);
|
||||
w.writeUInt32(10000);
|
||||
w.writeUInt32(0);
|
||||
w.writeUInt16(200);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_LINKED_REMOTES; j++) w.writeUInt32(0);
|
||||
w.writeUInt16(77);
|
||||
w.writeUInt8(0x01); // flags
|
||||
w.writeFloat(-1, 5);
|
||||
w.writeFloat(-1, 5);
|
||||
w.writeFloat(100, 5);
|
||||
w.writeFloat(0, 5);
|
||||
w.writeBool(true); // flipCommands
|
||||
w.writeBool(false); // flipPosition
|
||||
w.writeUInt8(3); // repeats
|
||||
w.writeUInt8(8, CFG_REC_END);
|
||||
TextReader r(w.text);
|
||||
shade_record_t got;
|
||||
readShadeRecord(r, header(13), got);
|
||||
TEST_ASSERT_EQUAL_UINT8(80, got.bitLength);
|
||||
TEST_ASSERT_EQUAL_UINT16(200, got.stepSize);
|
||||
TEST_ASSERT_EQUAL_UINT16(77, got.lastRollingCode);
|
||||
TEST_ASSERT_EQUAL_HEX8(0x01, got.flags);
|
||||
TEST_ASSERT_EQUAL_FLOAT(100, got.currentPos);
|
||||
TEST_ASSERT_TRUE(got.flipCommands);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, got.repeats);
|
||||
TEST_ASSERT_EQUAL_INT8(8, got.sortOrder);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.gpioUp);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, got.roomId);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
static void writeGroupStart(TextWriter &w) {
|
||||
w.writeUInt8(2);
|
||||
w.writeUInt8(0);
|
||||
w.writeUInt32(0x333333);
|
||||
w.writeString("Group", CFG_NAME_LEN);
|
||||
w.writeUInt8(0);
|
||||
w.writeUInt8(56);
|
||||
}
|
||||
// Version 23 put the group's rolling code before its shades.
|
||||
static void test_a_version_23_group() {
|
||||
TextWriter w;
|
||||
writeGroupStart(w);
|
||||
w.writeUInt16(4321);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j < 2 ? j + 1 : 0);
|
||||
w.writeUInt8(1);
|
||||
w.writeUInt8(1);
|
||||
w.writeBool(false);
|
||||
w.writeUInt8(6, CFG_REC_END);
|
||||
TextReader r(w.text);
|
||||
group_record_t got;
|
||||
readGroupRecord(r, header(23), got);
|
||||
TEST_ASSERT_EQUAL_UINT16(4321, got.lastRollingCode);
|
||||
TEST_ASSERT_TRUE(got.hasRollingCode);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, got.linkedShades[1]);
|
||||
TEST_ASSERT_EQUAL_UINT8(6, got.roomId);
|
||||
TEST_ASSERT_EQUAL(w.text.size(), r.position());
|
||||
}
|
||||
static void test_a_version_11_group() {
|
||||
TextWriter w;
|
||||
writeGroupStart(w);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(j == 0 ? 4 : 0, j == SOMFY_MAX_GROUPED_SHADES - 1 ? CFG_REC_END : CFG_VALUE_SEP);
|
||||
TextReader r(w.text);
|
||||
group_record_t got;
|
||||
readGroupRecord(r, header(11), got);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.linkedShades[0]);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.repeats);
|
||||
TEST_ASSERT_EQUAL_INT8(1, got.sortOrder);
|
||||
TEST_ASSERT_FALSE(got.flipCommands);
|
||||
TEST_ASSERT_EQUAL_UINT16(0, got.lastRollingCode);
|
||||
}
|
||||
// Before version 23 a group had no rolling code, so a restore leaves the one
|
||||
// NVS holds.
|
||||
static void test_a_group_before_version_23_has_no_rolling_code() {
|
||||
TextWriter w;
|
||||
writeGroupStart(w);
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES; j++) w.writeUInt8(0, j == SOMFY_MAX_GROUPED_SHADES - 1 ? CFG_REC_END : CFG_VALUE_SEP);
|
||||
TextReader r(w.text);
|
||||
group_record_t got = aGroup();
|
||||
readGroupRecord(r, header(11), got);
|
||||
TEST_ASSERT_FALSE(got.hasRollingCode);
|
||||
TEST_ASSERT_EQUAL_UINT16(1234, got.lastRollingCode);
|
||||
TEST_ASSERT_TRUE(got.flipCommands);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, got.roomId);
|
||||
TextWriter v24;
|
||||
writeGroupRecord(v24, aGroup());
|
||||
TextReader r24(v24.text);
|
||||
readGroupRecord(r24, header(), got);
|
||||
TEST_ASSERT_TRUE(got.hasRollingCode);
|
||||
}
|
||||
// Before version 20 the settings had no update check, which stays as it was.
|
||||
static void test_version_19_settings_leave_the_update_check() {
|
||||
TextReader r("\"v2.0.0\",\"host\",\"pool.ntp.org\",\"\",true \n");
|
||||
settings_record_t got;
|
||||
got.checkForUpdate = false;
|
||||
readSettingsRecord(r, header(19), got);
|
||||
TEST_ASSERT_EQUAL_STRING("host", got.hostname);
|
||||
TEST_ASSERT_TRUE(got.ssdpBroadcast);
|
||||
TEST_ASSERT_FALSE(got.checkForUpdate);
|
||||
}
|
||||
// Before version 22 the network record had no MQTT settings.
|
||||
static void test_a_version_21_network_has_no_mqtt() {
|
||||
TextReader r(" 1,true ,\"\",\"\",\"\",\"\",\"\", 1, 0, 0, 0, -1, 23, 18\n 9\n");
|
||||
net_record_t got;
|
||||
readNetRecord(r, header(21), got, true);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, got.connType);
|
||||
TEST_ASSERT_EQUAL_STRING("", got.mqttHostname);
|
||||
TEST_ASSERT_EQUAL_UINT16(1883, got.mqttPort);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, got.ethPWRPin);
|
||||
r.beginRecord();
|
||||
TEST_ASSERT_EQUAL_UINT8(9, r.readUInt8(0));
|
||||
}
|
||||
|
||||
// A real backup, which isn't in the repo: it holds the remotes' addresses.
|
||||
// CONFIG_BACKUP=<path> pio test -e native -f test_configrecords
|
||||
static void test_a_real_backup_reads_to_its_end() {
|
||||
const char *path = getenv("CONFIG_BACKUP");
|
||||
if(!path) TEST_IGNORE_MESSAGE("set CONFIG_BACKUP to a backup file");
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
TEST_ASSERT_TRUE_MESSAGE(in.good(), "can't open CONFIG_BACKUP");
|
||||
std::stringstream ss;
|
||||
ss << in.rdbuf();
|
||||
TextReader r(ss.str());
|
||||
config_header_t h;
|
||||
readConfigHeader(r, h);
|
||||
TEST_ASSERT_EQUAL(static_cast<uint32_t>(h.length), r.position());
|
||||
for(uint8_t i = 0; i < h.roomRecords; i++) {
|
||||
room_record_t rec;
|
||||
readRoomRecord(r, rec);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rec.roomId);
|
||||
}
|
||||
for(uint8_t i = 0; i < h.shadeRecords; i++) {
|
||||
shade_record_t rec;
|
||||
readShadeRecord(r, h, rec);
|
||||
TEST_ASSERT_TRUE(rec.shadeId >= 1 && rec.shadeId <= SOMFY_MAX_SHADES);
|
||||
TEST_ASSERT_TRUE(rec.currentPos >= 0 && rec.currentPos <= 100);
|
||||
// Written back, it is the record it was read from.
|
||||
TextWriter w;
|
||||
writeShadeRecord(w, rec);
|
||||
TEST_ASSERT_EQUAL(h.shadeRecordSize, w.text.size());
|
||||
}
|
||||
for(uint8_t i = 0; i < h.groupRecords; i++) {
|
||||
group_record_t rec;
|
||||
readGroupRecord(r, h, rec);
|
||||
TEST_ASSERT_TRUE(rec.groupId >= 1 && rec.groupId <= SOMFY_MAX_GROUPS);
|
||||
}
|
||||
for(uint8_t i = 0; i < h.repeaterRecords; i++) {
|
||||
uint32_t repeaters[SOMFY_MAX_REPEATERS];
|
||||
readRepeaterRecord(r, repeaters);
|
||||
}
|
||||
settings_record_t settings;
|
||||
readSettingsRecord(r, h, settings);
|
||||
TEST_ASSERT_EQUAL_UINT8('v', settings.fwVersion[0]);
|
||||
net_record_t net;
|
||||
readNetRecord(r, h, net, true);
|
||||
trans_record_t trans;
|
||||
readTransRecord(r, trans);
|
||||
TEST_ASSERT_TRUE(trans.frequency > 300 && trans.frequency < 950);
|
||||
TEST_ASSERT_EQUAL(ss.str().size(), r.position());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_each_record_is_the_size_the_header_declares);
|
||||
RUN_TEST(test_a_counter_measures_what_a_writer_writes);
|
||||
RUN_TEST(test_a_deleted_shade_is_written_cleared);
|
||||
RUN_TEST(test_the_header_reads_back);
|
||||
RUN_TEST(test_a_version_12_header_has_no_rooms_repeaters_or_settings);
|
||||
RUN_TEST(test_a_shade_reads_back);
|
||||
RUN_TEST(test_a_room_reads_back);
|
||||
RUN_TEST(test_a_group_reads_back);
|
||||
RUN_TEST(test_a_group_reads_its_shades_without_gaps);
|
||||
RUN_TEST(test_the_settings_read_back);
|
||||
RUN_TEST(test_the_network_reads_back_with_ethernet);
|
||||
RUN_TEST(test_the_radio_reads_back);
|
||||
RUN_TEST(test_a_name_with_a_comma_keeps_the_record_in_line);
|
||||
RUN_TEST(test_the_longest_name_reads_back);
|
||||
RUN_TEST(test_the_longest_settings_read_back);
|
||||
RUN_TEST(test_the_longest_mqtt_settings_read_back);
|
||||
RUN_TEST(test_a_value_too_long_is_cut);
|
||||
RUN_TEST(test_skipping_the_network_of_a_board_without_ethernet_finds_the_radio);
|
||||
RUN_TEST(test_a_network_record_without_ethernet_keeps_the_ethernet_defaults);
|
||||
RUN_TEST(test_the_radio_reads_whole_whatever_its_declared_size);
|
||||
RUN_TEST(test_values_a_newer_version_added_are_skipped);
|
||||
RUN_TEST(test_a_short_record_never_reads_the_next);
|
||||
RUN_TEST(test_skipping_records_passes_their_newlines);
|
||||
RUN_TEST(test_a_version_4_shade);
|
||||
RUN_TEST(test_what_an_older_version_lacks_stays_as_it_was);
|
||||
RUN_TEST(test_a_version_2_shade_has_fractions);
|
||||
RUN_TEST(test_a_version_13_shade);
|
||||
RUN_TEST(test_a_version_23_group);
|
||||
RUN_TEST(test_a_version_11_group);
|
||||
RUN_TEST(test_a_group_before_version_23_has_no_rolling_code);
|
||||
RUN_TEST(test_version_19_settings_leave_the_update_check);
|
||||
RUN_TEST(test_a_version_21_network_has_no_mqtt);
|
||||
RUN_TEST(test_a_real_backup_reads_to_its_end);
|
||||
return UNITY_END();
|
||||
}
|
||||
216
test/test_mqttdisco/test_mqttdisco.cpp
Normal file
216
test/test_mqttdisco/test_mqttdisco.cpp
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
#include <unity.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "MqttDisco.h"
|
||||
#include "MqttTopics.h"
|
||||
#include "ShadeChanges.h"
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static shade_disco_t shade(shade_types type = shade_types::roller) {
|
||||
shade_disco_t s;
|
||||
s.shadeId = 3;
|
||||
s.name = "Kitchen";
|
||||
s.shadeType = type;
|
||||
s.rootTopic = "somfy";
|
||||
s.discoTopic = "homeassistant";
|
||||
s.serverId = "abc123";
|
||||
s.hostname = "ESPSomfyRTS";
|
||||
s.fwVersion = "v4.0.0";
|
||||
return s;
|
||||
}
|
||||
// The config Home Assistant gets and the topic it goes to.
|
||||
struct disco_t {
|
||||
JsonDocument doc;
|
||||
char topic[128] = "";
|
||||
};
|
||||
static void build(const shade_disco_t &s, disco_t &d) {
|
||||
shadeDisco(s, d.doc.to<JsonObject>(), d.topic, sizeof(d.topic));
|
||||
}
|
||||
|
||||
static void test_a_roller_is_a_cover_under_the_discovery_topic() {
|
||||
disco_t d;
|
||||
build(shade(), d);
|
||||
TEST_ASSERT_EQUAL_STRING("homeassistant/cover/3/config", d.topic);
|
||||
TEST_ASSERT_EQUAL_STRING("somfy/shades/3", d.doc["~"]);
|
||||
TEST_ASSERT_EQUAL_STRING("Kitchen", d.doc["name"]);
|
||||
TEST_ASSERT_EQUAL_STRING("mqtt_abc123_shade3", d.doc["unique_id"]);
|
||||
TEST_ASSERT_EQUAL_STRING("shade", d.doc["device_class"]);
|
||||
TEST_ASSERT_TRUE(d.doc["enabled_by_default"].as<bool>());
|
||||
}
|
||||
// Every shade of a device is grouped under it in Home Assistant.
|
||||
static void test_the_device_is_the_esp() {
|
||||
disco_t d;
|
||||
build(shade(), d);
|
||||
JsonObject device = d.doc["device"];
|
||||
TEST_ASSERT_EQUAL_STRING("mqtt_espsomfyrts_abc123", device["identifiers"][0]);
|
||||
TEST_ASSERT_EQUAL_STRING("ESPSomfyRTS", device["name"]);
|
||||
TEST_ASSERT_EQUAL_STRING("v4.0.0", device["hw_version"]);
|
||||
TEST_ASSERT_EQUAL_STRING("ESPSomfy-RTS MQTT", device["model"]);
|
||||
TEST_ASSERT_EQUAL_STRING("somfy/status", d.doc["availability_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("online", d.doc["payload_available"]);
|
||||
TEST_ASSERT_EQUAL_STRING("offline", d.doc["payload_not_available"]);
|
||||
}
|
||||
// The topics under ~ are the ones MqttCommands listens on and the shade
|
||||
// publishes.
|
||||
static void test_a_cover_moves_through_the_shade_topics() {
|
||||
disco_t d;
|
||||
build(shade(), d);
|
||||
TEST_ASSERT_EQUAL_STRING("~/direction/set", d.doc["command_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/target/set", d.doc["set_position_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/position", d.doc["position_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/direction", d.doc["state_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("0", d.doc["payload_stop"]);
|
||||
TEST_ASSERT_EQUAL_STRING("0", d.doc["state_stopped"]);
|
||||
TEST_ASSERT_FALSE(d.doc["tilt_command_topic"].is<const char *>());
|
||||
}
|
||||
// 0 is open: an upward move opens.
|
||||
static void test_a_roller_opens_up() {
|
||||
disco_t d;
|
||||
build(shade(), d);
|
||||
TEST_ASSERT_EQUAL_STRING("-1", d.doc["payload_open"]);
|
||||
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_close"]);
|
||||
TEST_ASSERT_EQUAL(0, d.doc["position_open"].as<int>());
|
||||
TEST_ASSERT_EQUAL(100, d.doc["position_closed"].as<int>());
|
||||
TEST_ASSERT_EQUAL_STRING("-1", d.doc["state_opening"]);
|
||||
TEST_ASSERT_EQUAL_STRING("1", d.doc["state_closing"]);
|
||||
}
|
||||
static void test_a_flipped_position_opens_at_100() {
|
||||
shade_disco_t s = shade();
|
||||
s.flipPosition = true;
|
||||
disco_t d;
|
||||
build(s, d);
|
||||
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_open"]);
|
||||
TEST_ASSERT_EQUAL_STRING("-1", d.doc["payload_close"]);
|
||||
TEST_ASSERT_EQUAL(100, d.doc["position_open"].as<int>());
|
||||
TEST_ASSERT_EQUAL(0, d.doc["position_closed"].as<int>());
|
||||
TEST_ASSERT_EQUAL_STRING("1", d.doc["state_opening"]);
|
||||
TEST_ASSERT_EQUAL_STRING("-1", d.doc["state_closing"]);
|
||||
}
|
||||
// An awning opens downward, and flipped opens up again.
|
||||
static void test_an_awning_opens_down() {
|
||||
disco_t d;
|
||||
build(shade(shade_types::awning), d);
|
||||
TEST_ASSERT_EQUAL_STRING("awning", d.doc["device_class"]);
|
||||
TEST_ASSERT_EQUAL_STRING("1", d.doc["payload_open"]);
|
||||
TEST_ASSERT_EQUAL(100, d.doc["position_open"].as<int>());
|
||||
shade_disco_t s = shade(shade_types::awning);
|
||||
s.flipPosition = true;
|
||||
disco_t flipped;
|
||||
build(s, flipped);
|
||||
TEST_ASSERT_EQUAL_STRING("-1", flipped.doc["payload_open"]);
|
||||
TEST_ASSERT_EQUAL(0, flipped.doc["position_open"].as<int>());
|
||||
}
|
||||
static void test_each_kind_of_shade_has_its_device_class() {
|
||||
const struct { shade_types type; const char *deviceClass; } kinds[] = {
|
||||
{shade_types::roller, "shade"},
|
||||
{shade_types::blind, "blind"},
|
||||
{shade_types::ldrapery, "curtain"},
|
||||
{shade_types::rdrapery, "curtain"},
|
||||
{shade_types::cdrapery, "curtain"},
|
||||
{shade_types::lgate, "curtain"},
|
||||
{shade_types::cgate1, "curtain"},
|
||||
{shade_types::garage1, "garage"},
|
||||
{shade_types::garage3, "garage"},
|
||||
{shade_types::shutter, "shutter"},
|
||||
{shade_types::awning, "awning"},
|
||||
};
|
||||
for(const auto &kind : kinds) {
|
||||
disco_t d;
|
||||
build(shade(kind.type), d);
|
||||
TEST_ASSERT_EQUAL_STRING(kind.deviceClass, d.doc["device_class"]);
|
||||
}
|
||||
}
|
||||
static void test_a_tilting_blind_has_the_tilt_topics() {
|
||||
shade_disco_t s = shade(shade_types::blind);
|
||||
s.tiltType = tilt_types::tiltmotor;
|
||||
disco_t d;
|
||||
build(s, d);
|
||||
TEST_ASSERT_EQUAL_STRING("~/tiltTarget/set", d.doc["tilt_command_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/tiltPosition", d.doc["tilt_status_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/direction/set", d.doc["command_topic"]);
|
||||
}
|
||||
// A tilt only blind doesn't open or close, so it has only the tilt.
|
||||
static void test_a_tilt_only_blind_has_only_the_tilt() {
|
||||
shade_disco_t s = shade(shade_types::blind);
|
||||
s.tiltType = tilt_types::tiltonly;
|
||||
disco_t d;
|
||||
build(s, d);
|
||||
TEST_ASSERT_EQUAL_STRING("~/tiltTarget/set", d.doc["tilt_command_topic"]);
|
||||
TEST_ASSERT_FALSE(d.doc["command_topic"].is<const char *>());
|
||||
TEST_ASSERT_FALSE(d.doc["set_position_topic"].is<const char *>());
|
||||
// Sent as null so Home Assistant drops its defaults.
|
||||
std::string json;
|
||||
serializeJson(d.doc, json);
|
||||
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_open\":null"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_close\":null"));
|
||||
TEST_ASSERT_NOT_NULL(strstr(json.c_str(), "\"payload_stop\":null"));
|
||||
}
|
||||
// A dry contact is on (100) or off (0).
|
||||
static void test_a_dry_contact_is_a_switch() {
|
||||
for(shade_types type : {shade_types::drycontact, shade_types::drycontact2}) {
|
||||
disco_t d;
|
||||
build(shade(type), d);
|
||||
TEST_ASSERT_EQUAL_STRING("homeassistant/switch/3/config", d.topic);
|
||||
TEST_ASSERT_FALSE(d.doc["device_class"].is<const char *>());
|
||||
TEST_ASSERT_EQUAL(100, d.doc["payload_on"].as<int>());
|
||||
TEST_ASSERT_EQUAL(0, d.doc["payload_off"].as<int>());
|
||||
TEST_ASSERT_EQUAL(100, d.doc["state_on"].as<int>());
|
||||
TEST_ASSERT_EQUAL(0, d.doc["state_off"].as<int>());
|
||||
TEST_ASSERT_EQUAL_STRING("~/position", d.doc["state_topic"]);
|
||||
TEST_ASSERT_EQUAL_STRING("~/target/set", d.doc["command_topic"]);
|
||||
TEST_ASSERT_FALSE(d.doc["payload_open"].is<const char *>());
|
||||
}
|
||||
}
|
||||
// Deleting a shade clears both, as its kind may have changed.
|
||||
static void test_the_config_topic_of_each_kind() {
|
||||
char topic[64];
|
||||
shadeDiscoTopic("ha", 12, false, topic, sizeof(topic));
|
||||
TEST_ASSERT_EQUAL_STRING("ha/cover/12/config", topic);
|
||||
shadeDiscoTopic("ha", 12, true, topic, sizeof(topic));
|
||||
TEST_ASSERT_EQUAL_STRING("ha/switch/12/config", topic);
|
||||
TEST_ASSERT_TRUE(isDiscoSwitch(shade_types::drycontact2));
|
||||
TEST_ASSERT_FALSE(isDiscoSwitch(shade_types::garage1));
|
||||
}
|
||||
|
||||
static bool hasTopic(const char *const *topics, size_t count, const std::string &topic) {
|
||||
for(size_t i = 0; i < count; i++) if(topic == topics[i]) return true;
|
||||
return false;
|
||||
}
|
||||
static void keepRetained(const char *topic, int32_t value, bool retain, void *ctx) {
|
||||
(void)value;
|
||||
if(retain) static_cast<std::vector<std::string> *>(ctx)->push_back(topic);
|
||||
}
|
||||
// A deleted shade leaves none of its topics at the broker.
|
||||
static void test_deleting_a_shade_clears_every_state_topic_the_broker_keeps() {
|
||||
shade_snapshot_t shade{};
|
||||
std::vector<std::string> retained;
|
||||
publishShadeTopics(SHADE_CHANGED_TOPICS, shade, keepRetained, &retained);
|
||||
TEST_ASSERT_FALSE(retained.empty());
|
||||
for(const std::string &topic : retained)
|
||||
TEST_ASSERT_TRUE_MESSAGE(hasTopic(SHADE_TOPICS, SHADE_TOPIC_COUNT, topic), topic.c_str());
|
||||
}
|
||||
// Group unpublish named it SunSensor, so a deleted group's stayed.
|
||||
static void test_deleting_a_group_clears_its_sun_sensor_topic() {
|
||||
TEST_ASSERT_TRUE(hasTopic(GROUP_TOPICS, GROUP_TOPIC_COUNT, "sunSensor"));
|
||||
TEST_ASSERT_FALSE(hasTopic(GROUP_TOPICS, GROUP_TOPIC_COUNT, "SunSensor"));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_a_roller_is_a_cover_under_the_discovery_topic);
|
||||
RUN_TEST(test_the_device_is_the_esp);
|
||||
RUN_TEST(test_a_cover_moves_through_the_shade_topics);
|
||||
RUN_TEST(test_a_roller_opens_up);
|
||||
RUN_TEST(test_a_flipped_position_opens_at_100);
|
||||
RUN_TEST(test_an_awning_opens_down);
|
||||
RUN_TEST(test_each_kind_of_shade_has_its_device_class);
|
||||
RUN_TEST(test_a_tilting_blind_has_the_tilt_topics);
|
||||
RUN_TEST(test_a_tilt_only_blind_has_only_the_tilt);
|
||||
RUN_TEST(test_a_dry_contact_is_a_switch);
|
||||
RUN_TEST(test_the_config_topic_of_each_kind);
|
||||
RUN_TEST(test_deleting_a_shade_clears_every_state_topic_the_broker_keeps);
|
||||
RUN_TEST(test_deleting_a_group_clears_its_sun_sensor_topic);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -47,6 +47,16 @@ static void test_wraps_to_zero_after_0xFFFF() {
|
|||
TEST_ASSERT_EQUAL_UINT16(0, next(true, 0xFFFF, store));
|
||||
}
|
||||
|
||||
// NVS is newer than shades.cfg, so a load keeps the higher code.
|
||||
static void test_a_load_keeps_the_higher_rolling_code() {
|
||||
TEST_ASSERT_EQUAL_UINT16(500, rollingCodeAfterLoad(500, 300, false));
|
||||
TEST_ASSERT_EQUAL_UINT16(700, rollingCodeAfterLoad(500, 700, false));
|
||||
}
|
||||
static void test_a_restore_takes_the_backups_rolling_code() {
|
||||
TEST_ASSERT_EQUAL_UINT16(300, rollingCodeAfterLoad(500, 300, true));
|
||||
TEST_ASSERT_EQUAL_UINT16(700, rollingCodeAfterLoad(500, 700, true));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
|
@ -55,5 +65,7 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_synced_never_reads_the_store);
|
||||
RUN_TEST(test_reading_does_not_use_up_a_code);
|
||||
RUN_TEST(test_wraps_to_zero_after_0xFFFF);
|
||||
RUN_TEST(test_a_load_keeps_the_higher_rolling_code);
|
||||
RUN_TEST(test_a_restore_takes_the_backups_rolling_code);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue