Repository navigation
Releases
0.11.3
Fixed
- ESP32: saving and loading a profile no longer logs error lines for keys that
are absent by design —nvs_erase_key fail: ip NOT_FOUNDand the rest of the
static-address keys under DHCP,nvs_get_str len fail: host NOT_FOUNDfor an
unset hostname,nvs_get_blob len fail: mac NOT_FOUNDwithout a MAC override.
The profiles askisKey()beforeremove(),getString()andgetBytes()
(detail/prefs_tools.h); a real failure is still logged. ESP8266 and AVR are
unchanged
0.11.2
0.11.1
Fixed
- ESP32-P4 (and any chip without a radio): the WiFi profile's constructor no
longer asks the MAC table for a station address the chip does not have, so
the boot log losesE system_api: 0 mac type is incorrect. Nothing else
changes: the factory MAC was already unknown there until the adapter reports it
0.11.0
Added
hasMacOverride(): whether an active MAC override is sethasFactoryMac()/setFactoryMac(): the factory MAC can be unknown at
construction — WiFi on an ESP32-P4, whose station address belongs to the
ESP-Hosted co-processor. The adapter reports it once the interface is up;
accepted once, while it is unknown
Changed
getMac()returnsfalse, with the buffer zero-filled, when the address
asked for is the factory one and it is not known;toJson()gives"mac":""
then. Before, an unknown address came back as00:00:00:00:00:00andtrueisValidMac()refuses group (multicast) addresses: the I/G bit, bit 0 of
byte 0, set. This includes the broadcast address it already refused. The
drivers refuse such an address, so an override that passed would only fail at
the adaptersaveCfg()stores themackey only when an override is set, and removes it
otherwise — the convention the static-address keys already follow- The generated default hostname is built from the chip's base MAC on ESP32
(ESP_MAC_BASE), not from the interface's address. Unchanged for WiFi, where
the station address is the base. An Ethernet profile on ESP32 gets a new
default hostname — its address is base+3, or a local one — which now matches
the WiFi profile's, as the documentation always said it did
Fixed
WiFiProfileandEthProfileon ESP32 ignored a failedesp_read_mac(). On
an ESP32-P4, which has no radio of its own, the station address is not in the
chip's MAC table: the factory MAC came out as all zeros, the default hostname
asESP-000000
0.10.1
Fixed
MqttProfile::MAX_TOPIC_LENdefaulted to 64, less than the application's
typical default base topicsensors/<hostname>with a hostname at its
longest (71), so that default could neither be typed in nor noted as the
last-announced topic. It is nowNETWORK_PROFILE_HOSTNAME_LEN + 16(79)
0.10.0
Added
MqttProfile: the device's base topic,MqttConfig::baseTopic— the
namespace under which the suite's convention putsstate,config,
statusandset/+, and from which the discovery messages take their
addresses. Empty means the application's default (effectiveBaseTopic()),
so a device follows its hostname until a topic is written in. Checked by
isValidBaseTopic()(ConfigCheck::BadTopic): no wildcard, no leading,
trailing or doubled/, no control characters, at mostMAX_TOPIC_LEN
(MQTT_PROFILE_MAX_TOPIC_LEN, 64). In the backup asbaseTopicMqttProfile::getLastBaseTopic()/setLastBaseTopic(): the topic the
device last announced on, stored beside the settings as device state, so
that an application can clear the retained messages on the old one after
the base changed. Not inMqttConfigand not in the backup- Records written before these fields load with an empty base topic
0.9.0
Added
SyslogProfile: the settings a device stores to send its log to a syslog
server — address (validated through Host), port, an RFC 5424 severity floor
and a facility — with the samegetConfig/setConfig/checkConfig,
saveCfg/loadCfg/clearCfgandtoJsonsurface asMqttProfile. It
holds no socket and frames nothing; a sender reads it.NETWORK_PROFILE_VERSIONis now900.
0.8.0
Added
MqttProfile::MqttConfiggainshaDiscovery, a flag for whether the
application should publish Home Assistant MQTT discovery messages. Topics
remain absent by design: those are project-specific addresses, while this is a
yes/no about a broker-side convention. A base field rather than a subclass's
because it has no natural siblings — a second convention would make it an enum
rather than add a second boolean — and because a derived field is invisible to
MqttConfigComponent, which works throughMqttConfig.NetworkProfileVersion.hdefinesNETWORK_PROFILE_VERSIONas
major * 10000 + minor * 100 + patch, so a consumer can require a minimum
version at compile time. Libraries built on this one deliberately do not
declare it as a dependency — AsyncConfigPortal's network and MQTT modules are
opt-in — so a manifest entry cannot express the constraint and the check has
to live in the source. An undefined macro is 0 in#if, so the same test also
catches the releases from before this header existed.
Carried by_doSave(),_doLoad()andtoJson(); defaults to false, so
records written before the field existed load as "off", which is what those
devices were doing.
0.7.0
Fixed
-
loadCfg()could restore a value the last save did not contain.saveCfg()
removes the key of an empty string rather than storing a blank, and
Preferences::getString()/getBytes()leave the destination untouched when a
key is absent — so clearing the hostname, SSID, password, MAC or MQTT broker
address, saving, and then loading back into the same object handed the old
value straight back, and the store read as agreeing with it. The affected
fields are now reset to their defaults before they are read, in
NetworkProfile,WiFiProfileandMqttProfilealike. The numeric fields
were never affected: they pass a default to the getter, which does the same
job. Neither was a load into a freshly constructed profile, which is the usual
boot path — this only ever bit a reload into a populated object, such as a
configuration page reverting its edits.MqttProfile::_doLoad()additionally passed the current value as each
getter's default, which had the same effect on the bools and the port; it now
passes a default-constructedMqttConfig's. -
WiFiProfile(const WiFiConfig&)droppedcfg.txPower. The constructor applied
the SSID and the password but left the transmit power at the platform default,
whilesetConfig(WiFiConfig)applied all three — so the same struct produced
two different profiles depending on whether it arrived through the constructor
or the setter. It is now validated and stored like the other two, and ignored
the same way when invalid, since a constructor cannot report a rejection. -
WiFiProfile::_doLoad()could load a TX power of NAN.
Preferences::getFloat()answers a missing key with NAN unless given a
default, and the call passed none — so a namespace written beforetxpwr
existed came back with a poisoned_txPower, after which every
isValidTxPower()comparison against it was false and the radio never got a
usable value. It now falls back toDEFAULT_WIFI_TX_POWER_dBm, and does the
same for a stored value outside what this build accepts: the range is not the
same on both platforms (ESP32 discrete levels up to 19.5 dBm, ESP8266
continuous up to 20.5), so a namespace carried between them, or a build whose
WIFI_PROFILE_DEFAULT_WIFI_TX_POWERchanged, can hold one the radio would
refuse. The rest of the profile still loads andloadCfg()still returns
true; only the TX power is replaced.
Added
-
NtpProfile::clearCfg()andMqttProfile::clearCfg(), matching
NetworkProfile's. Both classes could write a namespace and read it back but
not erase it, which left a factory reset unable to finish the job: a device
wiped of its network configuration still came back holding the time servers it
synchronised from and — worse — the broker's address and the credentials for
it.MqttProfileleaves the password out oftoJson()precisely because a
backup file travels, so leaving it in flash through a reset was the same
concern answered two different ways.Same contract as
NetworkProfile::clearCfg(), including that it clears the
store rather than the object: the instance keeps what it holds until something
loads over it. -
NetworkProfile::configCheckName(), naming aConfigCheckvalue for logs and
configuration UIs.checkConfig()exists so a caller can report which field
was refused rather than only that something was, but with no name for the
value every caller had to write the same nine-case switch — so in practice
callers reported nothing, or skipped the check. The string is flash-resident,
costing no RAM on AVR, and names the field rather than the remedy: a UI knows
better than this library how to phrase advice to its user. -
NtpProfile— the NTP servers as a device-level setting, withgetServer()/
setServer(),checkConfig(),saveCfg()/loadCfg()andtoJson(). A
device has one clock, so the servers it synchronises from belong to the device,
not to whichever interface is up: keeping a copy in everyNetworkProfilemeant
the samepool.ntp.orgtyped twice, stored twice, and carried twice in the
largest document the portal serves. Where interfaces really do reach different
time servers, twoNtpProfileinstances say so plainly. -
NtpProfile::importLegacy()reads servers written by an earlier version, which
stored them per interface underntp0..ntpNin the network profile's own
namespace. Without it an upgraded device would come back with no time servers
and no indication why. Scheduled for removal in 1.0.0. -
detail/ntp_config.hholdsNTP_PROFILE_SERVER_COUNTon its own, because two
callers ask different things of it:NtpProfile.hasks how many servers it
holds and refuses to be included when the answer is none, while NetworkManager
asks whether to compile its NTP support and must be able to ask that when the
answer is no. -
NTP_PROFILE_ENABLEDdecides whether NTP exists in the build;
NTP_PROFILE_SERVER_COUNTsays how many servers a profile holds, and is now a
count in its own right (1..3) rather than a switch that also happened to be a
number. Zero no longer disables anything and is refused with a message naming
the replacement — a profile holding no servers was never a coherent state, which
is why the class had to error out on it. Defaults: NTP off on AVR (the switch
also guards a UDP SNTP client there, not just a buffer), three servers on ESP32,
one everywhere else. Code sizes buffers fromNtpProfile::SERVER_COUNTrather
than the macro. -
NTP_PROFILE_SERVER_COUNTreplacesNETWORK_PROFILE_NTP_SERVER_COUNT; the old
name is reported by a#warningrather than ignored, since a build still
passing it would silently get the default count. -
json_cat_esc()indetail/json_tools.h— appends a string escaped, without
a scratch buffer. A fully escaped host name needs six bytes per character, and
a temporary that size has no business on an ESP8266 stack, least of all inside
an async request handler.MqttProfile::toJson()uses it.
Changed
-
NtpProfile::setConfig()andMqttProfile::setConfig()validate what they are
given, through their owncheckConfig(), and store nothing unless it passes.
Both classes already had the check written; it simply was not wired in, so the
same object accepted throughsetConfig()what it refused through
setServer(). A stored address the resolver will never accept is a clock that
silently never syncs, or a broker never reached — the kind of failure that
looks like a network fault.The three profile families now answer the same way:
setConfig()refuses an
invalid struct everywhere.checkConfig()here is virtual, so unlike
NetworkProfile's staticisValidConfig(), a subclass's own rules apply
through it automatically.What still passes is unchanged where it matters: an
NtpProfilewith every
slot empty is a device told to take its time from DHCP, and a disabled
MqttProfilekeeps whatever half-filled form the user switched off. The
portal pattern — stage a config, check it, keep it — works as before; it just
no longer matters whether the check was remembered.This is a tightening. A struct that
setConfig()used to swallow is now
refused, and agetConfig()→ edit →setConfig()round-trip of something
loaded from an older or hand-edited namespace can fail where it did not.
loadCfg()andimportLegacy()deliberately stay permissive: the check
belongs where a new value arrives, not where an existing one is recovered. -
NtpProfileandMqttProfilegainedconfigCheckName(), matching
NetworkProfile's. Each class has its ownConfigCheckenum, so one shared
function could not have covered them; without their own, a caller reporting
which field was refused had to write the switch that the third class had
been given. Now thatsetConfig()can refuse, the name is what makes the
refusal reportable. -
detail/prefs_tools.hnames AVR instead of catching it with#else, and an
unsupported architecture now fails at the include with a message saying what a
port would need.EEPROMPreferences.hdeclares its class unconditionally and
guards only its.cpp, so the#elsebranch compiled on a fourth
architecture and then failed at link time withundefined reference to EEPROMPreferences::begin— a long way from the cause.NetworkProfile.h
already used the narrower guard; the two now agree, and the message follows
the oneWiFiProfile.hhas carried for the same situation. -
ESP8266:
isValidTxPower()now also requires the value to sit on the 0.25 dBm
grid, not merely inside
[MIN_WIFI_TX_POWER_dBm,MAX_WIFI_TX_POWER_dBm].setOutputPower()
truncates to the grid on its way to the radio, so a value between two steps
was accepted, stored and reported back as something the radio never produced —
the same silent step-down the ESP32 branch already existed to catch. The two
platforms now ask the same question about the grid and differ only where they
genuinely do: ESP32 additionally accepts just the dozen discrete levels its
radio offers.API.mdalready documented ESP8266 as accepting any multiple of 0.25 dBm,
so this brings the code to the documentation rather than the other way round.
The prose above that table, which said every value in range was accepted,
contradicted it and has been corrected.This narrows what is accepted. A stored TX power off the grid now fails
validation, andloadCfg()replaces it withDEFAULT_WIFI_TX_POWER_dBmas it
does for any other unacceptable value. -
toJson()printstxpwrwith two decimals instead of%f's six
("txpwr":19.50). Two are exact for the 0.25 dBm grid now enforced on both
platforms, and the other four were alw...
0.6.0
[0.6.0] - 2026-08-05
Added
MqttProfile— stored MQTT broker settings (enabled, host, port, TLS,
username, password) withcheckConfig(),saveCfg()/loadCfg()and
toJson(). It lives here rather than in a library of its own for the same
reason the NTP servers do: it is something a networked device stores in order
to reach a service, and the two are always deployed together. It knows nothing
about MQTT itself — no client, no topics, no connection — so the same profile
serves an ESP32 task, an ESP8266 loop and an AVR without a portal.
Topics are deliberately absent, being project-specific; the virtual hooks are
there for a subclass that needs them.enabledis a field of its own rather than an empty host standing in for it:
"switched off" and "not configured yet" are different facts, and a device taken
off the broker for a while should not have to forget its address to do it.MqttProfilelocks the same wayNetworkProfiledoes — astd::timed_mutex
where preemption exists (ESP32), a no-op on AVR and ESP8266 — because the
settings are copied field by field: the portal saving while a client task
reads would otherwise hand out half of each.getConfig()andsetConfig()
returnboolaccordingly.checkConfig()takes the config as a parameter, as
NetworkProfile's does, so validation runs on a staged copy and needs no lock
at all — but it is virtual rather than static, since a static one called
through a base reference silently skips a subclass's own rules.detail/prefs_tools.hwithprefs_putStr()— the empty-string storage rule
(store non-empty, remove the key otherwise) now has one implementation instead
of one per profile class.
Changed
-
Passing a subclass's config to a base-class
checkConfig(),isValidConfig()
orsetConfig()is now a compile error rather than a silent half-check. These
are static, so the compiler picks by declared type: aWiFiConfighanded to
the base version — which is what happens the moment a caller holds a
NetworkProfile&— checked the addressing, skipped the SSID, password and
TX-power rules, and reported Ok. Deleted derived-type overloads now catch it.Where it fires, either call through the concrete type
(WiFiProfile::checkConfig(cfg)), which is nearly always what was meant, or
state that only the addressing is of interest:
checkConfig(static_cast<const NetworkConfig&>(cfg)). Slicing a config into a
NetworkConfigvariable first still loses the subclass fields — by then they
are gone, and no signature can catch that.MqttProfilecarries the same
guard.Impact on the dependent libraries was one call site, inside
WiFiProfile
itself;NetworkManager,AsyncConfigPortalandAsyncWiFiPortalall already
called through the concrete type or cast explicitly.
Note
- The MQTT password is not written to
toJson(), matchingWiFiProfile: a
backup file may travels through mail and cloud storage. Restoring one
therefore needs the broker password entered again.