Skip to content

Commit fc90fc3

Browse files
committed
Add Clock widget with customizable time and date display
1 parent 3cb44f0 commit fc90fc3

8 files changed

Lines changed: 466 additions & 31 deletions

File tree

cmake/sources.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ set(MAIN_SOURCES
3232
main/widgets/light_switch_wide_widget.cpp
3333
main/widgets/temperature_widget.cpp
3434
main/widgets/scenario_widget.cpp
35+
main/widgets/clock_widget.cpp
3536
)
3637

3738
# Flux architecture sources

main/calaos_protocol.cpp

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,34 @@ PagesConfig PagesConfig::fromJson(const std::string& json_str)
108108
widget.h = widget_json["height"].get<int>();
109109
}
110110

111-
// Validate widget
112-
if (widget.io_id.empty())
111+
// Collect extra parameters (unknown keys) into params map
112+
static const std::vector<std::string> knownKeys = {
113+
"io_id", "type", "name", "x", "y", "w", "h", "width", "height"
114+
};
115+
for (auto it = widget_json.begin(); it != widget_json.end(); ++it)
113116
{
114-
ESP_LOGW(TAG, "Skipping widget with empty io_id");
115-
continue;
117+
const std::string& key = it.key();
118+
bool isKnown = false;
119+
for (const auto& k : knownKeys)
120+
{
121+
if (key == k)
122+
{
123+
isKnown = true;
124+
break;
125+
}
126+
}
127+
if (!isKnown && it.value().is_string())
128+
widget.params[key] = it.value().get<std::string>();
129+
else if (!isKnown && it.value().is_number())
130+
widget.params[key] = std::to_string(it.value().get<int>());
131+
else if (!isKnown && it.value().is_boolean())
132+
widget.params[key] = it.value().get<bool>() ? "true" : "false";
116133
}
117134

135+
// Validate widget
118136
if (widget.type.empty())
119137
{
120-
ESP_LOGW(TAG, "Skipping widget %s with empty type", widget.io_id.c_str());
138+
ESP_LOGW(TAG, "Skipping widget with empty type");
121139
continue;
122140
}
123141

main/calaos_protocol.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ inline constexpr const char* FW_HEADER_HARDWARE_ID = "X-Device-Hardware-Id";
3737
*/
3838
struct WidgetConfig
3939
{
40-
std::string io_id; // IO unique identifier (e.g., "io_0")
41-
std::string type; // Widget type (e.g., "LightSwitch", "Temperature")
40+
std::string io_id; // IO unique identifier (e.g., "io_0"), empty for non-IO widgets
41+
std::string type; // Widget type (e.g., "LightSwitch", "Temperature", "Clock")
4242
std::string override_name; // Optional user-defined name override for display
4343
int x = 0; // Grid position X
4444
int y = 0; // Grid position Y
4545
int w = 1; // Grid width
4646
int h = 1; // Grid height
4747

48+
// Extra parameters from JSON/XML attributes (e.g., clock_timezone, clock_format)
49+
std::map<std::string, std::string> params;
50+
4851
WidgetConfig() = default;
4952

5053
WidgetConfig(const std::string& io_id,

main/calaos_widget.cpp

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,34 @@ CalaosWidget::CalaosWidget(lv_obj_t* parent,
2424
setBgOpa(LV_OPA_COVER);
2525
setPadding(0, 0, 0, 0);
2626

27-
// Subscribe to state changes
28-
subscribeToStateChanges();
29-
30-
// Try to get initial state from AppStore
31-
const AppState& state = AppStore::getInstance().getState();
32-
auto it = state.ioStates.find(config.io_id);
33-
if (it != state.ioStates.end())
27+
// Only subscribe to IO state changes if io_id is set (non-IO widgets like Clock skip this)
28+
if (!config.io_id.empty())
3429
{
35-
currentState = it->second;
36-
ESP_LOGI(TAG, "Widget %s found initial state: %s",
37-
config.io_id.c_str(), currentState.state.c_str());
30+
// Subscribe to state changes
31+
subscribeToStateChanges();
32+
33+
// Try to get initial state from AppStore
34+
const AppState& state = AppStore::getInstance().getState();
35+
auto it = state.ioStates.find(config.io_id);
36+
if (it != state.ioStates.end())
37+
{
38+
currentState = it->second;
39+
ESP_LOGI(TAG, "Widget %s found initial state: %s",
40+
config.io_id.c_str(), currentState.state.c_str());
41+
}
42+
else
43+
{
44+
ESP_LOGW(TAG, "Widget %s: IO state not found in AppStore", config.io_id.c_str());
45+
// Set default state
46+
currentState.id = config.io_id;
47+
currentState.type = config.type;
48+
currentState.state = "unknown";
49+
currentState.name = config.io_id;
50+
}
3851
}
3952
else
4053
{
41-
ESP_LOGW(TAG, "Widget %s: IO state not found in AppStore", config.io_id.c_str());
42-
// Set default state
43-
currentState.id = config.io_id;
44-
currentState.type = config.type;
45-
currentState.state = "unknown";
46-
currentState.name = config.io_id;
54+
ESP_LOGI(TAG, "Widget type=%s has no io_id, skipping IO state subscription", config.type.c_str());
4755
}
4856
}
4957

@@ -67,10 +75,11 @@ const std::string& CalaosWidget::getDisplayName(const CalaosProtocol::IoState& s
6775

6876
CalaosWidget::~CalaosWidget()
6977
{
70-
ESP_LOGI(TAG, "Destroying widget: %s", config.io_id.c_str());
78+
ESP_LOGI(TAG, "Destroying widget: type=%s io_id=%s", config.type.c_str(), config.io_id.c_str());
7179

7280
// Unsubscribe from AppStore to prevent dangling pointer
73-
AppStore::getInstance().unsubscribe(subscriptionId_);
81+
if (subscriptionId_ != 0)
82+
AppStore::getInstance().unsubscribe(subscriptionId_);
7483
}
7584

7685
void CalaosWidget::calculateAndApplyPosition()

main/widget_factory.cpp

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "widgets/light_switch_wide_widget.h"
55
#include "widgets/temperature_widget.h"
66
#include "widgets/scenario_widget.h"
7+
#include "widgets/clock_widget.h"
78
#include "logging.h"
89
#include <sstream>
910

@@ -31,10 +32,18 @@ void WidgetFactory::registerWidget(const std::string& typeName,
3132
ESP_LOGI(TAG, "Registered widget: %s", key.c_str());
3233
}
3334

35+
void WidgetFactory::registerWidgetAnySize(const std::string& typeName, WidgetCreator creator)
36+
{
37+
anySizeCreators[typeName] = creator;
38+
ESP_LOGI(TAG, "Registered any-size widget: %s", typeName.c_str());
39+
}
40+
3441
bool WidgetFactory::isRegistered(const std::string& typeName, int width, int height) const
3542
{
3643
std::string key = makeKey(typeName, width, height);
37-
return creators.find(key) != creators.end();
44+
if (creators.find(key) != creators.end())
45+
return true;
46+
return anySizeCreators.find(typeName) != anySizeCreators.end();
3847
}
3948

4049
std::string WidgetFactory::makeKey(const std::string& type, int w, int h) const
@@ -53,14 +62,22 @@ std::unique_ptr<CalaosWidget> WidgetFactory::createWidget(
5362

5463
ESP_LOGI(TAG, "Creating widget: %s (io_id=%s)", key.c_str(), config.io_id.c_str());
5564

56-
// Look up creator
65+
// Look up creator - first try exact Type_WxH match
5766
auto it = creators.find(key);
5867
if (it != creators.end())
5968
{
60-
// Found - create the widget
69+
// Found exact match - create the widget
6170
return it->second(parent, config, gridInfo);
6271
}
6372

73+
// Try any-size fallback for this type
74+
auto anyIt = anySizeCreators.find(config.type);
75+
if (anyIt != anySizeCreators.end())
76+
{
77+
ESP_LOGI(TAG, "Using any-size creator for type: %s", config.type.c_str());
78+
return anyIt->second(parent, config, gridInfo);
79+
}
80+
6481
// Not found - create WidgetError
6582
ESP_LOGW(TAG, "Widget type/size not supported: %s - creating WidgetError", key.c_str());
6683

@@ -110,5 +127,13 @@ void WidgetFactory::registerBuiltinWidgets()
110127
}
111128
);
112129

113-
ESP_LOGI(TAG, "Built-in widgets registered: %zu", creators.size());
130+
// Register Clock for any size (dynamically adapts to grid dimensions)
131+
registerWidgetAnySize("Clock",
132+
[](lv_obj_t* parent, const auto& config, const auto& gridInfo) {
133+
return std::make_unique<ClockWidget>(parent, config, gridInfo);
134+
}
135+
);
136+
137+
ESP_LOGI(TAG, "Built-in widgets registered: %zu sized + %zu any-size",
138+
creators.size(), anySizeCreators.size());
114139
}

main/widget_factory.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,24 @@ class WidgetFactory
5454
int height,
5555
WidgetCreator creator);
5656

57+
/**
58+
* @brief Register a widget type that accepts any size
59+
*
60+
* Used for widgets like Clock that dynamically adapt to whatever grid size
61+
* is configured. The any-size creator is used as a fallback when no exact
62+
* Type_WxH match is found.
63+
*
64+
* @param typeName Widget type name (e.g., "Clock")
65+
* @param creator Function that creates the widget
66+
*/
67+
void registerWidgetAnySize(const std::string& typeName, WidgetCreator creator);
68+
5769
/**
5870
* @brief Check if a widget type/size is registered
5971
* @param typeName Widget type name
6072
* @param width Widget width in grid units
6173
* @param height Widget height in grid units
62-
* @return true if registered
74+
* @return true if registered (exact match or any-size)
6375
*/
6476
bool isRegistered(const std::string& typeName, int width, int height) const;
6577

@@ -86,7 +98,11 @@ class WidgetFactory
8698
*/
8799
std::string makeKey(const std::string& type, int w, int h) const;
88100

89-
// Map of widget creators
101+
// Map of widget creators for specific sizes
90102
// Key format: "LightSwitch_1x1", "Temperature_1x1", etc.
91103
std::map<std::string, WidgetCreator> creators;
104+
105+
// Map of widget creators that accept any size (fallback when no exact match)
106+
// Key: type name (e.g., "Clock")
107+
std::map<std::string, WidgetCreator> anySizeCreators;
92108
};

0 commit comments

Comments
 (0)