Arduino and NodeMCU Aquarium Weather
Reproducing real-time local weather for your aquarium occupants.


OpenWeatherMap
1. Current Time
2. Sunrise
3. Sunset
4. Condition
5. Wind Speed
6. Cloud %

NodeMCU
1. Get weather data
2. Segregate conditions
3. Convert values to strings
4. Aggregate sequence
5. Serial to Arduino



Arduino
1. Receive and test serial data
2. Split data to functions
3. Set light position to time of day
4. Set cloud percent relative to light position
5. Activate weather event (rain, etc)
6. Set fan speed to mapped wind speed m/s

Conditions
1. Sunrise to Sunset
2. Clouds
3. Rain or Drizzle
4. Thunderstorm
5. Snow
6. Fog
7. Wind
8. Tides



Ingredients
NodeMCU
Arduino Uno
MG995 Servo (2)
MOSFET (3) + 10K
TIP120 (2) + 2.2K
Water Pump 5V
Neopixels + 2.2K + 1000uf
Vinyl Tubing 3/16ID
Peltier Element (2) + Heat Sink
9G Servo (4)
Gang Valve (3)
40mm 12V Fan
80mm 5V Fan
Lever Switch
160 LED 5600K
24V 12V-500W 9V 5V
Ultrasonic Mist Maker
Wire Strainer (Mesh)
Acrylic Tube

5 gallon tank demonstrated
aka "(too) small foot print" version


Notations
1. Water temperature remains static, bcz the above are novelties that shd not harm the aquarium inhabitants.
2. This --> Fredo6 Rounded Corners Sketchup plugin. https://sketchucation.com/pluginstore?pauthor=fredo6
3. Ran out of black filament for a spell, painted some natural with black acrylic ... ignore the sheen.
4. Keep it simple, stupid -- as the variables of if/then when controlling several elements at once -- reason why NodeMCU isolated the data collection and aggregation while then displacing the labor to the Arduino Uno.
5. "Cheating" on tides as the OpenWeatherMap does not provide - will oscillate tides based on a 28 day cycle relative to time.
6. Debated on mp3 module and thunder noise .. figured less is more when it comes to obnoxious items around the tank creaking and whirring.
7. Peltier cascading -- thank me later.
8. NodeMCU [gets] local weather data once every hour via https://github.com/ThingPulse/esp8266-weather-station
9. Needed rain and snow distribution --> wire mesh strainer material from the dollar store. Thinkingness!
10. Snow? Snaybe. Clumping and scraping permitted the hint of falling ice but this element was 90% fail.
11. Larger tank (10g+) would have better allowed the cloud to light distribution (designed differently, tracks?!) but - as is the running theme - limited money, supplies, space, time. Thought that counts ~(O_o)~ next...

NodeMCU
// Aquarium Weather
// Vije Miller
// NodeMCU Code
// ESP8266 Weather Credit:
// https://github.com/ThingPulse/esp8266-weather-station

// Weather
#include <ESP8266WiFi.h>
#include <JsonListener.h>
#include "OpenWeatherMapCurrent.h"
#include <math.h>
#include <SoftwareSerial.h>

// Serial to Arduino
SoftwareSerial com(D6, D5); // Rx, Tx

// Open Weather API
OpenWeatherMapCurrent client;
// API
String OPEN_WEATHER_MAP_APP_ID = "api number";
// Weather Address
String OPEN_WEATHER_MAP_LOCATION_ID = "location id";
// Language
String OPEN_WEATHER_MAP_LANGUAGE = "en";
// Metric?
boolean IS_METRIC = true;

// WIFI
const char* ESP_HOST_NAME = "esp-" + ESP.getFlashChipId();
const char* WIFI_SSID = "network name";
const char* WIFI_PASSWORD = "network password";
WiFiClient wifiClient;

void connectWifi() {
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.println(WIFI_SSID);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Connected");
Serial.println(WiFi.localIP());
}

// Weather Values
int emit; // current time --> unix epoch
int sunr; // sunrise
int suns; // sunset
int clou; // cloud percent
int wind; // wind speed m/s
String main; // conditions

// Values to Send
int light;
int cloud;
int event;
int wind_speed;
String sequence;

// reset
int mulligan = D7;

void setup() {
// for testing
delay(5000);

digitalWrite(mulligan, HIGH);
delay(500);
pinMode(mulligan, OUTPUT);

Serial.begin(115200);
while (!Serial) {
delay(1);
}
delay(500);
// Arduino
com.begin(9600);

connectWifi();
}

void loop() {

local_weather();
delay(1000);

time_position();
cloud_position();
wind_position();
event_position();
aggregate();

delay(1000);
transmit();

// Transmit Serial Every 30 Seconds
// For 1 Hour Then Reset
for (int ditto = 0; ditto < 120; ditto++) {
transmit();
delay(30000);
}

// reset
// bcz -> NodeMCU n' such
digitalWrite(mulligan, LOW);
delay(1000);
}

void local_weather() {
// Get the weather report
OpenWeatherMapCurrentData data;
client.setLanguage(OPEN_
WEATHER_MAP_LANGUAGE);
client.setMetric(IS_METRIC);
client.updateCurrentById(&data, OPEN_WEATHER_MAP_APP_ID, OPEN_WEATHER_MAP_LOCATION_ID);

// Convert Data to Values
emit = data.observationTime;
sunr = data.sunrise;
suns = data.sunset;
main = String(data.main);
wind = data.windSpeed;
clou = data.clouds;

// Print Data
Serial.println(emit);
Serial.println(sunr);
Serial.println(suns);
Serial.println(main);
Serial.println(wind);
Serial.println(clou);
Serial.println();
}

void time_position() {
// before sunrise
if (emit < sunr) {
light = 0;
// after sunset
} else if (emit > suns) {
light = 0;
// after sunrise before sunset
} else if (emit > sunr && emit < suns) {
// maths ugh
int total_dif = suns - sunr;
int since = emit - sunr;
float percent = (float)(since * 100) / total_dif;
percent = round(percent);
light = percent;
} else {
// error allowance
light = 0;
}
Serial.println(light);
}

void cloud_position() {
cloud = clou;
Serial.println(cloud);
}

void wind_position() {
wind_speed = round(wind);

Serial.println(wind_speed);
}

void event_position() {
// OpenWeather Conditions --> https://openweathermap.org/weather-conditions
// Selecting the [main] following: 1 Thunderstorm, 2 Drizzle, 3 Rain, 4 Snow, 5 Fog, 6 Clouds
if (main.indexOf("Thunderstorm") >= 0) {
event = 1;
} else if (main.indexOf("Drizzle") >= 0) {
event = 2;
} else if (main.indexOf("Rain") >= 0) {
event = 3;
} else if (main.indexOf("Snow") >= 0) {
event = 4;
} else if (main.indexOf("Fog") >= 0) {
event = 5;
} else if (main.indexOf("Clouds") >= 0) {
event = 6;
} else {
event = 7;
}
Serial.println(event);
}

void aggregate() {
// Combine values to one string to send
// Done to ease serial limitations
// Example: 0800500052 [10]
// 080 = 80% of day
// 050 = 50% clouds
// 005 = 5 mph wind
// 2 = Drizzle (see event_position)

String light_str = String(light);
if (light < 100 && light > 9) {
light_str = String("0" + light_str);
} else if (light < 10) {
light_str = String("00" + light_str);
} else {
light_str = String(100);
}

String cloud_str = String(cloud);
if (cloud < 100 && cloud > 9) {
cloud_str = String("0" + cloud_str);
} else if (cloud < 10) {
cloud_str = String("00" + cloud_str);
} else {
cloud_str = String(100);
}

String wind_str = String(wind_speed);
if (wind_speed < 100 && wind_speed > 9) {
wind_str = String("0" + wind_str);
} else if (wind_speed < 10) {
wind_str = String("00" + wind_str);
} else {
wind_str = String(100);
}

String event_str = String(event);

sequence = String(light_str + cloud_str + wind_str + event_str);
Serial.println(sequence);
Serial.println("");
}

void transmit() {
com.println(sequence);
}



Arduino
// Aquarium Weather
// Vije Miller
// Arduino Code

#include <VarSpeedServo.h>
#include <FastLED.h>
#include <SoftwareSerial.h>

// Serial
SoftwareSerial com(5, 6); // Rx, Tx

// Light
VarSpeedServo light_servo;
const int light_servo_pin = 2;
const int light_dimmer = A0; // 0-255

// Cloud
VarSpeedServo cloud_servo;
const int cloud_servo_pin = 3;

// Pump
const int pump = A1; // 0-255
// lever switch kills power to pump
const int tank_level = 16;

// Lightning (Neopixels)
// FastLED Lightning --> James Bruce
// Tip: Actually 20 but 50+ Spreads Effect
#define NUM_LEDS 50
#define DATA_PIN 4
CRGB leds[NUM_LEDS];

// Valves
VarSpeedServo rain_valve_servo;
const int rain_valve = 7;
VarSpeedServo snow_valve_servo;
const int snow_valve = 8;
VarSpeedServo tank_valve_servo;
const int tank_valve = 9;

// Snow
VarSpeedServo snow_maker_servo;
const int snow_maker = 10;
const int snow = 11;

// Fog
const int fog = 12;

// Wind (meters/sec)
const int fan = A3; // 0-255

// Values
String rec; // Weather Data
String last; // Saved Weather
const int off = 0; // MOSFET Controllers OFF
const int top = 255; // Peak
const int start = 0; // light + cloud
const int middle = 65; // light + cloud
const int full = 130; // light + cloud
String light_percent;
String cloud_percent;
String wind_speed; // m/s
String event;
int light_value;
int cloud_value;
int wind_value;
int event_value;
int light_pos;
int light_mos;
int cloud_pos;
int wind_pos;
unsigned long who;
unsigned long what;
unsigned long when;
int tank_level_state;
int tide_state = 0;

void setup() {
Serial.begin(9600);
com.begin(9600);

// Declare and Set Defaults

// Light
light_servo.attach(light_servo_pin);
light_servo.write(0, 80, false);
pinMode(light_dimmer, OUTPUT);
analogWrite(light_dimmer, off);

// Cloud
cloud_servo.attach(cloud_servo_pin);
cloud_servo.write(130, 80, false);

// Rain
rain_valve_servo.attach(rain_valve);
rain_valve_servo.write(0, 80, false);

// Snow
snow_valve_servo.attach(snow_valve);
snow_valve_servo.write(0, 80, false);
pinMode(snow, OUTPUT);
digitalWrite(snow, LOW);

// Pump
tank_valve_servo.attach(tank_valve);
tank_valve_servo.write(0, 80, true);
pinMode(pump, OUTPUT);
digitalWrite(pump, off);
pinMode(tank_level, INPUT);

// Fog
pinMode(fog, OUTPUT);
digitalWrite(fog, LOW);

// Lightning
FastLED.addLeds(leds, NUM_LEDS);

// Wind
pinMode(fan, OUTPUT);
analogWrite(fan, off);
}

void loop() {
// Clean House
com.flush();
// Wait for Serial from NodeMCU
while (!com.available()) {
; // do nothing
}
// What did they say?!
rec = com.readString();
Serial.println(rec);

// Check if string is 10 characters
// 0000000000
if (rec.length() != 10) {
// Check if different than last data
if (rec != last) {
// Process Data
// 000-000-000-0
// 1 Light %
// 2 Cloud %
// 3 Wind Speed
// 4 Event

// Parse Light
light_percent = rec.substring(0, 2);
light_value = light_percent.toInt();
Serial.println(light_value);

// Parse Cloud
cloud_percent = rec.substring(3, 5);
cloud_value = cloud_percent.toInt();
Serial.println(cloud_value);

// Parse Wind
wind_speed = rec.substring(6, 8);
wind_value = wind_speed.toInt();
Serial.println(wind_value);

// Parse Event
event = rec.substring(9, 9);
event_value = event.toInt();
Serial.println(event_value);

// Fire Functions
light_okay();
cloud_what();
tide();
wind_sure();
event_dunno();

// Save Last Data
last = String(rec);
} else {

// Status Quo
light_okay();
cloud_what();
tide();
wind_sure();
event_dunno();

} // parse and event call
} // if not 10 characters
} // loop

void light_okay() {
// 130 = East 0 = West
light_pos = map(light_value, 0, 100, 130, 0);
light_servo.write(light_pos, 80, true);

// Sunrise and Sunset
if (light_pos > 115) {
light_mos = map(light_pos, 130, 115, 0, 255);
analogWrite(light_dimmer, light_mos);
} if (light_pos < 15) {
light_mos = map(light_pos, 15, 0, 255, 0);
analogWrite(light_dimmer, light_mos);
} else {
analogWrite(light_dimmer, 255);
}
}

void cloud_what() {
if (event_value == 7) {
// if clear --> position opposite after 50% light
if (light_pos < 50) {
cloud_servo.write(start, 80, true);
} else if (light_pos >= 50) {
cloud_servo.write(full, 80, true);
}
} else if (event_value == 6) {
// if clouds then position percent
// then percent relative to light position
// 65 = 100%
if (light_pos < 50) {
cloud_pos = map(cloud_value, 0, 100, 130, 65);
cloud_servo.write(cloud_pos, 80, true);
} else if (light_pos >= 50) {
cloud_pos = map(cloud_value, 0, 100, 0, 65);
cloud_servo.write(cloud_pos, 80, true);
}
} else {
// if event then cloud at peak
cloud_servo.write(middle, 80, true);
}
}

void wind_sure() {
// m/s to percent-ish
// 15 m/s = 33.6 mph
if (wind_value <= 15) {
wind_pos = map(wind_value, 0, 15, 0, 255);
analogWrite(fan, wind_pos);
} else if (wind_value > 15) {
analogWrite(fan, 255);
}
}

void event_dunno() {
if (event_value == 1) {
thunderstorm_boom();
} else if (event_value == 2) {
drizzle_fizz();
} else if (event_value == 3) {
rain_game();
} else if (event_value == 4) {
snow_man();
} else if (event_value == 5) {
fog_bottom();
}
}

void tide() {
// Every 6 Hours Change
// Uses Arduino Active As Time
// This is cheating! No data!

// get time since program began
when = millis();
// first time or time reset?
if (what < 1000) {
what = when;
// no action
} else {
// math difference
who = when - what;
// is it 6 hours ago?
if (who >= 21600000) {
// low or high tide?
// 0 = high tide 1 = low tide
if (tide_state == 0) {
// open tank valve
tank_valve_servo.write(90, 80, true);
delay(1000);
while (tank_level_state == LOW) {
digitalWrite(pump, HIGH);
// check float level
tank_level_state = digitalRead(tank_level);
}
digitalWrite(pump, LOW);
tank_valve_servo.write(0, 80, true);
tide_state = 1;
what = when;
} else if (tide_state == 1) {
// open tank valve to dump tank
tank_valve_servo.write(90, 80, true);
delay(20000); // adj
tank_valve_servo.write(0, 80, true);
tide_state = 0;
what = when;
}
}
}
}

void thunderstorm_boom() {
// open rain valve
analogWrite(light_dimmer, 120);
rain_valve_servo.write(90, 80, true);
delay(1000);
// let it pour
for (int x = 0; x < 25; x++) {
digitalWrite(pump, HIGH);
// call lightning for delay
for (int bolt = 0; bolt < 10; bolt++) {
lightning();
delay(500);
}
digitalWrite(pump, LOW);
// lightning again (so smart!)
for (int bolt = 0; bolt < 10; bolt++) {
lightning();
delay(500);
}
}
rain_valve_servo.write(0, 80, true);
digitalWrite(pump, LOW);
}

void drizzle_fizz() {
analogWrite(light_dimmer, 120);
// open rain valve
rain_valve_servo.write(90, 80, true);
delay(1000);
// less than rain
for (int x = 0; x < 20; x++) {
digitalWrite(pump, HIGH);
delay(3000);
digitalWrite(pump, LOW);
delay(6000);
}
rain_valve_servo.write(0, 80, true);
digitalWrite(pump, LOW);
}

void rain_game() {
analogWrite(light_dimmer, 120);
// open rain valve
rain_valve_servo.write(90, 80, true);
delay(1000);
// let it pour
for (int x = 0; x < 20; x++) {
digitalWrite(pump, HIGH);
delay(5000);
digitalWrite(pump, LOW);
delay(5000);
}
rain_valve_servo.write(0, 80, true);
digitalWrite(pump, LOW);
}

// This requires tweaking!
void snow_man() {
analogWrite(light_dimmer, 120);
// relay peltier elements + fan (2)
digitalWrite(snow, HIGH);
delay(5000);
// open snow valve
snow_valve_servo.write(90, 80, true);
delay(1000);

for (int redundant = 0; redundant = 50; redundant++) {
// intermittent pump (drip)
for (int x = 0; x < 20; x++) {
digitalWrite(pump, HIGH);
delay(3000);
digitalWrite(pump, LOW);
delay(5000);
}
// grate the snow
for (int y = 0; y < 10; y++) {
snow_maker_servo.write(45, 100, true);
delay(500);
snow_maker_servo.write(0, 100, true);
delay(500);
}
}

digitalWrite(snow, LOW);
digitalWrite(pump, LOW);
snow_valve_servo.write(0, 80, true);
}

void fog_bottom() {
analogWrite(light_dimmer, 120);
// turn on fog maker (relay)
digitalWrite(fog, HIGH);
// run for 60 seconds
delay(60000);
digitalWrite(fog, LOW);
// return
}

void lightning() {
switch (random(1, 3)) {
case 1:
thunderburst();
delay(random(10, 500));
break;
case 2:
rolling();
break;
case 3:
crack();
delay(random(50, 250));
break;
}
}

void reset() {
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV( 0, 0, 0);
}
FastLED.show();
}

void rolling() {
for (int r = 0; r < random(2, 10); r++) {
for (int i = 0; i < NUM_LEDS; i++) {
if (random(0, 100) > 90) {
leds[i] = CHSV( 0, 0, 255);
}
else {
leds[i] = CHSV(0, 0, 0);
}
}
FastLED.show();
delay(random(5, 100));
reset();
}
}

void crack() {
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV( 0, 0, 255);
}
FastLED.show();
delay(random(10, 100));
reset();
}

void thunderburst() {
int rs1 = random(0, NUM_LEDS / 2);
int rl1 = random(10, 20);
int rs2 = random(rs1 + rl1, NUM_LEDS);
int rl2 = random(10, 20);
for (int r = 0; r < random(3, 6); r++) {
for (int i = 0; i < rl1; i++) {
leds[i + rs1] = CHSV( 0, 0, 255);
}
if (rs2 + rl2 < NUM_LEDS) {
for (int i = 0; i < rl2; i++) {
leds[i + rs2] = CHSV( 0, 0, 255);
}
}
FastLED.show();
delay(random(10, 50));
reset();
delay(random(10, 50));
}
}

Archive
http://vijemiller.com/index.php?miter=019331204333
http://vijemiller.com/index.php?miter=019331204224
Fritzing --> ?miter=019330081544
http://vijemiller.com/index.php?miter=019325212901
http://vijemiller.com/index.php?miter=019323232420
http://vijemiller.com/index.php?miter=019309184318
http://vijemiller.com/index.php?miter=019309184113
http://vijemiller.com/index.php?miter=019301181534
http://vijemiller.com/index.php?miter=019300181253