Skip to content

Releases

0.11.3

Choose a tag to compare

@soosp soosp released this 28 Oct 23:24

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_FOUND and the rest of the
    static-address keys under DHCP, nvs_get_str len fail: host NOT_FOUND for an
    unset hostname, nvs_get_blob len fail: mac NOT_FOUND without a MAC override.
    The profiles ask isKey() before remove(), getString() and getBytes()
    (detail/prefs_tools.h); a real failure is still logged. ESP8266 and AVR are
    unchanged

0.11.2

Choose a tag to compare

@soosp soosp released this 28 Oct 18:36

Fixed

  • Sync version across all occurrences (CHANGELOG.md, library.json, library.properties)

0.11.1

Choose a tag to compare

@soosp soosp released this 28 Oct 18:28

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 loses E 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

Choose a tag to compare

@soosp soosp released this 28 Oct 08:23

Added

  • hasMacOverride(): whether an active MAC override is set
  • hasFactoryMac() / 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() returns false, 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 as 00:00:00:00:00:00 and true
  • isValidMac() 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 adapter
  • saveCfg() stores the mac key 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

  • WiFiProfile and EthProfile on ESP32 ignored a failed esp_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
    as ESP-000000

0.10.1

Choose a tag to compare

@soosp soosp released this 13 Oct 19:40

Fixed

  • MqttProfile::MAX_TOPIC_LEN defaulted to 64, less than the application's
    typical default base topic sensors/<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 now NETWORK_PROFILE_HOSTNAME_LEN + 16 (79)

0.10.0

Choose a tag to compare

@soosp soosp released this 13 Oct 18:42

Added

  • MqttProfile: the device's base topic, MqttConfig::baseTopic — the
    namespace under which the suite's convention puts state, config,
    status and set/+, 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 most MAX_TOPIC_LEN
    (MQTT_PROFILE_MAX_TOPIC_LEN, 64). In the backup as baseTopic
  • MqttProfile::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 in MqttConfig and not in the backup
  • Records written before these fields load with an empty base topic

0.9.0

Choose a tag to compare

@soosp soosp released this 11 Sep 21:23

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 same getConfig/setConfig/checkConfig,
    saveCfg/loadCfg/clearCfg and toJson surface as MqttProfile. It
    holds no socket and frames nothing; a sender reads it.
  • NETWORK_PROFILE_VERSION is now 900.

0.8.0

Choose a tag to compare

@soosp soosp released this 28 Aug 19:45

Added

  • MqttProfile::MqttConfig gains haDiscovery, 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 through MqttConfig.
  • NetworkProfileVersion.h defines NETWORK_PROFILE_VERSION as
    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() and toJson(); defaults to false, so
    records written before the field existed load as "off", which is what those
    devices were doing.

0.7.0

Choose a tag to compare

@soosp soosp released this 26 Aug 20:35

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, WiFiProfile and MqttProfile alike. 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-constructed MqttConfig's.

  • WiFiProfile(const WiFiConfig&) dropped cfg.txPower. The constructor applied
    the SSID and the password but left the transmit power at the platform default,
    while setConfig(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 before txpwr
    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 to DEFAULT_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_POWER changed, can hold one the radio would
    refuse. The rest of the profile still loads and loadCfg() still returns
    true; only the TX power is replaced.

Added

  • NtpProfile::clearCfg() and MqttProfile::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. MqttProfile leaves the password out of toJson() 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 a ConfigCheck value 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, with getServer() /
    setServer(), checkConfig(), saveCfg() / loadCfg() and toJson(). 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 every NetworkProfile meant
    the same pool.ntp.org typed twice, stored twice, and carried twice in the
    largest document the portal serves. Where interfaces really do reach different
    time servers, two NtpProfile instances say so plainly.

  • NtpProfile::importLegacy() reads servers written by an earlier version, which
    stored them per interface under ntp0..ntpN in 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.h holds NTP_PROFILE_SERVER_COUNT on its own, because two
    callers ask different things of it: NtpProfile.h asks 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_ENABLED decides whether NTP exists in the build;
    NTP_PROFILE_SERVER_COUNT says 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 from NtpProfile::SERVER_COUNT rather
    than the macro.

  • NTP_PROFILE_SERVER_COUNT replaces NETWORK_PROFILE_NTP_SERVER_COUNT; the old
    name is reported by a #warning rather than ignored, since a build still
    passing it would silently get the default count.

  • json_cat_esc() in detail/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() and MqttProfile::setConfig() validate what they are
    given, through their own checkConfig(), and store nothing unless it passes.
    Both classes already had the check written; it simply was not wired in, so the
    same object accepted through setConfig() 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 static isValidConfig(), a subclass's own rules apply
    through it automatically.

    What still passes is unchanged where it matters: an NtpProfile with every
    slot empty is a device told to take its time from DHCP, and a disabled
    MqttProfile keeps 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 a getConfig() → edit → setConfig() round-trip of something
    loaded from an older or hand-edited namespace can fail where it did not.
    loadCfg() and importLegacy() deliberately stay permissive: the check
    belongs where a new value arrives, not where an existing one is recovered.

  • NtpProfile and MqttProfile gained configCheckName(), matching
    NetworkProfile's. Each class has its own ConfigCheck enum, 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 that setConfig() can refuse, the name is what makes the
    refusal reportable.

  • detail/prefs_tools.h names 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.h declares its class unconditionally and
    guards only its .cpp, so the #else branch compiled on a fourth
    architecture and then failed at link time with undefined 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 one WiFiProfile.h has 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.md already 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, and loadCfg() replaces it with DEFAULT_WIFI_TX_POWER_dBm as it
    does for any other unacceptable value.

  • toJson() prints txpwr with 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...

Read more

0.6.0

Choose a tag to compare

@soosp soosp released this 06 Aug 19:35

[0.6.0] - 2026-08-05

Added

  • MqttProfile — stored MQTT broker settings (enabled, host, port, TLS,
    username, password) with checkConfig(), 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.
  • enabled is 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.
  • MqttProfile locks the same way NetworkProfile does — a std::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() and setConfig()
    return bool accordingly. 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.h with prefs_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()
    or setConfig() is now a compile error rather than a silent half-check. These
    are static, so the compiler picks by declared type: a WiFiConfig handed 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
    NetworkConfig variable first still loses the subclass fields — by then they
    are gone, and no signature can catch that. MqttProfile carries the same
    guard.

    Impact on the dependent libraries was one call site, inside WiFiProfile
    itself; NetworkManager, AsyncConfigPortal and AsyncWiFiPortal all already
    called through the concrete type or cast explicitly.

Note

  • The MQTT password is not written to toJson(), matching WiFiProfile: a
    backup file may travels through mail and cloud storage. Restoring one
    therefore needs the broker password entered again.