In this article I describe how I created a synthetic flower which shows the current state of the solar power battery of our house in a playful way.
Flower Power
Meet Flower Power: a quirky, physical gadget that indicates your homeâs solar battery charge level. A 3D-printed plant sits in a flower pot and spins around to either show off a happy or a sleepy face, depending on how much sunshine juice is currently stored.
Smart Home Monitoring with Home Assistant
About a year ago, I joined the Photovoltaics Club by installing solar panels on my roof. They convert sunlight into electricity, which then gets stashed in a battery for later use. Like most setups, the monitoring system that tells you how full your battery is lives in the basement. Luckily, I donât have to jog downstairs every time I wonder if thereâs enough power for the dishwasher. Modern smart home solutions let you check this information via a computer or smartphone.
Letâs take a quick look at how itâs integrated with Home Assistant. Below, you can see a typical pattern of day vs. night, sunshine vs. darkness:
Hereâs a week full of sunâexcept for July 12, which saw nonstop rain. During bright days, the battery soaks up excess solar power and then releases it at night for things like hot water or floor heating. On those long, sunny days, thereâs usually more leftover juice by the end of the night. On that rainy July 12, though, the battery completely drained and only got recharged the next day when the sun finally showed up.
With Home Assistant (or similar commercial options), itâs easy to keep tabs on how much power your household is using in real time. But opening an app, clicking through menus, and hoping you picked a good time to run the dishwasher (so youâre not buying extra electricity from the grid) can be a hassle. Plus, not every family member has access to the smart home systemâor the desire to figure it out.
From Digital to Analog
Iâm fascinated by the idea of blending modern technology with everyday objects, making the digital tangible and more⌠well, real. We already spend a huge chunk of our day glued to screens. Now, even turning on the living room lamp has gone âsmart,â which often means pulling out a phone instead of simply flipping a switch like weâve done for the last hundred years.
One of the biggest challenges in building physical gadgets with always-on connectivity is power consumption. But if you can get that consumption low enough so that your device can run for days or weeks without a recharge, it takes on an almost magical air.
The Power Plant
Enter the ESP32 microcontroller: itâs a dream come true for low-power projects. Itâs far more efficient than, say, a Raspberry Pi, yet it still has Wi-Fi, Bluetooth, a bunch of I/O pins, and an enormous community offering libraries for just about any purpose. The ESP32 also has a sleep mode that sips electricity, waking up only at preset intervals to run whatever code youâve stored in its flash memory.
For displaying the battery charge without burning power, a continuously lit LED display was a no-go (LEDs would drain a small battery in just a few days). Instead, I used a tiny servo motorâcommonly found in kidsâ electronics kitsâthat can rotate up to 180 degrees in either direction based on a control signal. Once positioned, it stays put without using more power, making it ideal for showing a batteryâs state of charge at intervals rather than in real time.
Less power consumption means a happier planet (and a friendlier electricity bill), so I chose a flower theme: the 3D-printed bloom turns inside its pot based on the batteryâs fill level. After trying out various materials (and a few fails), I ended up modeling the flower in Blender and printing it. Itâs not as cute as Iâd hoped, but it gets the job done.
A quick trip to the hardware store for a flowerpot and some decorative bits, and voilĂ âmy parts list was complete.
Bill Of Materials
No affiliate links here, just an idea of prices. Buy locally if you can! I had most of the stuff lying around already, so I only spent around 5 EUR on the pot and fake plants.
Part | Info | Where to get | Cost |
---|---|---|---|
ESP32 Mini | These have Wifi & Bluetooth onboard | Amazon | 11 EUR |
ESP32 Mini Battery Shield | Youâll get three of them | Amazon | 9 EUR |
2000mA LiPo Battery | I used a spare one I already had | Amazon | 11 EUR |
Servo Motor | You can get 3 for double the price | Amazon | 6 EUR |
PLA Filament | I used white filament to paint it later | Amazon | 1 EUR |
Flower Pot | Make sure itâs not conducting electricity | Flower Shop or Hardware Store | 3 EUR |
Plastic Plant Decoration | Not needed but looks nice | Flower Shop or Hardware Store | 2 EUR |
Acrylic Paint | Stolen from the kidâs room | The kidâs room | free |
Total cost | 43 EUR |
Assembly
I did some small battery surgery first: removing the Mini-JST connector and soldering the cables onto the shield that used a standard JST connector. Not ideal, but I didnât want to buy a separate battery.
Next, I hooked up the ESP32 Mini to the servo motor. A 3.3V line and ground (GHD) supply the motor, while pin 21 on the ESP32 handles the control signal.
After double-checking, I soldered the cables directly to the ESP32 pins and bent them slightly inward to save space.
Then I nestled the motor in its 3D-printed cradle, taped the battery to the bottom, and squeezed the board and wires between the motor housing and the flowerpot. When the servo moves, the whole 3D-printed flower rotates in a rather dramatic flair.
Thatâs it for the hardware. Time to write some code!
Calling the API
Home Assistant has a powerful API that exposes most connected devices and their data. For my battery, for example, the relevant endpoint is http://192.168.178.69:8123/api/states/sensor.s10x_state_of_charge
.
That makes it super easy to query the data, as a quick curl
command in the terminal can confirm:
curl \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhOTNiNzI3N2U3Mzc0YTIzODA2OTYwZmQ4ZmQ3MzQ3YSIsImlhdCI6MTcxMTcxNzcxOCwiZXhwIjoyMDI3MDc3NzE4fQ.UoSOlMJO64vPc9iJtbOecwPE1dweVMoKwPLdWV9L3m0" \
-H "Content-Type: application/json" \
http://192.168.178.69:8123/api/states/sensor.s10x_state_of_charge
Home Assistant returns a JSON string with the batteryâs state
indicating its charge level.
{
"entity_id": "sensor.s10x_state_of_charge",
"state": "0",
"attributes": {
"state_class": "measurement",
"unit_of_measurement": "%",
"device_class": "battery",
"friendly_name": "S10X State of charge"
},
"last_changed": "2024-12-23T15:34:39.260849+00:00",
"last_reported": "2024-12-26T13:57:35.276359+00:00",
"last_updated": "2024-12-23T15:34:39.260849+00:00",
"context": {
"id": "01JFT2FWWWVYSN902EP54A97XJ",
"parent_id": null,
"user_id": null
}
}
The Code
For smaller projects with just one file, I prefer the free Arduino IDE. Itâs less hassle compared to wrangling libraries and board settings in something like VSCode with PlatformIO.
Basic program flow:
- Run some setup tasks (like reducing CPU frequency to save power).
- Connect to Wi-Fi.
- Get the current time from an NTP server (the ESP32 doesnât have a built-in clock).
- If itâs night (midnight to 6 AM), go back to sleep.
- Query the battery state from the API.
- Map the state (0â100) to a rotation angle (0â180).
- Move the servo motor.
- Go to sleep.
This entire loop repeats every 20 minutes. The actual code for making the API call and telling the servo to move is pretty short:
// --------------------------------------------------------
// GET SENSOR DATA (HTTP GET)
// --------------------------------------------------------
int get_sensor_data() {
float state = -1;
if (WiFi.status() == WL_CONNECTED) {
http.begin(wifiClient, apiEndpoint);
http.addHeader("Authorization", authorizationHeader);
http.addHeader("Content-Type", "application/json");
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
state = doc["state"].as<float>();
} else {
Serial.println("Failed to parse JSON");
}
} else {
Serial.println("Error in HTTP request");
}
http.end();
}
return state;
}
And the main code inside the loop()
function:
// --------------------------------------------------------
// LOOP
// --------------------------------------------------------
void loop() {
[..]
// Get battery state from API or use random for testing
int battery_state = get_sensor_data();
if (battery_state == -1) {
Serial.println("API request failed, going to sleep...");
go_to_sleep();
}
int servo_rotation = map(battery_state, 0, 100, 0, 180);
myservo.attach(SERVO_PIN);
myservo.write(servo_rotation);
[..]
}
Most lines are about Wi-Fi setup or fetching the time. Checking the hour is optional, but since my 3D plant lives in the living room (where my dog also sleeps), I didnât want it buzzing to life every 20 minutes overnight. Also, skipping those 18 or so nightly updates further trims power use.
Hereâs the full code, which is also available on my GitHub account.
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
// -------------------- DEFINES & CONSTANTS --------------------
#define LED_BUILTIN 2
#define SERVO_PIN 21
#define DEEP_SLEEP_MINUTE 60000000
#define DEEP_SLEEP_TIME (DEEP_SLEEP_MINUTE * 20)
#define DEEP_SLEEP_TIME_TEST 4000000
// Wi-Fi credentials
const char* ssid = "[enter your SSID]]";
const char* password = "[enter your password]";
// API endpoint and authentication
const char* apiEndpoint = "http://192.168.178.69:8123/api/states/sensor.s10x_state_of_charge";
const char* authorizationHeader =
"Bearer [your bearer code]";
// Test mode flag
const bool testmode = false;
// -------------------- GLOBAL OBJECTS --------------------
WiFiClient wifiClient;
HTTPClient http;
Servo myservo;
// --------------------------------------------------------
// SETUP
// --------------------------------------------------------
void setup() {
setCpuFrequencyMhz(80);
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
connect_to_wifi();
}
// --------------------------------------------------------
// CONNECT TO WIFI FUNCTION
// --------------------------------------------------------
void connect_to_wifi() {
int attempts = 0;
Serial.println("Connecting to WiFi...");
WiFi.begin(ssid, password);
// Limit the connection attempts to 10 with exponential backoff
while (WiFi.status() != WL_CONNECTED && attempts < 10) {
delay(500 * (1 << attempts)); // 500ms, 1s, 2s, 4s...
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // Toggle LED
attempts++;
}
digitalWrite(LED_BUILTIN, LOW); // Turn off LED after connection attempt
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nConnected to WiFi");
} else {
Serial.println("\nFailed to connect to WiFi");
go_to_sleep();
}
}
// --------------------------------------------------------
// INIT TIME VIA NTP
// --------------------------------------------------------
void init_time() {
// Central European Time (Berlin): 3600 offset + 3600 DST
configTime(3600, 3600, "pool.ntp.org", "time.nist.gov");
Serial.println("Waiting for NTP time sync...");
time_t nowSecs = time(nullptr);
while (nowSecs < 24 * 3600) {
delay(500);
Serial.print(".");
nowSecs = time(nullptr);
}
Serial.println("\nTime synchronized");
}
// --------------------------------------------------------
// GET CURRENT LOCAL TIME
// --------------------------------------------------------
tm get_current_time() {
time_t now;
tm localTime;
time(&now);
localtime_r(&now, &localTime);
return localTime;
}
// --------------------------------------------------------
// GET SENSOR DATA (HTTP GET)
// --------------------------------------------------------
int get_sensor_data() {
float state = -1;
if (WiFi.status() == WL_CONNECTED) {
http.begin(wifiClient, apiEndpoint);
http.addHeader("Authorization", authorizationHeader);
http.addHeader("Content-Type", "application/json");
int httpCode = http.GET();
if (httpCode > 0) {
String payload = http.getString();
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, payload);
if (!error) {
state = doc["state"].as<float>();
} else {
Serial.println("Failed to parse JSON");
}
} else {
Serial.println("Error in HTTP request");
}
http.end();
}
return state;
}
// --------------------------------------------------------
// GO TO DEEP SLEEP
// --------------------------------------------------------
void go_to_sleep() {
myservo.detach(); // Detach servo to save power
WiFi.disconnect(true); // Disconnect WiFi to save power
WiFi.mode(WIFI_OFF); // Turn off WiFi module
Serial.println("Servo off, WiFi off, going to sleep.");
delay(500);
Serial.end();
delay(2000);
if (testmode) {
esp_sleep_enable_timer_wakeup(DEEP_SLEEP_TIME_TEST);
} else {
esp_sleep_enable_timer_wakeup(DEEP_SLEEP_TIME);
}
esp_deep_sleep_start();
}
// --------------------------------------------------------
// LOOP
// --------------------------------------------------------
void loop() {
// Initialize time via NTP
init_time();
// Get current local time
tm currentTime = get_current_time();
Serial.print("Hour: ");
Serial.println(currentTime.tm_hour);
// If it's night time (0:00 - 6:00), go to sleep
if (currentTime.tm_hour < 6) {
Serial.println("It's night time, going to sleep...");
go_to_sleep();
}
// Get battery state from API or use random for testing
int battery_state = (testmode) ? random(0, 100) : get_sensor_data();
if (battery_state == -1) {
Serial.println("API request failed, going to sleep...");
go_to_sleep();
}
int servo_rotation = map(battery_state, 0, 100, 0, 180);
// Print debug info
Serial.print("Battery: ");
Serial.println(battery_state);
Serial.print("Servo Rotation: ");
Serial.println(servo_rotation);
// Move the servo
myservo.attach(SERVO_PIN);
myservo.write(servo_rotation);
delay(1000);
// Sleep
go_to_sleep();
}
Final Thoughts
Iâm pretty happy with how this project turned out. One battery charge lasts around two weeksânot quite the holy grail, but still a neat conversation piece whenever guests spot a rotating flower thatâs apparently powered by sunshine. Itâs mostly showing its âsleepyâ face these days in winter, so I finally turned it around so we can enjoy that sunny smile.
If I were to take this further, Iâd use an even lighter flower design so itâd draw less power during rotation. Iâd also consider adding a USB micro port extender to the battery shield, so I could charge it easily without fishing out the hardware from the pot every time.
And thatâs all, folks. Thanks for reading! Stay sunny. đť