Public Access
Add HA dropdowns, off image, flip fixes and refresh effects
Home Assistant: remove the Clock switch (its entity is deleted on connect), add an IP sensor and Mode / Off image dropdowns whose options follow the saved bitmaps. Remove the built-in "Nintendo border" bitmap. Off image: a bitmap (or blank) left on the dots when the display switches off. Nano: pause after each flip pulse and repeat pulses for changed dots, both stored in EEPROM (G/N commands). Refresh effects (E command): instant, sparkle, wipe, rain, from the middle out, diagonal, spiral, flip all then reveal, push and random, optionally only for small changes. Effects run in steps from loop() so serial input keeps flowing. Web UI: flip settings, refresh effect and test patterns under Settings; the System tab shows why the ESP32 last restarted. Settings records from versions 1 and 2 are migrated. Firmware 2.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a5f6a8b5d
commit
1d3b084de1
42 files changed
+1631
-230
No files matched your search
@@ -2,10 +2,17 @@
|
||||
|
||||
#include <ArduinoHA.h>
|
||||
#include <WiFi.h>
|
||||
#include <algorithm>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <utils/HADictionary.h>
|
||||
#include <utils/HASerializer.h>
|
||||
|
||||
#include "app.h"
|
||||
#include "config.h"
|
||||
#include "credentials.h"
|
||||
#include "display/bitmap_store.h"
|
||||
#include "net/ha_page_select.h"
|
||||
#include "system/boot_target.h"
|
||||
|
||||
static WiFiClient client;
|
||||
@@ -13,30 +20,153 @@ static HADevice device(HA_DEVICE_ID, sizeof(HA_DEVICE_ID));
|
||||
static HAMqtt mqtt(client, device);
|
||||
|
||||
// Unique ids: do not change, Home Assistant's entities are linked to them
|
||||
static HASwitch clockSwitch("FlipDotDisplayClock");
|
||||
static HASwitch powerSwitch("FlipDotDisplayPower");
|
||||
static HAPageSelect modeSelect("FlipDotDisplayMode");
|
||||
static HAPageSelect offSelect("FlipDotDisplayOffImage");
|
||||
static HASensor firmwareSensor("FlipDotDisplayFirmware");
|
||||
static HASensor ipSensor("FlipDotDisplayIP");
|
||||
static HAButton recoveryButton("FlipDotDisplayRecovery");
|
||||
|
||||
static char firmware_status[64] = "";
|
||||
static bool firmware_status_sent = false;
|
||||
// Removed entities: their retained discovery config is cleared, so Home Assistant
|
||||
// deletes them instead of showing them as unavailable
|
||||
static const char *const REMOVED_SWITCHES[] = {"FlipDotDisplayClock"};
|
||||
|
||||
// A text sensor that is only sent when its value changes, and again after a reconnect
|
||||
// (HASensor doesn't resend its value by itself)
|
||||
struct TextSensor
|
||||
{
|
||||
HASensor &sensor;
|
||||
char value[64];
|
||||
bool sent;
|
||||
|
||||
void set(const char *new_value)
|
||||
{
|
||||
if (strcmp(new_value, value) == 0)
|
||||
return;
|
||||
strncpy(value, new_value, sizeof(value) - 1);
|
||||
value[sizeof(value) - 1] = '\0';
|
||||
sent = false;
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
if (!sent && value[0] && mqtt.isConnected())
|
||||
sent = sensor.setValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
static TextSensor firmware = {firmwareSensor, "", false};
|
||||
static TextSensor ip = {ipSensor, "", false};
|
||||
|
||||
static DisplayController *displayController = nullptr;
|
||||
static bool enabled = false;
|
||||
|
||||
// The switches are not optimistic: the new state must be reported back to Home Assistant
|
||||
static void onClockCommand(bool state, HASwitch *sender)
|
||||
{
|
||||
displayController->setClockEnabled(state);
|
||||
sender->setState(state);
|
||||
}
|
||||
|
||||
static void onPowerCommand(bool state, HASwitch *sender)
|
||||
{
|
||||
displayController->setPowerEnabled(state);
|
||||
sender->setState(state);
|
||||
}
|
||||
|
||||
// The bitmap names for the dropdowns: built-in ones first, then the uploaded ones
|
||||
// alphabetically. Rebuilt when bitmaps were added or deleted.
|
||||
static const std::vector<String> &bitmapNames()
|
||||
{
|
||||
static std::vector<String> names;
|
||||
static bool loaded = false;
|
||||
static uint32_t revision = 0;
|
||||
if (loaded && revision == bitmapListRevision())
|
||||
return names;
|
||||
loaded = true;
|
||||
revision = bitmapListRevision();
|
||||
|
||||
std::vector<String> uploaded;
|
||||
names.clear();
|
||||
for (const BitmapInfo &b : listBitmaps())
|
||||
(b.builtin ? names : uploaded).push_back(b.name);
|
||||
std::sort(uploaded.begin(), uploaded.end(),
|
||||
[](const String &a, const String &b) { return strcasecmp(a.c_str(), b.c_str()) < 0; });
|
||||
names.insert(names.end(), uploaded.begin(), uploaded.end());
|
||||
return names;
|
||||
}
|
||||
|
||||
// A dropdown with one fixed option (the clock, or blank) followed by the bitmaps, for a
|
||||
// setting that holds either the fixed value or a bitmap name
|
||||
struct BitmapChoice
|
||||
{
|
||||
HAPageSelect &select;
|
||||
const char *fixed_option; // shown in Home Assistant
|
||||
const char *fixed_value; // stored in the settings
|
||||
const char *(*setting)();
|
||||
const char *(*choose)(const char *value); // returns nullptr or an error
|
||||
|
||||
bool options_loaded;
|
||||
uint32_t options_revision;
|
||||
bool state_known;
|
||||
char state_for[PAGE_NAME_SIZE]; // setting the state was published for
|
||||
std::vector<String> options;
|
||||
|
||||
// The option for the setting; the fixed one when the bitmap is gone (the display
|
||||
// falls back to the clock or blank then too)
|
||||
const char *current()
|
||||
{
|
||||
const char *value = setting();
|
||||
if (strcmp(value, fixed_value) != 0)
|
||||
for (const String &option : options)
|
||||
if (strcasecmp(option.c_str(), value) == 0)
|
||||
return option.c_str();
|
||||
return fixed_option;
|
||||
}
|
||||
|
||||
// Rebuilds the options when bitmaps were added or deleted and reports the setting
|
||||
void update()
|
||||
{
|
||||
if (!options_loaded || options_revision != bitmapListRevision())
|
||||
{
|
||||
const std::vector<String> &names = bitmapNames();
|
||||
options_loaded = true;
|
||||
options_revision = bitmapListRevision();
|
||||
options.assign(1, fixed_option);
|
||||
options.insert(options.end(), names.begin(), names.end());
|
||||
select.setOptions(options);
|
||||
state_known = false;
|
||||
}
|
||||
if (!state_known || strcmp(state_for, setting()) != 0)
|
||||
{
|
||||
state_known = true;
|
||||
strncpy(state_for, setting(), sizeof(state_for) - 1);
|
||||
state_for[sizeof(state_for) - 1] = '\0';
|
||||
select.setState(current());
|
||||
}
|
||||
}
|
||||
|
||||
void command(const char *option)
|
||||
{
|
||||
const char *error = choose(strcmp(option, fixed_option) == 0 ? fixed_value : option);
|
||||
if (error)
|
||||
Serial.printf("Home Assistant \"%s\": %s\n", option, error);
|
||||
update(); // report the (new) state back
|
||||
}
|
||||
};
|
||||
|
||||
// Bitmaps can't be called "clock", and the blank off image is stored as ""
|
||||
static BitmapChoice mode = {modeSelect, "Clock", PAGE_CLOCK, []() { return appSettings().page; }, appSelectPage};
|
||||
static BitmapChoice offImage = {offSelect, "Blank", OFF_PAGE_BLANK, []() { return appSettings().off_page; },
|
||||
appSelectOffPage};
|
||||
|
||||
static void clearRemovedEntities()
|
||||
{
|
||||
for (const char *id : REMOVED_SWITCHES)
|
||||
{
|
||||
uint16_t length = HASerializer::calculateConfigTopicLength(AHATOFSTR(HAComponentSwitch), id);
|
||||
if (length == 0)
|
||||
continue;
|
||||
char topic[length];
|
||||
if (HASerializer::generateConfigTopic(topic, AHATOFSTR(HAComponentSwitch), id))
|
||||
mqtt.publish(topic, "", true);
|
||||
}
|
||||
}
|
||||
|
||||
void homeAssistantBegin(DisplayController &controller, const HomeAssistantSettings &settings)
|
||||
{
|
||||
displayController = &controller;
|
||||
@@ -50,24 +180,36 @@ void homeAssistantBegin(DisplayController &controller, const HomeAssistantSettin
|
||||
device.setName(HA_DEVICE_NAME);
|
||||
device.setSoftwareVersion(HA_SOFTWARE_VERSION);
|
||||
|
||||
clockSwitch.setName("FlipDot Display Clock");
|
||||
clockSwitch.setIcon("mdi:lightbulb");
|
||||
clockSwitch.setCurrentState(controller.clockEnabled());
|
||||
clockSwitch.onCommand(onClockCommand);
|
||||
|
||||
powerSwitch.setName("FlipDot Display Power");
|
||||
powerSwitch.setIcon("mdi:lightbulb");
|
||||
powerSwitch.setCurrentState(controller.powerEnabled());
|
||||
powerSwitch.onCommand(onPowerCommand);
|
||||
|
||||
modeSelect.setName("FlipDot Display Mode");
|
||||
modeSelect.setIcon("mdi:image-multiple");
|
||||
modeSelect.onCommand([](const char *option) { mode.command(option); });
|
||||
mode.update();
|
||||
|
||||
offSelect.setName("FlipDot Display Off image");
|
||||
offSelect.setIcon("mdi:image-off-outline");
|
||||
offSelect.onCommand([](const char *option) { offImage.command(option); });
|
||||
offImage.update();
|
||||
|
||||
recoveryButton.setName("FlipDot Display Recovery");
|
||||
recoveryButton.setIcon("mdi:lifebuoy");
|
||||
recoveryButton.onCommand([](HAButton *) { rebootToRecovery(RecoveryReason::Manual); });
|
||||
|
||||
firmwareSensor.setName("FlipDot Display Firmware");
|
||||
firmwareSensor.setIcon("mdi:chip");
|
||||
// The sensor doesn't resend its value on reconnect by itself
|
||||
mqtt.onConnected([]() { firmware_status_sent = false; });
|
||||
|
||||
ipSensor.setName("FlipDot Display IP");
|
||||
ipSensor.setIcon("mdi:ip-network");
|
||||
|
||||
mqtt.onConnected([]() {
|
||||
firmware.sent = false;
|
||||
ip.sent = false;
|
||||
clearRemovedEntities();
|
||||
});
|
||||
|
||||
// settings must stay alive: the MQTT library keeps the pointers
|
||||
if (settings.host[0])
|
||||
@@ -87,14 +229,20 @@ void homeAssistantLoop()
|
||||
if (!enabled)
|
||||
return;
|
||||
mqtt.loop();
|
||||
if (!firmware_status_sent && firmware_status[0] && mqtt.isConnected())
|
||||
firmware_status_sent = firmwareSensor.setValue(firmware_status);
|
||||
|
||||
static uint32_t last_ip = 0;
|
||||
if (WiFi.isConnected() && (uint32_t)WiFi.localIP() != last_ip)
|
||||
{
|
||||
last_ip = WiFi.localIP();
|
||||
ip.set(WiFi.localIP().toString().c_str());
|
||||
}
|
||||
ip.loop();
|
||||
firmware.loop();
|
||||
mode.update();
|
||||
offImage.update();
|
||||
}
|
||||
|
||||
void homeAssistantSetFirmwareStatus(const char *status)
|
||||
{
|
||||
if (strcmp(status, firmware_status) == 0)
|
||||
return;
|
||||
strncpy(firmware_status, status, sizeof(firmware_status) - 1);
|
||||
firmware_status_sent = false;
|
||||
firmware.set(status);
|
||||
}
|
||||
Reference in new issue
Block a user