user28278: In my limited testing it appears that the EEPROM in the Wemos D1 is not initialized to 255 like the specs say is the case for AtMega based Arduinos. In fact, the values look to be more-or-less random. Presumably this is because the ESP8266 doesn't actually contain EEPROM, but instead emulates it using flash. Is there a way to initialize the EEPROM region when flashing?
(This is pretty much the same question as here, which has not been answered.) Since I need only a few bytes of data, I'd prefer not to have to use the SPIFS. 
Update: 
Use case: I'm using the WiFiManager library to select the network to connect a Wemos D1 mini to the web to upload data to Thingspeak. I'd like to use WiFiManager to request the api-key and channel id from the user. However, once the user has entered these, I'd prefer not to make them enter the values again. (The api key is 16 random looking chars and difficult to enter correctly.) This means that the device has to store the values persistently between boots - the main use case for EEPROM. But how do you tell that the values in EEPROM (actually flash on an ESP device) are uninitialized crap, not the previous value? As suggested in the comments, setting flag values in a number of vars make it statistically unlikely to the random values from uninitialized memory. On an AtMega device, unless previously used, the values will be 255 in each byte. 

user47164: Rather than use EEPROM, which is a real pain considering that it's shared between all stored data and you need to remember offsets and lengths, use SPIFFS.
By including FS.h in your program (or the include file that needs to store the data, if you are trying to be modular), you can store several megabytes of data as strings in a named file (much like fprintf() or open() would let you do in C++).
Call  SPIFFS.begin();  in setup(), and then use the following sections of code to read or write data to the file:
(assuming you want to store start and end in SPIFFS, but this works for any datatype known to Arduino, and probably anything with the printable attribute)
Read saved config:
Serial.println(F("Loading config"));
File f = SPIFFS.open("/ACTIVE_TIMES.cnf", "r");
if (!f) {
  //File does not exist -- first run or someone called format()
  //Will not create file; run save code to actually do so (no need here since 
  //it's not changed)
  Serial.println(F("Failed to open config file"));
  start = 8;
  end = 20; // defaults
  return;
}
while (f.available()) {
  String key = f.readStringUntil('=');
  String value = f.readStringUntil('\r');
  f.read();
  Serial.println(key + F(" = [") + value + ']');
  Serial.println(key.length());
  if (key == F("START_TIME")) {
    start = value.toInt();
  }
  if (key == F("END_TIME")) {
    end = value.toInt();
  }
}
f.close();

Save configuration:
Serial.println(F("Saving config"));
File f = SPIFFS.open("/ACTIVE_TIMES.cnf", "w+");
if (!f) {
  //It's cases like this when calling SPIFFS.format() might be a good idea, 
  //but you would not want to do so here, since if it's an error for a 
  //different reason it would delete all your data. Make it manual or a prompt 
  //instead.
  Serial.println(F("Failed to open config file"));
  return;
}
f.print(F("START_TIME="));
f.println(start);
f.print(F("END_TIME="));
f.println(end);
f.flush();
f.close();
Serial.println(F("Saved values"));

You may also call SPIFFS.format() to reset the filesystem (say, for a factory reset), but this will cause issues on attempting to read any stored data, since the file will not exist. Thus, have a section that sets reasonable defaults (or just prompts the user to set the values).
Note: Since this is for storing an API key and such, just have it fail, since the API key not working is a good notice the user needs to configure it, and you do NOT want to ship a product with a working API key that belongs to you rather than the user.
Final warning: Note carefully the use of F() around certain items in that code. This macro, specific to Arduino, allows one to store data in Flash ONLY, where strings normally consume RAM as well (once the system boots, it loads to RAM, since most code expects it there). The benefit of F(), rather than a progmem directive, is that many functions native to Arduino can read it directly thanks to overloads, or you can wrap String() around the F() to convert it to a string at runtime if the function does not allow that overload. The end result is that, as I've written it, this code should consume RAM only when it is actually running, as good code should (otherwise, it's really easy to use up all your RAM on such embedded devices if you add enough strings).
I have not bothered to do the latter with the filename passed to open() since it is used only once per load or save operation, does not support F() (thus I'd need to use the String() trick), and I'm fairly sure the compiler then shares the memory location, reducing the RAM used to only one edition of that filename.

user313: Calculate a checksum for the data and store as part of the data. Recalculate the checksum when you read the (questionable) data back again. If the checksum fails, it's invalid. If it passes, the data is valid to some statistical level of confidence. 
A 16-bit checksum would make false acceptance highly unlikely, but  make it long enough for your own comfort level. What would be the worst consequence of a false acceptance: inconvenience (user has to re-enter a number)? Loss of money or property? Loss of life? Choose the complexity of your solution accordingly.

28278: <p>In my limited testing it appears that the EEPROM in the Wemos D1 is not initialized to 255 like the specs say is the case for AtMega based Arduinos. In fact, the values look to be more-or-less random. Presumably this is because the ESP8266 doesn't actually contain EEPROM, but instead emulates it using flash. Is there a way to initialize the EEPROM region when flashing?</p>

<p>(This is pretty much the same question as <a href="https://arduino.stackexchange.com/questions/46519/set-eeprom-to-initial-values-when-flashing-esp8266">here</a>, which has not been answered.) Since I need only a few bytes of data, I'd prefer not to have to use the SPIFS. </p>

<p>Update: </p>

<p>Use case: I'm using the WiFiManager library to select the network to connect a Wemos D1 mini to the web to upload data to Thingspeak. I'd like to use WiFiManager to request the api-key and channel id from the user. However, once the user has entered these, I'd prefer not to make them enter the values again. (The api key is 16 random looking chars and difficult to enter correctly.) This means that the device has to store the values persistently between boots - the main use case for EEPROM. But how do you tell that the values in EEPROM (actually flash on an ESP device) are uninitialized crap, not the previous value? As suggested in the comments, setting flag values in a number of vars make it statistically unlikely to the random values from uninitialized memory. On an AtMega device, unless previously used, the values will be 255 in each byte. </p>
 47164: <p>Rather than use EEPROM, which is a real pain considering that it's shared between all stored data and you need to remember offsets and lengths, use SPIFFS.</p>

<p>By including <code>FS.h</code> in your program (or the include file that needs to store the data, if you are trying to be modular), you can store several megabytes of data as strings in a named file (much like fprintf() or open() would let you do in C++).</p>

<p>Call  <code>SPIFFS.begin();</code>  in setup(), and then use the following sections of code to read or write data to the file:</p>

<p>(assuming you want to store <code>start</code> and <code>end</code> in SPIFFS, but this works for any datatype known to Arduino, and probably anything with the printable attribute)</p>

<p>Read saved config:</p>

<pre><code>Serial.println(F("Loading config"));
File f = SPIFFS.open("/ACTIVE_TIMES.cnf", "r");
if (!f) {
  //File does not exist -- first run or someone called format()
  //Will not create file; run save code to actually do so (no need here since 
  //it's not changed)
  Serial.println(F("Failed to open config file"));
  start = 8;
  end = 20; // defaults
  return;
}
while (f.available()) {
  String key = f.readStringUntil('=');
  String value = f.readStringUntil('\r');
  f.read();
  Serial.println(key + F(" = [") + value + ']');
  Serial.println(key.length());
  if (key == F("START_TIME")) {
    start = value.toInt();
  }
  if (key == F("END_TIME")) {
    end = value.toInt();
  }
}
f.close();
</code></pre>

<p>Save configuration:</p>

<pre><code>Serial.println(F("Saving config"));
File f = SPIFFS.open("/ACTIVE_TIMES.cnf", "w+");
if (!f) {
  //It's cases like this when calling SPIFFS.format() might be a good idea, 
  //but you would not want to do so here, since if it's an error for a 
  //different reason it would delete all your data. Make it manual or a prompt 
  //instead.
  Serial.println(F("Failed to open config file"));
  return;
}
f.print(F("START_TIME="));
f.println(start);
f.print(F("END_TIME="));
f.println(end);
f.flush();
f.close();
Serial.println(F("Saved values"));
</code></pre>

<p>You may also call <code>SPIFFS.format()</code> to reset the filesystem (say, for a factory reset), but this will cause issues on attempting to read any stored data, since the file <em>will not exist</em>. Thus, have a section that sets reasonable defaults (or just prompts the user to set the values).</p>

<p>Note: Since this is for storing an API key and such, just have it fail, since the API key not working is a good notice the user needs to configure it, and you do NOT want to ship a product with a <em>working</em> API key that belongs to you rather than the user.</p>

<p>Final warning: Note carefully the use of <code>F()</code> around certain items in that code. This macro, specific to Arduino, allows one to store data in Flash ONLY, where strings normally consume RAM as well (once the system boots, it loads to RAM, since most code expects it there). The <em>benefit</em> of <code>F()</code>, rather than a progmem directive, is that many functions native to Arduino can read it directly thanks to overloads, or you can wrap <code>String()</code> around the <code>F()</code> to convert it to a string at runtime if the function does not allow that overload. The end result is that, as I've written it, this code should consume RAM only when it is actually running, as good code should (otherwise, it's really easy to use up all your RAM on such embedded devices if you add enough strings).</p>

<p>I have not bothered to do the latter with the filename passed to <code>open()</code> since it is used only once per load or save operation, does not support F() (thus I'd need to use the <code>String()</code> trick), and I'm fairly sure the compiler then shares the memory location, reducing the RAM used to only one edition of that filename.</p>
 313: <p>Calculate a checksum for the data and store as part of the data. Recalculate the checksum when you read the (questionable) data back again. If the checksum fails, it's invalid. If it passes, the data is valid to some statistical level of confidence. </p>

<p>A 16-bit checksum would make false acceptance highly unlikely, but  make it long enough for your own comfort level. What would be the worst consequence of a false acceptance: inconvenience (user has to re-enter a number)? Loss of money or property? Loss of life? Choose the complexity of your solution accordingly.</p>
