last updated: 2023-11-15
Use esp32 core version 2.0.14; the alpha versions of 3 have a problem with the neopixel lib
I liked the idea of an illuminated waste-bin to remind us to put out the rubbish bins. Meanwhile we have 5 different bins, so the task to remind the day and the bin is not as simple as it was years ago :).
A phone call to our municipality revealed that there is no API or other possibility on a server to get the data. The friendly official explained that they work with an Excel sheet, an was kind enough to send me the document by e-mail.
The table had the following format (excerpt):
ID | DATE | TOUR | MENAGERS | ORGANIQUES | VERRE |
---|---|---|---|---|---|
1 | 2023-01-11 | DEFAULT | 1 | ||
2 | 2023-01-25 | DEFAULT | 1 | ||
3 | 2023-02-08 | DEFAULT | 1 | ||
... | ... | ... | |||
27 | 2023-01-12 | DEFAULT | 1 | ||
28 | 2023-02-09 | DEFAULT | 1 | ||
... | ... | ... | |||
39 | 2023-01-03 | DEFAULT | 1 | ||
40 | 2023-01-17 | DEFAULT | 1 |
Next I needed a PHP script to convert the data to a JSON stream. I used an older script to retrieve the data called SimpleXLSX.php
(version 0.8.23) from the Excel sheet. Thanks to Sergey Shuchkin. Newer versions can be found on his github page: https://github.com/shuchkin/simplexlsx, but they will not work with the following script.
With the help of Bob Fisch (https://www.fisch.lu/) I cooked a script to do the job:
<?php
header('Content-Type: text/json');
// include library to ready XLSX
include 'SimpleXLSX.php';
// define what file to load
$IMPORT='2023-DECHETS_Calendrier_Mersch.xlsx';
// open the file
if($xlsx=SimpleXLSX::parse($IMPORT)) {
/*echo '<pre>';
print_r( $xlsx->sheetNames() );
print_r( $xlsx->sheetName( $xlsx->activeSheet ) );
print_r( $xlsx->rows(1) );
echo '</pre>';
exit;*/
// get name of cols of sheet #1 (Tours)
$rows=$xlsx->rows(1);
$cols=array();
foreach($rows[0] as $key=>$value) {
$cols[$value]=$key;
}
//echo '<pre>'.print_r($cols,true).'</pre>';
$data = array(array());
for($i=0; $i<count($rows); $i++) {
if (($rows[$i][$cols['MENAGERS']])=="1") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = 'waste';
}
else if (($rows[$i][$cols['ORGANIQUES']])=="1") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = "biomass";
}
else if (($rows[$i][$cols['VERRE']])=="1") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = "glas";
}
else if (($rows[$i][$cols['PAPIER']])=="1") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = "paper";
}
else if (($rows[$i][$cols['VALORLUX']])=="1") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = "valorlux";
}
else if (($rows[$i][$cols['ENCOMBRANTS']])=="1(SD)") {
$data[substr($rows[$i][$cols['DATE']],0,10)] = "bulky_waste";
}
}
$void = array_shift($data);
ksort($data);
echo json_encode($data);
}
else {
echo '{"error":"file not found"}'; // error in JSON format
}
?>
You find everything on github.
Save the script as index.php
together with the SimpleXLSX.php
script and the excel file in a folder of your server, and you can access the data. You find everything on github.
Look on https://www.weigu.lu/waste_collection/index.php
{"2023-01-03":"biomass","2023-01-06":"valorlux","2023-01-11":"waste","2023-01-12":"paper",
"2023-01-17":"biomass","2023-01-19":"bulky_waste","2023-01-20":"valorlux","2023-01-25":"waste",
...,
"2023-12-14":"paper","2023-12-19":"biomass","2023-12-22":"valorlux","2023-12-27":"waste"}
The hardware consists of a microcontroller board and some plastic :).
I choose a Lolin C3 mini board with an RISC-V core because I love the idea and an open standard instruction set architecture and because of the integrated WS2812B RGB LED.
The Luxemburgish bins look like the one I found on thingiverse from Stephen Miles (Thanks).
I had to add a hole to the bin body for my USB-C connector and did this in FreeCAD. But then I remarked there where problems with the stl from Stephen. So I grabbed the original file from Wojciech Madej (also Thanks :)) and stretched it to 100 mm height.
I used black PLA for the wheels and Felix Glas filament for the body and lid.
You find the Freecad and stl files on github.
I used my ESP Toolbox library and so we get OTA and logging via UDP. The data is also sent via MQTT to an MQTT server. In the config.h
file (comment #define USE_SECRETS
) or the secrets_waste_bin_reminder.h
file in a Secrets
folder inside your library folder you can change the important data like your password.
More infos: ESP Toolbox library
/*
waste_bin_reminder.ino
www.weigu.lu
Hardware used: Lolin C3 Mini
for UDP, listen on Linux PC (UDP_LOG_PC_IP) with netcat command:
nc -kulw 0 5558
more infos: www.weigu.lu/microcontroller/esptoolbox/index.html
---------------------------------------------------------------------------
Copyright (C) 2023 Guy WEILER www.weigu.lu
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---------------------------------------------------------------------------
*/
/*!!!!!! Make your changes in config.h (or secrets_xxx.h) !!!!!!*/
/*------ Comment or uncomment the following line suiting your needs -------*/
#define USE_SECRETS
#define OTA // if Over The Air update needed (security risk!)
//#define MQTTPASSWORD // if you want an MQTT connection with password
#define STATIC // if static IP needed (no DHCP)
/****** Arduino libraries needed ******/
#include "ESPToolbox.h" // ESP helper lib (more on weigu.lu)
#ifdef USE_SECRETS
// The file "secrets_xxx.h" has to be placed in a sketchbook libraries
// folder. Create a folder named "Secrets" in sketchbook/libraries and copy
// the config.h file there. Rename it to secrets_xxx.h
#include <secrets_waste_bin_reminder.h> // things you need to change are here
#else
#include "config.h" // or things you need to change are here
#endif // USE_SECRETS
#include <PubSubClient.h> // for MQTT
#include <ArduinoJson.h> // convert MQTT messages to JSON
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Adafruit_NeoPixel.h>
/****** WiFi and network settings ******/
const char *WIFI_SSID = MY_WIFI_SSID; // if no secrets, use config.h
const char *WIFI_PASSWORD = MY_WIFI_PASSWORD; // if no secrets, use config.h
#ifdef STATIC
IPAddress NET_LOCAL_IP (NET_LOCAL_IP_BYTES); // 3x optional for static IP
IPAddress NET_GATEWAY (NET_GATEWAY_BYTES); // look in config.h
IPAddress NET_MASK (NET_MASK_BYTES);
IPAddress NET_DNS (NET_DNS_BYTES);
#endif // ifdef STATIC
#ifdef OTA // Over The Air update settings
const char *OTA_NAME = MY_OTA_NAME;
const char *OTA_PASS_HASH = MY_OTA_PASS_HASH; // use the config.h file
#endif // ifdef OTA
IPAddress UDP_LOG_PC_IP(UDP_LOG_PC_IP_BYTES); // UDP log if enabled in setup
/****** MQTT settings ******/
const short MQTT_PORT = MY_MQTT_PORT;
WiFiClient espClient;
PubSubClient MQTT_Client(espClient);
#ifdef MQTTPASSWORD
const char *MQTT_USER = MY_MQTT_USER;
const char *MQTT_PASS = MY_MQTT_PASS;
#endif // MQTTPASSWORD
Adafruit_NeoPixel pixels(NUMPIXELS, PIXEL_PIN, NEO_GRB + NEO_KHZ800);
ESPToolbox Tb; // Create an ESPToolbox Object
/****** SETUP ****************************************************************/
void setup() {
Tb.set_udp_log(true, UDP_LOG_PC_IP, UDP_LOG_PORT);
#ifdef STATIC
Tb.set_static_ip(true,NET_LOCAL_IP, NET_GATEWAY, NET_MASK, NET_DNS);
#endif // ifdef STATIC
Tb.init_ntp_time();
Tb.init_wifi_sta(WIFI_SSID, WIFI_PASSWORD, NET_MDNSNAME, NET_HOSTNAME);
MQTT_Client.setBufferSize(MQTT_MAXIMUM_PACKET_SIZE);
MQTT_Client.setServer(MQTT_SERVER,MQTT_PORT); //open connection MQTT server
#ifdef OTA
Tb.init_ota(OTA_NAME, OTA_PASS_HASH);
#endif // ifdef OTA
init_pixels();
show_pixels();
Tb.log_ln("Setup done!");
}
/****** LOOP *****************************************************************/
void loop() {
#ifdef OTA
ArduinoOTA.handle();
#endif // ifdef OTA
if (Tb.non_blocking_delay(PUBLISH_TIME)) { // PUBLISH_TIME in config.h
which_bin = get_waste_bin_info_from_server();
if (which_bin!="no_info") {
Tb.log("Which bin to use: " + String(which_bin) + '\n');
set_wastebin_color(which_bin);
mqtt_publish();
Tb.log("Waiting some time before the next round...\n");
}
}
if (WiFi.status() != WL_CONNECTED) { // if WiFi disconnected, reconnect
Tb.init_wifi_sta(WIFI_SSID, WIFI_PASSWORD, NET_MDNSNAME, NET_HOSTNAME);
}
if (!MQTT_Client.connected()) { // reconnect mqtt client, if needed
mqtt_connect();
}
MQTT_Client.loop(); // make the MQTT live
delay(10); // needed for the watchdog! (alt. yield())
}
/********** MQTT functions ***************************************************/
// connect to MQTT server
void mqtt_connect() {
while (!MQTT_Client.connected()) { // Loop until we're reconnected
Tb.log("Attempting MQTT connection...");
#ifdef MQTTPASSWORD
if (MQTT_Client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) {
#else
if (MQTT_Client.connect(MQTT_CLIENT_ID)) { // Attempt to connect
#endif // ifdef MQTTPASSWORD
Tb.log_ln("MQTT connected");
MQTT_Client.subscribe(MQTT_TOPIC_IN.c_str());
}
else {
Tb.log("MQTT connection failed, rc=");
Tb.log(String(MQTT_Client.state()));
Tb.log_ln(" try again in 5 seconds");
delay(5000); // Wait 5 seconds before retrying
}
}
}
// MQTT get the time, relay flags ant temperature an publish the data
void mqtt_publish() {
DynamicJsonDocument doc_out(1024);
String mqtt_msg, we_msg;
Tb.get_time();
doc_out["datetime"] = Tb.t.datetime;
//doc_out["date"] = Tb.t.date;
//doc_out["tomorrow"] = Tb.t.tomorrow;
doc_out["waste_bin"] = which_bin;
serializeJson(doc_out, mqtt_msg);
MQTT_Client.publish(MQTT_TOPIC_OUT.c_str(),mqtt_msg.c_str());
Tb.log("MQTT published at ");
Tb.log_ln(Tb.t.time);
}
/********** HTTPS functions **************************************************/
String get_waste_bin_info_from_server() {
String json_payload;
DynamicJsonDocument http_doc(4096);
String which_bin;
int http_code;
WiFiClientSecure *Wifi_s_client = new WiFiClientSecure;
if(Wifi_s_client) {
Wifi_s_client->setInsecure(); // set secure client without certificate
HTTPClient Https; //create an HTTPClient instance
if (Https.begin(*Wifi_s_client, server_name)) { // HTTPS
http_code = Https.GET(); // start connection and send HTTP header
Tb.log("[HTTPS] GET... code: " + String(http_code) + '\n');
if (http_code > 0) { // httpCode will be negative on error
if (http_code == HTTP_CODE_OK || http_code == HTTP_CODE_MOVED_PERMANENTLY) {
json_payload = Https.getString(); // file found
deserializeJson(http_doc, json_payload);
//Tb.log(json_payload);
if (Tb.t.hour > 11) {
which_bin = String(http_doc[Tb.t.tomorrow]);
}
else {
which_bin = String(http_doc[Tb.t.date]);
}
}
else {
Tb.log("[HTTPS] GET... failed, error: " + String(http_code) + '\n');
}
Https.end();
}
else {
Tb.log("[HTTPS] Unable to connect\n");
return "no_info";
}
}
return which_bin;
}
}
/****** NeoPixel functions ***************************************************/
void init_pixels() {
pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
pixels.clear(); // Set all pixel colors to 'off'
}
void show_pixels() {
unsigned int my_delay = 250;
pixels.clear(); // Set all pixel colors to 'off'
pixels.setPixelColor(0, pixels.Color(255, 255, 255)); // white
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(165, 22, 22)); // brown
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(0, 255, 0)); // green
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(0, 0, 255)); // blue
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(125, 0, 125)); // light blue
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(255, 255, 0)); // yellow
pixels.show();
delay (my_delay);
pixels.setPixelColor(0, pixels.Color(0, 0, 0)); // off
pixels.show();
delay (my_delay);
}
void set_wastebin_color(String which_bin) {
if (which_bin == "waste") {
pixels.setPixelColor(0, pixels.Color(255, 255, 255)); // white
}
else if (which_bin == "biomass") {
pixels.setPixelColor(0, pixels.Color(165, 22, 22)); // brown
}
else if (which_bin == "glas") {
pixels.setPixelColor(0, pixels.Color(0, 255, 0)); // green
}
else if (which_bin == "paper") {
pixels.setPixelColor(0, pixels.Color(0, 0, 255)); // blue
}
else if (which_bin == "valorlux") {
pixels.setPixelColor(0, pixels.Color(125, 0, 125)); // light blue
}
else if (which_bin == "bulky_waste") {
pixels.setPixelColor(0, pixels.Color(255, 255, 0)); // yellow
}
else if (which_bin == "null") {
pixels.setPixelColor(0, pixels.Color(0, 0, 0));
}
pixels.show();
}