├── .gitignore ├── src ├── sensors │ ├── microphone.h │ ├── bh1750sensor.h │ ├── dhtsensor.h │ ├── bme280sensor.h │ ├── motionsensor.h │ ├── bh1750sensor.cpp │ ├── bme280sensor.cpp │ ├── motionsensor.cpp │ ├── dhtsensor.cpp │ └── microphone.cpp └── weather │ ├── openweather.h │ ├── OpenWeatherMapCurrent.h │ ├── OpenWeatherMapForecast.h │ ├── OpenWeatherMapCurrent.cpp │ └── OpenWeatherMapForecast.cpp ├── wifi.h ├── notifications.h ├── mqtt.h ├── api.h ├── neopixel.h ├── display.h ├── README.md ├── wifi.cpp ├── LICENSE ├── notifications.cpp ├── neopixel.cpp ├── mqtt.cpp ├── display.cpp ├── api.cpp ├── config.example.h └── saloon.ino /.gitignore: -------------------------------------------------------------------------------- 1 | config.h 2 | build 3 | -------------------------------------------------------------------------------- /src/sensors/microphone.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | double measureNoiseLevel(int pin); 4 | -------------------------------------------------------------------------------- /wifi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void setupWifi(); 4 | void setupMDNS(); 5 | void setupNTP(); 6 | void updateMDNS(); 7 | -------------------------------------------------------------------------------- /src/sensors/bh1750sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Arduino.h" 4 | 5 | void setupBH1750(); 6 | uint16_t measureBH1750(); 7 | -------------------------------------------------------------------------------- /src/sensors/dhtsensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void setupDHT(int pin, int sensor_type); 4 | void measureDHT(float* temperature, float* humidity); 5 | -------------------------------------------------------------------------------- /src/sensors/bme280sensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void setupBME280(int sda, int sclk); 4 | void measureBME280(float* temperature, float* humidity); 5 | -------------------------------------------------------------------------------- /notifications.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void flashLED(uint r, uint g, uint b); 6 | void notify(String text, uint r, uint g, uint b); 7 | -------------------------------------------------------------------------------- /mqtt.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void setupMQTT(); 4 | void mqttPublish(char *topic, float payload); 5 | void mqttPublishState(bool state); 6 | void mqttReconnect(); 7 | void mqttLoop(); 8 | -------------------------------------------------------------------------------- /api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void handleClient(); 6 | void handleRoot(); 7 | void handleNotification(); 8 | void handleNotFound(); 9 | void setupAPI(); 10 | -------------------------------------------------------------------------------- /src/sensors/motionsensor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | static bool motionTriggered = false; 6 | static time_t motionLast = time(nullptr); 7 | 8 | bool handleMotionSensor(int pin); 9 | -------------------------------------------------------------------------------- /neopixel.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void setupNeoPixel(float brightness); 6 | void setNeoPixelColor(uint16_t n, uint32_t c); 7 | void setAllNeoPixelColor(uint r, uint g, uint b); 8 | void rainbowCycle(uint8_t wait); 9 | uint32_t Wheel(byte WheelPos); 10 | -------------------------------------------------------------------------------- /src/sensors/bh1750sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "bh1750sensor.h" 2 | 3 | #include 4 | 5 | static BH1750FVI LightSensor(BH1750FVI::k_DevModeContLowRes); 6 | 7 | void setupBH1750() 8 | { 9 | LightSensor.begin(); 10 | } 11 | 12 | uint16_t measureBH1750() 13 | { 14 | return LightSensor.GetLightIntensity(); 15 | } 16 | -------------------------------------------------------------------------------- /display.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | const String WDAY_NAMES[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" }; 7 | 8 | void setupDisplay(); 9 | void clearDisplay(); 10 | void updateDisplay(); 11 | 12 | unsigned int displayWidth(); 13 | unsigned int displayHeight(); 14 | 15 | void paintCenterString(uint x, uint y, String s); 16 | 17 | void paintWeatherIconAndTemp(uint x, uint y, float temperature, int weatherId, tm* timeInfo); 18 | void paintWeatherDesc(uint x, uint y, String desc); 19 | void paintTime(uint x, uint y, tm* timeInfo); 20 | -------------------------------------------------------------------------------- /src/sensors/bme280sensor.cpp: -------------------------------------------------------------------------------- 1 | #include "bme280sensor.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | static Adafruit_BME280 bme; // I2C 8 | 9 | void setupBME280(int sda, int sclk) 10 | { 11 | Wire.begin(sda, sclk); 12 | Wire.setClock(100000); 13 | 14 | if (!bme.begin(0x76)) { 15 | Serial.println("Could not initialize BME280 sensor!"); 16 | } 17 | } 18 | 19 | void measureBME280(float* temperature, float* humidity) 20 | { 21 | static float t, h; 22 | 23 | t = bme.readTemperature(); 24 | h = bme.readHumidity(); 25 | 26 | *temperature = t; 27 | *humidity = h; 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # saloon 2 | 3 | Saloon, an Arduino/ESP-based Information Monitor 4 | 5 | ## Support 6 | 7 | ### Sensors 8 | 9 | - [x] BME280 (temperature, humidity) 10 | - [x] DHT-11/22 (temperature, humidity) 11 | - [x] BH1750 (light sensor) 12 | - [x] PIR Motion Sensor 13 | - [x] Microphone 14 | 15 | ### Displays 16 | 17 | - [x] SH1106 18 | - [x] SSD1306 19 | 20 | ### LEDs 21 | 22 | - [x] Neopixel 23 | - [x] RGB LED 24 | - [x] Built-in LED 25 | 26 | ### APIs 27 | 28 | - [x] MQTT 29 | - [x] OpenWeatherMap 30 | 31 | ## Configure 32 | 33 | Make a copy of `config.example.h` named `config.h` and start editing it to your 34 | needs. 35 | 36 | ## Build 37 | 38 | arduino --pref build.path=./build --verify saloon.ino 39 | -------------------------------------------------------------------------------- /src/sensors/motionsensor.cpp: -------------------------------------------------------------------------------- 1 | #include "motionsensor.h" 2 | 3 | #include 4 | 5 | bool handleMotionSensor(int pin) 6 | { 7 | int pirValue = digitalRead(pin); 8 | 9 | if (pirValue > 0 && !motionTriggered) { 10 | motionTriggered = true; 11 | motionLast = time(nullptr); 12 | 13 | /* 14 | Serial.println("Motion detected"); 15 | digitalWrite(STATUS_LED_RED, LOW); 16 | analogWrite(STATUS_LED_BLUE, 10); 17 | */ 18 | return true; 19 | } else if (pirValue == 0 && motionTriggered) { 20 | motionTriggered = false; 21 | motionLast = time(nullptr); 22 | 23 | /* 24 | Serial.println("Motion gone"); 25 | analogWrite(STATUS_LED_RED, 10); 26 | digitalWrite(STATUS_LED_BLUE, LOW); 27 | */ 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | -------------------------------------------------------------------------------- /src/sensors/dhtsensor.cpp: -------------------------------------------------------------------------------- 1 | #include "dhtsensor.h" 2 | 3 | #include 4 | #include "DHTesp.h" 5 | 6 | static DHTesp dht; 7 | 8 | void setupDHT(int pin, int sensor_type) 9 | { 10 | if (sensor_type == 11) { 11 | dht.setup(pin, DHTesp::DHT11); 12 | } else if (sensor_type == 22) { 13 | dht.setup(pin, DHTesp::DHT22); 14 | } 15 | } 16 | 17 | void measureDHT(float* temperature, float* humidity) 18 | { 19 | static unsigned long measurement_timestamp = millis(); 20 | static float t, h; 21 | 22 | /* Measure once every four seconds. */ 23 | if (millis() - measurement_timestamp > dht.getMinimumSamplingPeriod() * 4) { 24 | float nt, nh; 25 | nt = dht.getTemperature(); 26 | nh = dht.getHumidity(); 27 | 28 | if (nt == nt) { // check for nt being NaN 29 | t = nt; 30 | } 31 | if (nh == nh) { // check for nh being NaN 32 | h = nh; 33 | } 34 | 35 | measurement_timestamp = millis(); 36 | } 37 | 38 | *temperature = t; 39 | *humidity = h; 40 | } 41 | -------------------------------------------------------------------------------- /src/sensors/microphone.cpp: -------------------------------------------------------------------------------- 1 | #include "microphone.h" 2 | 3 | #include 4 | 5 | // Sample window width in mS (50 mS = 20Hz) 6 | const int sampleWindow = 50; 7 | 8 | double measureNoiseLevel(int pin) 9 | { 10 | unsigned int sample; 11 | unsigned int signalMax = 0; 12 | unsigned int signalMin = 1024; 13 | unsigned long startMillis = millis(); 14 | 15 | while (millis() - startMillis < sampleWindow) { 16 | sample = analogRead(pin); 17 | if (sample < 1024) { 18 | // toss out spurious readings 19 | if (sample > signalMax) { 20 | // save just the max levels 21 | signalMax = sample; 22 | } 23 | if (sample < signalMin) { 24 | // save just the min levels 25 | signalMin = sample; 26 | } 27 | } 28 | } 29 | 30 | if (signalMin >= signalMax) { 31 | return 0; 32 | } 33 | 34 | // convert to volts 35 | unsigned int peakToPeak = signalMax - signalMin; 36 | return (peakToPeak * 5.0) / 1024; 37 | } 38 | -------------------------------------------------------------------------------- /wifi.cpp: -------------------------------------------------------------------------------- 1 | #include "wifi.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "config.h" 8 | #include "notifications.h" 9 | 10 | // #define TZ_SEC ((TIME_TZ)*3600) 11 | // #define DST_SEC ((TIME_DST_MN)*60) 12 | 13 | void setupWifi() 14 | { 15 | WiFi.mode(WIFI_STA); 16 | WiFi.begin(WIFI_SSID, WIFI_PSK); 17 | 18 | // Wait for connection 19 | while (WiFi.status() != WL_CONNECTED) { 20 | Serial.print("."); 21 | 22 | if (NOTIFICATION_CONNECT) { 23 | flashLED(0, 0, 255); 24 | } 25 | 26 | delay(50); 27 | } 28 | 29 | Serial.println(); 30 | Serial.print("Connected to "); 31 | Serial.println(WIFI_SSID); 32 | Serial.print("IP address: "); 33 | Serial.println(WiFi.localIP()); 34 | } 35 | 36 | void setupMDNS() 37 | { 38 | if (MDNS.begin("esp8266")) { 39 | Serial.println("MDNS responder started"); 40 | } 41 | } 42 | 43 | void updateMDNS() { 44 | MDNS.update(); 45 | } 46 | 47 | void setupNTP() 48 | { 49 | setenv("TZ", TIME_TZ, 1); 50 | configTime(0, 0, "pool.ntp.org"); 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Muehlhaeuser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /notifications.cpp: -------------------------------------------------------------------------------- 1 | #include "notifications.h" 2 | 3 | #include "config.h" 4 | #include "display.h" 5 | #include "neopixel.h" 6 | 7 | void setColor(uint r, uint g, uint b) 8 | { 9 | if (LED_RED >= 0 && LED_GREEN >= 0 && LED_BLUE >= 0) { 10 | analogWrite(LED_RED, r); 11 | analogWrite(LED_GREEN, g); 12 | analogWrite(LED_BLUE, b); 13 | } 14 | if (NEOPIXEL >= 0) { 15 | setAllNeoPixelColor(r, g, b); 16 | } 17 | } 18 | 19 | void flashLED(uint r, uint g, uint b) 20 | { 21 | if (NEOPIXEL >= 0) { 22 | rainbowCycle(3); 23 | setColor(0, 0, 0); 24 | return; 25 | } 26 | 27 | for (int i = 0; i < 255; i++) { 28 | setColor((float(i) / 255.0) * float(r), 29 | (float(i) / 255.0) * float(g), 30 | (float(i) / 255.0) * float(b)); 31 | delay(1); 32 | } 33 | delay(50); 34 | 35 | for (int i = 0; i < 255; i++) { 36 | setColor((1.0 - (float(i) / 255.0)) * float(r), 37 | (1.0 - (float(i) / 255.0)) * float(g), 38 | (1.0 - (float(i) / 255.0)) * float(b)); 39 | delay(1); 40 | } 41 | setColor(0, 0, 0); 42 | } 43 | 44 | void notify(String text, uint r, uint g, uint b) 45 | { 46 | clearDisplay(); 47 | paintCenterString(displayWidth() / 2, displayHeight() / 2, text); 48 | updateDisplay(); 49 | 50 | for (int x = 0; x < 5; x++) { 51 | flashLED(r, g, b); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /neopixel.cpp: -------------------------------------------------------------------------------- 1 | #include "neopixel.h" 2 | 3 | #include 4 | 5 | #include "config.h" 6 | 7 | static Adafruit_NeoPixel np = Adafruit_NeoPixel(NEOPIXEL_PIXEL_COUNT, NEOPIXEL, NEO_GRB + NEO_KHZ800); 8 | 9 | void setupNeoPixel(float brightness) 10 | { 11 | np.begin(); 12 | np.setBrightness(brightness); 13 | } 14 | 15 | void setNeoPixelColor(uint16_t n, uint32_t c) 16 | { 17 | np.setPixelColor(n, c); 18 | } 19 | 20 | void setAllNeoPixelColor(uint r, uint g, uint b) 21 | { 22 | auto col = np.Color(r, g, b); 23 | for (uint16_t i = 0; i < np.numPixels(); i++) { 24 | setNeoPixelColor(i, col); 25 | } 26 | np.show(); 27 | } 28 | 29 | // Slightly different, this makes the rainbow equally distributed throughout 30 | void rainbowCycle(uint8_t wait) 31 | { 32 | uint16_t i, j; 33 | 34 | for(j=0; j<256*2; j++) { // 5 cycles of all colors on wheel 35 | for(i=0; i< np.numPixels(); i++) { 36 | setNeoPixelColor(i, Wheel(((i * 256 / np.numPixels()) + j) & 255)); 37 | } 38 | np.show(); 39 | delay(wait); 40 | } 41 | } 42 | 43 | // Input a value 0 to 255 to get a color value. 44 | // The colours are a transition r - g - b - back to r. 45 | uint32_t Wheel(byte WheelPos) 46 | { 47 | WheelPos = 255 - WheelPos; 48 | if(WheelPos < 85) { 49 | return np.Color(255 - WheelPos * 3, 0, WheelPos * 3); 50 | } 51 | if(WheelPos < 170) { 52 | WheelPos -= 85; 53 | return np.Color(0, WheelPos * 3, 255 - WheelPos * 3); 54 | } 55 | WheelPos -= 170; 56 | return np.Color(WheelPos * 3, 255 - WheelPos * 3, 0); 57 | } 58 | -------------------------------------------------------------------------------- /src/weather/openweather.h: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | const uint8_t MAX_FORECASTS = 28; 4 | 5 | #include "OpenWeatherMapCurrent.h" 6 | #include "OpenWeatherMapForecast.h" 7 | 8 | static OpenWeatherMapCurrentData currentWeather; 9 | static OpenWeatherMapCurrent currentWeatherClient; 10 | 11 | static OpenWeatherMapForecastData forecasts[MAX_FORECASTS]; 12 | static OpenWeatherMapForecast forecastClient; 13 | 14 | void updateWeather() 15 | { 16 | currentWeatherClient.setMetric(IS_METRIC); 17 | currentWeatherClient.setLanguage(OPEN_WEATHER_MAP_LANGUAGE); 18 | currentWeatherClient.updateCurrentById(¤tWeather, OPEN_WEATHER_MAP_APP_ID, OPEN_WEATHER_MAP_LOCATION_ID); 19 | } 20 | 21 | void updateWeatherForecast() 22 | { 23 | forecastClient.setMetric(IS_METRIC); 24 | forecastClient.setLanguage(OPEN_WEATHER_MAP_LANGUAGE); 25 | uint8_t allowedHours[] = { 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 }; 26 | forecastClient.setAllowedHours(allowedHours, sizeof(allowedHours)); 27 | forecastClient.updateForecastsById(forecasts, OPEN_WEATHER_MAP_APP_ID, OPEN_WEATHER_MAP_LOCATION_ID, MAX_FORECASTS); 28 | } 29 | 30 | OpenWeatherMapForecastData forecastForWDay(int wday) 31 | { 32 | OpenWeatherMapForecastData h; 33 | h.weatherId = 0; 34 | 35 | bool first = true; 36 | for (int i = 0; i < MAX_FORECASTS; i++) { 37 | if (forecasts[i].weatherId <= 0) { 38 | break; 39 | } 40 | 41 | struct tm* otimeInfo; 42 | time_t obstime = forecasts[i].observationTime; 43 | otimeInfo = localtime(&obstime); 44 | if (otimeInfo->tm_wday != wday) { 45 | continue; 46 | } 47 | 48 | if (first || forecasts[i].tempMax > h.tempMax) { 49 | first = false; 50 | h = forecasts[i]; 51 | } 52 | } 53 | 54 | return h; 55 | } 56 | -------------------------------------------------------------------------------- /mqtt.cpp: -------------------------------------------------------------------------------- 1 | #include "mqtt.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "config.h" 7 | 8 | #define MQTT_CLIENT_ID "saloon-%s" 9 | 10 | static WiFiClient espClient; 11 | static PubSubClient mqttClient(espClient); 12 | 13 | void setupMQTT() 14 | { 15 | mqttClient.setServer(MQTT_SERVER, 1883); 16 | } 17 | 18 | void mqttReconnect() 19 | { 20 | if (!mqttClient.connected()) { 21 | Serial.print("Attempting MQTT connection..."); 22 | 23 | char topicf[256]; 24 | sprintf(topicf, MQTT_TOPIC_STATE, STATION_NAME); 25 | char clientID[256]; 26 | sprintf(clientID, MQTT_CLIENT_ID, STATION_NAME); 27 | 28 | // Attempt to connect 29 | if (mqttClient.connect(clientID, MQTT_USER, MQTT_PASSWORD, topicf, 1, true, "disconnected", false)) { 30 | Serial.println("connected"); 31 | 32 | // Once connected, publish an announcement... 33 | mqttClient.publish(topicf, "connected", true); 34 | } else { 35 | Serial.print("failed, rc="); 36 | Serial.print(mqttClient.state()); 37 | Serial.println(" try again in 5 seconds"); 38 | delay(1000); 39 | } 40 | } 41 | } 42 | 43 | void mqttPublish(char *topic, float payload) 44 | { 45 | char topicf[256]; 46 | sprintf(topicf, topic, STATION_NAME); 47 | 48 | Serial.print(topicf); 49 | Serial.print(": "); 50 | Serial.println(payload); 51 | 52 | mqttClient.publish(topicf, String(payload).c_str(), true); 53 | } 54 | 55 | void mqttPublishState(bool state) 56 | { 57 | char topicf[256]; 58 | sprintf(topicf, MQTT_TOPIC_MOTION, STATION_NAME); 59 | 60 | mqttClient.publish(topicf, "motion", false); 61 | } 62 | 63 | void mqttLoop() 64 | { 65 | if (!mqttClient.connected()) { 66 | mqttReconnect(); 67 | } 68 | 69 | mqttClient.loop(); 70 | } 71 | -------------------------------------------------------------------------------- /display.cpp: -------------------------------------------------------------------------------- 1 | #include "display.h" 2 | 3 | #include "config.h" 4 | 5 | #define U8X8_USE_PINS 6 | #include 7 | 8 | static DISPLAY_DEFINITION; 9 | 10 | void setupDisplay() 11 | { 12 | Serial.println("Setting up display..."); 13 | u8g2.begin(); 14 | u8g2.enableUTF8Print(); 15 | u8g2.setFont(u8g2_font_ncenB08_tr); 16 | } 17 | 18 | void clearDisplay() 19 | { 20 | u8g2.clearBuffer(); 21 | } 22 | 23 | void updateDisplay() 24 | { 25 | u8g2.sendBuffer(); 26 | } 27 | 28 | unsigned int displayWidth() 29 | { 30 | return u8g2.getDisplayWidth(); 31 | } 32 | 33 | unsigned int displayHeight() 34 | { 35 | return u8g2.getDisplayHeight(); 36 | } 37 | 38 | void paintString(uint x, uint y, String s) 39 | { 40 | const unsigned int l = s.length() + 1; 41 | char b[l]; 42 | s.toCharArray(b, l); 43 | u8g2.drawUTF8(x, y, b); 44 | } 45 | 46 | void paintCenterString(uint x, uint y, String s) 47 | { 48 | const unsigned int l = s.length() + 1; 49 | char b[l]; 50 | s.toCharArray(b, l); 51 | 52 | const int sw = u8g2.getUTF8Width(b); 53 | const int dw = displayWidth(); 54 | 55 | if (sw <= dw) { 56 | u8g2.drawUTF8(x - sw / 2, y, b); 57 | } else { 58 | for (int i = 0; i > dw - sw; i--) { 59 | u8g2.drawUTF8(i, y, b); 60 | updateDisplay(); 61 | delay(1000 / 100); 62 | } 63 | for (int i = dw - sw; i < 0; i++) { 64 | u8g2.drawUTF8(i, y, b); 65 | updateDisplay(); 66 | delay(1000 / 100); 67 | } 68 | } 69 | } 70 | 71 | void paintWeatherIconAndTemp(uint x, uint y, float temperature, int weatherId, tm* timeInfo) 72 | { 73 | u8g2.setFont(u8g2_font_open_iconic_embedded_4x_t); 74 | unsigned int glyph = 68; 75 | if (weatherId >= 200) { 76 | glyph = 67; 77 | } 78 | if (weatherId >= 300) { 79 | u8g2.setFont(u8g2_font_open_iconic_weather_4x_t); 80 | glyph = 67; 81 | } 82 | if (weatherId >= 800) { 83 | if (timeInfo->tm_hour <= 6 || timeInfo->tm_hour >= 21) { 84 | glyph = 66; 85 | } else { 86 | glyph = 69; 87 | 88 | if (weatherId >= 801) { 89 | glyph = 65; 90 | } 91 | if (weatherId >= 803) { 92 | glyph = 64; 93 | } 94 | } 95 | } 96 | u8g2.drawGlyph(x, y + 38, glyph); 97 | 98 | u8g2.setFont(u8g2_font_logisoso24_tf); 99 | paintString(x + 40, y + 36, String(temperature, 1) + "°"); 100 | } 101 | 102 | void paintWeatherDesc(uint x, uint y, String desc) 103 | { 104 | u8g2.setFont(u8g2_font_8x13_mf); 105 | paintCenterString(u8g2.getDisplayWidth() / 2, u8g2.getDisplayHeight() - 6, desc); 106 | } 107 | 108 | void paintTime(uint x, uint y, tm* timeInfo) 109 | { 110 | char buf[64]; 111 | sprintf_P(buf, PSTR("%02d:%02d:%02d"), timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); 112 | 113 | u8g2.setFont(u8g2_font_8x13_mf); 114 | u8g2.drawStr(u8g2.getDisplayWidth() / 2 - u8g2.getStrWidth(buf) / 2, u8g2.getDisplayHeight() - 6, buf); 115 | } 116 | -------------------------------------------------------------------------------- /api.cpp: -------------------------------------------------------------------------------- 1 | #include "api.h" 2 | 3 | #include 4 | 5 | #include "config.h" 6 | #include "notifications.h" 7 | 8 | static ESP8266WebServer server(80); 9 | 10 | void handleClient() 11 | { 12 | server.handleClient(); 13 | } 14 | 15 | void handleRoot() 16 | { 17 | char msg[256]; 18 | sprintf(msg, "Hello from saloon \"%s\"!", STATION_NAME); 19 | server.send(200, "text/plain", msg); 20 | } 21 | 22 | void handleNotification() 23 | { 24 | StaticJsonDocument<256> doc; 25 | deserializeJson(doc, server.arg("plain")); 26 | String value = doc["message"]; 27 | 28 | notify(value, 0, 255, 0); 29 | Serial.printf("Notification received: %s\n", value.c_str()); 30 | server.send(200, "text/json", "{success: true}"); 31 | } 32 | 33 | void handleUpdate() 34 | { 35 | if (String(OTA_PASSWORD).length() > 0 && 36 | (!server.hasArg("password") || server.arg("password") != OTA_PASSWORD)) { 37 | return; 38 | } 39 | 40 | HTTPUpload& upload = server.upload(); 41 | if (upload.status == UPLOAD_FILE_START) { 42 | Serial.setDebugOutput(true); 43 | Serial.printf("Receiving OTA Update: %s\n", upload.filename.c_str()); 44 | uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; 45 | if (!Update.begin(maxSketchSpace)) { 46 | // start with max available size 47 | Update.printError(Serial); 48 | } 49 | } else if (upload.status == UPLOAD_FILE_WRITE) { 50 | if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { 51 | Update.printError(Serial); 52 | } 53 | } else if (upload.status == UPLOAD_FILE_END) { 54 | if (Update.end(true)) { 55 | // true to set the size to the current progress 56 | Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); 57 | } else { 58 | Update.printError(Serial); 59 | } 60 | Serial.setDebugOutput(false); 61 | } 62 | } 63 | 64 | void handleNotFound() 65 | { 66 | String message = "File Not Found\n\n"; 67 | message += "URI: "; 68 | message += server.uri(); 69 | message += "\nMethod: "; 70 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 71 | message += "\nArguments: "; 72 | message += server.args(); 73 | message += "\n"; 74 | for (uint8_t i = 0; i < server.args(); i++) { 75 | message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; 76 | } 77 | server.send(404, "text/plain", message); 78 | } 79 | 80 | void setupAPI() 81 | { 82 | server.onNotFound(handleNotFound); 83 | server.on("/", handleRoot); 84 | server.on("/notification", HTTP_POST, handleNotification); 85 | 86 | if (OTA_UPDATE_ENABLED) { 87 | server.on("/update", HTTP_POST, []() { 88 | if (String(OTA_PASSWORD).length() > 0 && 89 | (!server.hasArg("password") || server.arg("password") != OTA_PASSWORD)) { 90 | server.send(401, "text/plain", "Password required"); 91 | return; 92 | } 93 | server.sendHeader("Connection", "close"); 94 | server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); 95 | 96 | yield(); 97 | delay(1000); 98 | ESP.restart(); 99 | }, handleUpdate); 100 | } 101 | 102 | server.begin(); 103 | Serial.println("HTTP server started"); 104 | } 105 | -------------------------------------------------------------------------------- /src/weather/OpenWeatherMapCurrent.h: -------------------------------------------------------------------------------- 1 | /**The MIT License (MIT) 2 | 3 | Copyright (c) 2018 by ThingPulse Ltd., https://thingpulse.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 24 | #pragma once 25 | #include 26 | #include 27 | 28 | typedef struct OpenWeatherMapCurrentData { 29 | // "lon": 8.54, 30 | float lon; 31 | // "lat": 47.37 32 | float lat; 33 | // "id": 521, 34 | uint16_t weatherId; 35 | // "main": "Rain", 36 | String main; 37 | // "description": "shower rain", 38 | String description; 39 | // "icon": "09d" 40 | String icon; 41 | // "temp": 290.56, 42 | float temp; 43 | // "pressure": 1013, 44 | uint16_t pressure; 45 | // "humidity": 87, 46 | uint8_t humidity; 47 | // "temp_min": 289.15, 48 | float tempMin; 49 | // "temp_max": 292.15 50 | float tempMax; 51 | // visibility: 10000, 52 | uint16_t visibility; 53 | // "wind": {"speed": 1.5}, 54 | float windSpeed; 55 | // "wind": {deg: 226.505}, 56 | float windDeg; 57 | // "clouds": {"all": 90}, 58 | uint8_t clouds; 59 | // "dt": 1527015000, 60 | uint32_t observationTime; 61 | // "country": "CH", 62 | String country; 63 | // "sunrise": 1526960448, 64 | uint32_t sunrise; 65 | // "sunset": 1527015901 66 | uint32_t sunset; 67 | // "name": "Zurich", 68 | String cityName; 69 | } OpenWeatherMapCurrentData; 70 | 71 | class OpenWeatherMapCurrent : public JsonListener { 72 | private: 73 | String currentKey; 74 | String currentParent; 75 | OpenWeatherMapCurrentData* data; 76 | uint8_t weatherItemCounter = 0; 77 | boolean metric = true; 78 | String language; 79 | 80 | void doUpdate(OpenWeatherMapCurrentData* data, String url); 81 | String buildUrl(String appId, String locationParameter); 82 | 83 | public: 84 | OpenWeatherMapCurrent(); 85 | void updateCurrent(OpenWeatherMapCurrentData* data, String appId, String location); 86 | void updateCurrentById(OpenWeatherMapCurrentData* data, String appId, String locationId); 87 | 88 | void setMetric(boolean metric) { this->metric = metric; } 89 | boolean isMetric() { return metric; } 90 | void setLanguage(String language) { this->language = language; } 91 | String getLanguage() { return language; } 92 | 93 | virtual void whitespace(char c); 94 | 95 | virtual void startDocument(); 96 | 97 | virtual void key(String key); 98 | 99 | virtual void value(String value); 100 | 101 | virtual void endArray(); 102 | 103 | virtual void endObject(); 104 | 105 | virtual void endDocument(); 106 | 107 | virtual void startArray(); 108 | 109 | virtual void startObject(); 110 | }; 111 | -------------------------------------------------------------------------------- /src/weather/OpenWeatherMapForecast.h: -------------------------------------------------------------------------------- 1 | /**The MIT License (MIT) 2 | 3 | Copyright (c) 2018 by ThingPulse Ltd., https://thingpulse.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 24 | #pragma once 25 | #include 26 | #include 27 | #include 28 | 29 | typedef struct OpenWeatherMapForecastData { 30 | // {"dt":1527066000, 31 | uint32_t observationTime; 32 | // "main":{ 33 | // "temp":17.35, 34 | float temp; 35 | // "temp_min":16.89, 36 | float tempMin; 37 | // "temp_max":17.35, 38 | float tempMax; 39 | // "pressure":970.8, 40 | float pressure; 41 | // "sea_level":1030.62, 42 | float pressureSeaLevel; 43 | // "grnd_level":970.8, 44 | float pressureGroundLevel; 45 | // "humidity":97, 46 | uint8_t humidity; 47 | // "temp_kf":0.46 48 | // },"weather":[{ 49 | // "id":802, 50 | uint16_t weatherId; 51 | // "main":"Clouds", 52 | String main; 53 | // "description":"scattered clouds", 54 | String description; 55 | // "icon":"03d" 56 | String icon; 57 | // }],"clouds":{"all":44}, 58 | uint8_t clouds; 59 | // "wind":{ 60 | // "speed":1.77, 61 | float windSpeed; 62 | // "deg":207.501 63 | float windDeg; 64 | // rain: {3h: 0.055}, 65 | float rain; 66 | // },"sys":{"pod":"d"} 67 | // dt_txt: "2018-05-23 09:00:00" 68 | String observationTimeText; 69 | 70 | } OpenWeatherMapForecastData; 71 | 72 | class OpenWeatherMapForecast : public JsonListener { 73 | private: 74 | String currentKey; 75 | String currentParent; 76 | OpenWeatherMapForecastData* data; 77 | uint8_t weatherItemCounter = 0; 78 | uint8_t maxForecasts; 79 | uint8_t currentForecast; 80 | boolean metric = true; 81 | String language = "en"; 82 | uint8_t* allowedHours; 83 | uint8_t allowedHoursCount = 0; 84 | boolean isCurrentForecastAllowed = true; 85 | 86 | uint8_t doUpdate(OpenWeatherMapForecastData* data, String url); 87 | String buildUrl(String appId, String locationParameter); 88 | 89 | public: 90 | OpenWeatherMapForecast(); 91 | uint8_t updateForecasts(OpenWeatherMapForecastData* data, String appId, String location, uint8_t maxForecasts); 92 | uint8_t updateForecastsById(OpenWeatherMapForecastData* data, String appId, String locationId, uint8_t maxForecasts); 93 | 94 | void setMetric(boolean metric) { this->metric = metric; } 95 | boolean isMetric() { return this->metric; } 96 | void setLanguage(String language) { this->language = language; } 97 | String getLanguage() { return this->language; } 98 | void setAllowedHours(uint8_t* allowedHours, uint8_t allowedHoursCount) 99 | { 100 | this->allowedHours = allowedHours; 101 | this->allowedHoursCount = allowedHoursCount; 102 | } 103 | 104 | virtual void whitespace(char c); 105 | 106 | virtual void startDocument(); 107 | 108 | virtual void key(String key); 109 | 110 | virtual void value(String value); 111 | 112 | virtual void endArray(); 113 | 114 | virtual void endObject(); 115 | 116 | virtual void endDocument(); 117 | 118 | virtual void startArray(); 119 | 120 | virtual void startObject(); 121 | }; 122 | -------------------------------------------------------------------------------- /config.example.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // General Settings 4 | 5 | /** 6 | * The station's name 7 | */ 8 | #define STATION_NAME "livingroom" 9 | 10 | /** 11 | * Your local time-zone offset from UTC+ in hours 12 | * Look up your definition at: 13 | * https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv 14 | */ 15 | #define TIME_TZ "CET-1CEST,M3.5.0,M10.5.0/3" 16 | 17 | /** 18 | * Use metric units 19 | */ 20 | #define IS_METRIC true 21 | 22 | // Intervals 23 | #define UPDATE_WEATHER_INTERVAL_SECS 15 * 60 // Update every 15 minutes 24 | #define UPDATE_FORECAST_INTERVAL_SECS 60 * 60 // Update every hour 25 | #define UPDATE_SENSOR_INTERVAL_SECS 60 // Update every minute 26 | #define PUBLISH_MQTT_INTERVAL_SECS 60 // Update every minute 27 | 28 | // Wifi Configuration 29 | #define WIFI_SSID "ssid" 30 | #define WIFI_PSK "password" 31 | 32 | // OTA Update Settings 33 | 34 | /** 35 | * Enables remote firmware updates via HTTP 36 | */ 37 | #define OTA_UPDATE_ENABLED true 38 | #define OTA_PASSWORD "admin" 39 | 40 | // MQTT Configuration 41 | #define MQTT_SERVER "192.168.0.1" 42 | #define MQTT_USER "saloon" 43 | #define MQTT_PASSWORD "saloon" 44 | 45 | // MQTT topic definitions. "%s" will get replaced by the STATION_NAME 46 | #define MQTT_TOPIC_OUTSIDE_TEMPERATURE "home/outside/temperature" 47 | #define MQTT_TOPIC_OUTSIDE_HUMIDITY "home/outside/humidity" 48 | #define MQTT_TOPIC_HUMIDITY "home/%s/humidity" 49 | #define MQTT_TOPIC_TEMPERATURE "home/%s/temperature" 50 | #define MQTT_TOPIC_STATE "home/%s/status" 51 | #define MQTT_TOPIC_MOTION "home/%s/motion" 52 | #define MQTT_TOPIC_LIGHTINTENSITY "home/%s/lightintensity" 53 | #define MQTT_TOPIC_NOISELEVEL "home/%s/noiselevel" 54 | 55 | /** 56 | * Publishes the retrieved OpenWeather data to MQTT when true 57 | */ 58 | #define MQTT_PUBLISH_WEATHER true 59 | 60 | // Display Configuration 61 | #define DISPLAY_DEFINITION U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE) 62 | // #define DISPLAY_DEFINITION U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE) 63 | // #define DISPLAY_DEFINITION U8G2_SH1106_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, D1, D2, U8X8_PIN_NONE) 64 | 65 | // Pin Configuration 66 | 67 | /** 68 | * Pin the PIR motion sensor is connected to 69 | * 70 | * Set to -1 to disable the sensor 71 | */ 72 | #define MOTION_PIR_SENSOR D0 73 | 74 | /** 75 | * Pin the BME280 air sensor CLK & SLA signals are connected to 76 | * 77 | * Set to -1 to disable the sensor 78 | */ 79 | #define BME280_SENSOR_CLK D1 80 | #define BME280_SENSOR_SDA D2 81 | 82 | /** 83 | * Pin the DHT air sensor is connected to 84 | * 85 | * Set to -1 to disable the sensor 86 | */ 87 | #define DHT_SENSOR -1 // D8 88 | 89 | /** 90 | * Which type of DHT sensor to use 91 | * 92 | * 0 for auto-detection 93 | * 11 for DHT-11 94 | * 22 for DHT-22 95 | */ 96 | #define DHT_SENSOR_TYPE 0 97 | 98 | /** 99 | * Pin the BH1750 light sensor CLK is connected to 100 | * 101 | * Set to -1 to disable the sensor 102 | */ 103 | #define BH1750_SENSOR_CLK D1 // D1 104 | 105 | /** 106 | * Pin the microphone is connected to 107 | * 108 | * Set to -1 to disable the sensor 109 | */ 110 | #define MICROPHONE A0 // A0 111 | 112 | /** 113 | * Pin of the status LED 114 | * 115 | * Set to -1 to disable the status LED 116 | */ 117 | #define STATUS_LED LED_BUILTIN 118 | 119 | /** 120 | * Notification Settings 121 | */ 122 | #define NOTIFICATION_CONNECT true 123 | #define NOTIFICATION_HOUR true 124 | #define NOTIFICATION_MINUTE false 125 | #define NOTIFICATION_MOTION false 126 | 127 | /** 128 | * Pins of the notification LEDs 129 | * 130 | * Set to -1 to disable the notification LEDs 131 | */ 132 | #define LED_BLUE D5 133 | #define LED_GREEN D6 134 | #define LED_RED D8 135 | 136 | /** 137 | * Neopixel notification LED configuration 138 | */ 139 | #define NEOPIXEL -1 // D7 140 | #define NEOPIXEL_PIXEL_COUNT 12 141 | #define NEOPIXEL_BRIGHTNESS 5 // in percent 142 | 143 | // OpenWeatherMap Settings 144 | 145 | /** 146 | * Your OpenWeatherMap API key 147 | * 148 | * Sign up here to get an API key: https://openweathermap.org/api 149 | */ 150 | #define OPEN_WEATHER_MAP_APP_ID "your_api_key" 151 | 152 | /** 153 | * Location ID for weather forecasts 154 | * 155 | * Go to https://openweathermap.org/find?q= and search for a location. Go through the 156 | * result set and select the entry closest to the actual location you want to display 157 | * data for. It'll be a URL like https://openweathermap.org/city/2954172. The number 158 | * at the end is what you assign to the constant below. 159 | */ 160 | #define OPEN_WEATHER_MAP_LOCATION_ID "1234" 161 | 162 | /** 163 | * Language for weather forecasts 164 | * 165 | * Pick a language code from this list: 166 | * 167 | * Arabic - ar, Bulgarian - bg, Catalan - ca, Czech - cz, German - de, Greek - el, 168 | * English - en, Persian (Farsi) - fa, Finnish - fi, French - fr, Galician - gl, 169 | * Croatian - hr, Hungarian - hu, Italian - it, Japanese - ja, Korean - kr, 170 | * Latvian - la, Lithuanian - lt, Macedonian - mk, Dutch - nl, Polish - pl, 171 | * Portuguese - pt, Romanian - ro, Russian - ru, Swedish - se, Slovak - sk, 172 | * Slovenian - sl, Spanish - es, Turkish - tr, Ukrainian - ua, Vietnamese - vi, 173 | * Chinese Simplified - zh_cn, Chinese Traditional - zh_tw. 174 | */ 175 | #define OPEN_WEATHER_MAP_LANGUAGE "en" 176 | -------------------------------------------------------------------------------- /saloon.ino: -------------------------------------------------------------------------------- 1 | #include "api.h" 2 | #include "config.h" 3 | #include "display.h" 4 | #include "mqtt.h" 5 | #include "neopixel.h" 6 | #include "notifications.h" 7 | #include "wifi.h" 8 | 9 | #include "src/sensors/bh1750sensor.h" 10 | #include "src/sensors/bme280sensor.h" 11 | #include "src/sensors/dhtsensor.h" 12 | #include "src/sensors/microphone.h" 13 | #include "src/sensors/motionsensor.h" 14 | 15 | #include "src/weather/openweather.h" 16 | 17 | const String SALOON_VER = "0.0.1"; 18 | 19 | long lastWeatherUpdate = 0; 20 | long lastForecastUpdate = 0; 21 | long lastMQTTPublish = 0; 22 | long lastSensorUpdate = 0; 23 | long lastUpdate = 0; 24 | 25 | static float temperature = 0; 26 | static float humidity = 0; 27 | static double noise = 0; 28 | static uint16_t lux = 0; 29 | 30 | void setup() 31 | { 32 | Serial.begin(115200); 33 | delay(1000); 34 | Serial.printf("\Saloon v%s\n", SALOON_VER.c_str()); 35 | 36 | setupPins(); 37 | 38 | if (NEOPIXEL >= 0) { 39 | setupNeoPixel(((NEOPIXEL_BRIGHTNESS) / 100.0) * 255); 40 | } 41 | 42 | setupDisplay(); 43 | paintCenterString(displayWidth() / 2, displayHeight() / 2, "Connecting..."); 44 | updateDisplay(); 45 | 46 | setupWifi(); 47 | setupNTP(); 48 | setupMDNS(); 49 | setupAPI(); 50 | setupMQTT(); 51 | 52 | if (DHT_SENSOR >= 0) { 53 | setupDHT(DHT_SENSOR, DHT_SENSOR_TYPE); 54 | } 55 | if (BME280_SENSOR_CLK >= 0) { 56 | setupBME280(BME280_SENSOR_SDA, BME280_SENSOR_CLK); 57 | } 58 | if (BH1750_SENSOR_CLK >= 0) { 59 | setupBH1750(); 60 | } 61 | 62 | clearDisplay(); 63 | paintCenterString(displayWidth() / 2, displayHeight() / 2, "Gathering data..."); 64 | updateDisplay(); 65 | } 66 | 67 | void setupPins() 68 | { 69 | pinMode(MOTION_PIR_SENSOR, INPUT); 70 | pinMode(STATUS_LED, OUTPUT); 71 | pinMode(LED_RED, OUTPUT); 72 | pinMode(LED_BLUE, OUTPUT); 73 | pinMode(LED_GREEN, OUTPUT); 74 | 75 | digitalWrite(STATUS_LED, HIGH); 76 | digitalWrite(LED_RED, LOW); 77 | digitalWrite(LED_BLUE, LOW); 78 | digitalWrite(LED_GREEN, LOW); 79 | } 80 | 81 | void loop() 82 | { 83 | time_t now = time(nullptr); 84 | struct tm timeInfo; 85 | timeInfo = *(localtime(&now)); 86 | 87 | cron(timeInfo); 88 | handleClient(); 89 | updateMDNS(); 90 | mqttLoop(); 91 | 92 | /* Motion Sensor */ 93 | bool motionChange = handleMotionSensor(MOTION_PIR_SENSOR); 94 | if (motionChange) { 95 | mqttPublishState(true); 96 | 97 | if (NOTIFICATION_MOTION) { 98 | notify("Motion triggered!", 255, 255, 0); 99 | } 100 | } 101 | 102 | /* Update Display */ 103 | clearDisplay(); 104 | 105 | OpenWeatherMapForecastData forecast; 106 | if (timeInfo.tm_hour < 12) { 107 | forecast = forecastForWDay(timeInfo.tm_wday); 108 | } else { 109 | forecast = forecastForWDay((timeInfo.tm_wday + 1) % 7); 110 | } 111 | 112 | int smod = timeInfo.tm_sec / 5; 113 | int step = smod % 4; 114 | if (step == 2 && forecast.weatherId <= 0) step = 0; 115 | if (step < 2 && forecast.weatherId <= 0) step = 3; 116 | 117 | switch (step) { 118 | case 0: 119 | { 120 | paintWeatherIconAndTemp(16, 0, currentWeather.temp, currentWeather.weatherId, &timeInfo); 121 | paintTime(0, 0, &timeInfo); 122 | } 123 | break; 124 | 125 | case 1: 126 | { 127 | paintWeatherIconAndTemp(16, 0, currentWeather.temp, currentWeather.weatherId, &timeInfo); 128 | paintWeatherDesc(0, 0, currentWeather.description); 129 | } 130 | break; 131 | 132 | case 2: 133 | { 134 | struct tm* ftimeInfo; 135 | time_t obstime = forecast.observationTime; 136 | ftimeInfo = localtime(&obstime); 137 | 138 | paintWeatherIconAndTemp(16, 0, forecast.tempMax, forecast.weatherId, ftimeInfo); 139 | paintWeatherDesc(0, 0, WDAY_NAMES[ftimeInfo->tm_wday] + ", " + String(ftimeInfo->tm_hour) + "h"); 140 | } 141 | break; 142 | 143 | case 3: 144 | { 145 | paintWeatherIconAndTemp(16, 0, temperature, 0, &timeInfo); 146 | paintWeatherDesc(0, 0, "Indoor"); 147 | } 148 | break; 149 | } 150 | 151 | updateDisplay(); 152 | } 153 | 154 | void cron(tm timeInfo) 155 | { 156 | if (timeInfo.tm_min != lastUpdate) { 157 | lastUpdate = timeInfo.tm_min; 158 | if (timeInfo.tm_min == 0) { 159 | if (NOTIFICATION_HOUR) { 160 | // flash once per full hour of the day, at the full hour 161 | int h = timeInfo.tm_hour; 162 | if (h == 0) { 163 | h = 12; 164 | } 165 | if (h > 12) { 166 | h -= 12; 167 | } 168 | for (int x = 0; x < h; x++) { 169 | flashLED(255, 255, 255); 170 | } 171 | } 172 | } else { 173 | // flash once per minute 174 | if (NOTIFICATION_MINUTE) { 175 | flashLED(64, 64, 64); 176 | } 177 | } 178 | } 179 | 180 | /* Update sensor values */ 181 | if (lastSensorUpdate == 0 || millis() - lastSensorUpdate > (1000L * (UPDATE_SENSOR_INTERVAL_SECS))) { 182 | updateSensors(); 183 | lastSensorUpdate = millis(); 184 | } 185 | 186 | /* Publish MQTT values */ 187 | if (lastMQTTPublish == 0 || millis() - lastMQTTPublish > (1000L * (PUBLISH_MQTT_INTERVAL_SECS))) { 188 | publishMQTTValues(); 189 | lastMQTTPublish = millis(); 190 | } 191 | 192 | /* Update Weather & Forecast */ 193 | if (lastWeatherUpdate == 0 || millis() - lastWeatherUpdate > (1000L * (UPDATE_WEATHER_INTERVAL_SECS))) { 194 | Serial.println("Getting weather..."); 195 | updateWeather(); 196 | lastWeatherUpdate = millis(); 197 | } 198 | if (lastForecastUpdate == 0 || millis() - lastForecastUpdate > (1000L * (UPDATE_FORECAST_INTERVAL_SECS))) { 199 | Serial.println("Getting forecast..."); 200 | updateWeatherForecast(); 201 | lastForecastUpdate = millis(); 202 | } 203 | } 204 | 205 | void updateSensors() 206 | { 207 | if (DHT_SENSOR >= 0) { 208 | measureDHT(&temperature, &humidity); 209 | } 210 | if (BME280_SENSOR_CLK >= 0) { 211 | measureBME280(&temperature, &humidity); 212 | } 213 | if (MICROPHONE >= 0) { 214 | noise = measureNoiseLevel(MICROPHONE); 215 | } 216 | if (BH1750_SENSOR_CLK >= 0) { 217 | lux = measureBH1750(); 218 | } 219 | } 220 | 221 | void publishMQTTValues() 222 | { 223 | /* Weather */ 224 | if (MQTT_PUBLISH_WEATHER && (currentWeather.temp != 0 || currentWeather.humidity > 0)) { 225 | mqttPublish(MQTT_TOPIC_OUTSIDE_TEMPERATURE, currentWeather.temp); 226 | mqttPublish(MQTT_TOPIC_OUTSIDE_HUMIDITY, currentWeather.humidity); 227 | } 228 | 229 | /* Temperature */ 230 | if (temperature != 0 || humidity > 0) { 231 | mqttPublish(MQTT_TOPIC_TEMPERATURE, temperature); 232 | mqttPublish(MQTT_TOPIC_HUMIDITY, humidity); 233 | } 234 | 235 | /* Microphone */ 236 | if (MICROPHONE >= 0) { 237 | mqttPublish(MQTT_TOPIC_NOISELEVEL, noise); 238 | } 239 | 240 | /* Light Intensity */ 241 | if (BH1750_SENSOR_CLK >= 0) { 242 | mqttPublish(MQTT_TOPIC_LIGHTINTENSITY, lux); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/weather/OpenWeatherMapCurrent.cpp: -------------------------------------------------------------------------------- 1 | /**The MIT License (MIT) 2 | 3 | Copyright (c) 2018 by ThingPulse Ltd., https://thingpulse.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 24 | #include "OpenWeatherMapCurrent.h" 25 | #include 26 | #include 27 | #include 28 | 29 | OpenWeatherMapCurrent::OpenWeatherMapCurrent() 30 | { 31 | } 32 | 33 | void OpenWeatherMapCurrent::updateCurrent(OpenWeatherMapCurrentData* data, String appId, String location) 34 | { 35 | doUpdate(data, buildUrl(appId, "q=" + location)); 36 | } 37 | 38 | void OpenWeatherMapCurrent::updateCurrentById(OpenWeatherMapCurrentData* data, String appId, String locationId) 39 | { 40 | doUpdate(data, buildUrl(appId, "id=" + locationId)); 41 | } 42 | 43 | String OpenWeatherMapCurrent::buildUrl(String appId, String locationParameter) 44 | { 45 | String units = metric ? "metric" : "imperial"; 46 | return "http://api.openweathermap.org/data/2.5/weather?" + locationParameter + "&appid=" + appId + "&units=" + units + "&lang=" + language; 47 | } 48 | 49 | void OpenWeatherMapCurrent::doUpdate(OpenWeatherMapCurrentData* data, String url) 50 | { 51 | unsigned long lostTest = 10000UL; 52 | unsigned long lost_do = millis(); 53 | this->weatherItemCounter = 0; 54 | this->data = data; 55 | JsonStreamingParser parser; 56 | parser.setListener(this); 57 | Serial.printf("Getting url: %s\n", url.c_str()); 58 | HTTPClient http; 59 | 60 | http.begin(url); 61 | bool isBody = false; 62 | char c; 63 | int size; 64 | // Serial.print("[HTTP] GET...\n"); 65 | // start connection and send HTTP header 66 | int httpCode = http.GET(); 67 | Serial.printf("[HTTP] GET... code: %d\n", httpCode); 68 | if (httpCode > 0) { 69 | 70 | WiFiClient* client = http.getStreamPtr(); 71 | 72 | while (client->connected() || client->available()) { 73 | while ((size = client->available()) > 0) { 74 | if ((millis() - lost_do) > lostTest) { 75 | Serial.println("lost in client with a timeout"); 76 | client->stop(); 77 | ESP.restart(); 78 | } 79 | c = client->read(); 80 | if (c == '{' || c == '[') { 81 | 82 | isBody = true; 83 | } 84 | if (isBody) { 85 | parser.parse(c); 86 | } 87 | // give WiFi and TCP/IP libraries a chance to handle pending events 88 | yield(); 89 | } 90 | } 91 | } 92 | this->data = nullptr; 93 | } 94 | 95 | void OpenWeatherMapCurrent::whitespace(char c) 96 | { 97 | // Serial.println("whitespace"); 98 | } 99 | 100 | void OpenWeatherMapCurrent::startDocument() 101 | { 102 | // Serial.println("start document"); 103 | } 104 | 105 | void OpenWeatherMapCurrent::key(String key) 106 | { 107 | currentKey = String(key); 108 | } 109 | 110 | void OpenWeatherMapCurrent::value(String value) 111 | { 112 | // "lon": 8.54, float lon; 113 | if (currentKey == "lon") { 114 | this->data->lon = value.toFloat(); 115 | } 116 | // "lat": 47.37 float lat; 117 | if (currentKey == "lat") { 118 | this->data->lat = value.toFloat(); 119 | } 120 | // weatherItemCounter: only get the first item if more than one is available 121 | if (currentParent == "weather" && weatherItemCounter == 0) { 122 | // "id": 521, weatherId weatherId; 123 | if (currentKey == "id") { 124 | this->data->weatherId = value.toInt(); 125 | } 126 | // "main": "Rain", String main; 127 | if (currentKey == "main") { 128 | this->data->main = value; 129 | } 130 | // "description": "shower rain", String description; 131 | if (currentKey == "description") { 132 | this->data->description = value; 133 | } 134 | // "icon": "09d" String icon; 135 | //String iconMeteoCon; 136 | if (currentKey == "icon") { 137 | this->data->icon = value; 138 | // this->data->iconMeteoCon = getMeteoconIcon(value); 139 | } 140 | } 141 | 142 | // "temp": 290.56, float temp; 143 | if (currentKey == "temp") { 144 | this->data->temp = value.toFloat(); 145 | } 146 | // "pressure": 1013, uint16_t pressure; 147 | if (currentKey == "pressure") { 148 | this->data->pressure = value.toInt(); 149 | } 150 | // "humidity": 87, uint8_t humidity; 151 | if (currentKey == "humidity") { 152 | this->data->humidity = value.toInt(); 153 | } 154 | // "temp_min": 289.15, float tempMin; 155 | if (currentKey == "temp_min") { 156 | this->data->tempMin = value.toFloat(); 157 | } 158 | // "temp_max": 292.15 float tempMax; 159 | if (currentKey == "temp_max") { 160 | this->data->tempMax = value.toFloat(); 161 | } 162 | // visibility: 10000, uint16_t visibility; 163 | if (currentKey == "visibility") { 164 | this->data->visibility = value.toInt(); 165 | } 166 | // "wind": {"speed": 1.5}, float windSpeed; 167 | if (currentKey == "speed") { 168 | this->data->windSpeed = value.toFloat(); 169 | } 170 | // "wind": {deg: 226.505}, float windDeg; 171 | if (currentKey == "deg") { 172 | this->data->windDeg = value.toFloat(); 173 | } 174 | // "clouds": {"all": 90}, uint8_t clouds; 175 | if (currentKey == "all") { 176 | this->data->clouds = value.toInt(); 177 | } 178 | // "dt": 1527015000, uint64_t observationTime; 179 | if (currentKey == "dt") { 180 | this->data->observationTime = value.toInt(); 181 | } 182 | // "country": "CH", String country; 183 | if (currentKey == "country") { 184 | this->data->country = value; 185 | } 186 | // "sunrise": 1526960448, uint32_t sunrise; 187 | if (currentKey == "sunrise") { 188 | this->data->sunrise = value.toInt(); 189 | } 190 | // "sunset": 1527015901 uint32_t sunset; 191 | if (currentKey == "sunset") { 192 | this->data->sunset = value.toInt(); 193 | } 194 | // "name": "Zurich", String cityName; 195 | if (currentKey == "name") { 196 | this->data->cityName = value; 197 | } 198 | } 199 | 200 | void OpenWeatherMapCurrent::endArray() 201 | { 202 | } 203 | 204 | void OpenWeatherMapCurrent::startObject() 205 | { 206 | currentParent = currentKey; 207 | } 208 | 209 | void OpenWeatherMapCurrent::endObject() 210 | { 211 | if (currentParent == "weather") { 212 | weatherItemCounter++; 213 | } 214 | currentParent = ""; 215 | } 216 | 217 | void OpenWeatherMapCurrent::endDocument() 218 | { 219 | } 220 | 221 | void OpenWeatherMapCurrent::startArray() 222 | { 223 | } 224 | -------------------------------------------------------------------------------- /src/weather/OpenWeatherMapForecast.cpp: -------------------------------------------------------------------------------- 1 | /**The MIT License (MIT) 2 | 3 | Copyright (c) 2018 by ThingPulse Ltd., https://thingpulse.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | */ 23 | 24 | #include "OpenWeatherMapForecast.h" 25 | #include 26 | #include 27 | #include 28 | 29 | OpenWeatherMapForecast::OpenWeatherMapForecast() 30 | { 31 | } 32 | 33 | uint8_t OpenWeatherMapForecast::updateForecasts(OpenWeatherMapForecastData* data, String appId, String location, uint8_t maxForecasts) 34 | { 35 | this->maxForecasts = maxForecasts; 36 | return doUpdate(data, buildUrl(appId, "q=" + location)); 37 | } 38 | 39 | uint8_t OpenWeatherMapForecast::updateForecastsById(OpenWeatherMapForecastData* data, String appId, String locationId, uint8_t maxForecasts) 40 | { 41 | this->maxForecasts = maxForecasts; 42 | return doUpdate(data, buildUrl(appId, "id=" + locationId)); 43 | } 44 | 45 | String OpenWeatherMapForecast::buildUrl(String appId, String locationParameter) 46 | { 47 | String units = metric ? "metric" : "imperial"; 48 | return "http://api.openweathermap.org/data/2.5/forecast?" + locationParameter + "&appid=" + appId + "&units=" + units + "&lang=" + language; 49 | } 50 | 51 | uint8_t OpenWeatherMapForecast::doUpdate(OpenWeatherMapForecastData* data, String url) 52 | { 53 | unsigned long lostTest = 10000UL; 54 | unsigned long lost_do = millis(); 55 | this->weatherItemCounter = 0; 56 | this->currentForecast = 0; 57 | this->data = data; 58 | JsonStreamingParser parser; 59 | parser.setListener(this); 60 | Serial.printf("Getting url: %s\n", url.c_str()); 61 | HTTPClient http; 62 | 63 | http.begin(url); 64 | bool isBody = false; 65 | char c; 66 | int size; 67 | // Serial.print("[HTTP] GET...\n"); 68 | // start connection and send HTTP header 69 | int httpCode = http.GET(); 70 | Serial.printf("[HTTP] GET... code: %d\n", httpCode); 71 | if (httpCode > 0) { 72 | 73 | WiFiClient* client = http.getStreamPtr(); 74 | 75 | while (client->connected() || client->available()) { 76 | while ((size = client->available()) > 0) { 77 | if ((millis() - lost_do) > lostTest) { 78 | Serial.println("lost in client with a timeout"); 79 | client->stop(); 80 | ESP.restart(); 81 | } 82 | c = client->read(); 83 | if (c == '{' || c == '[') { 84 | 85 | isBody = true; 86 | } 87 | if (isBody) { 88 | parser.parse(c); 89 | } 90 | // give WiFi and TCP/IP libraries a chance to handle pending events 91 | yield(); 92 | } 93 | } 94 | } 95 | this->data = nullptr; 96 | return currentForecast; 97 | } 98 | 99 | void OpenWeatherMapForecast::whitespace(char c) 100 | { 101 | // Serial.println("whitespace"); 102 | } 103 | 104 | void OpenWeatherMapForecast::startDocument() 105 | { 106 | // Serial.println("start document"); 107 | } 108 | 109 | void OpenWeatherMapForecast::key(String key) 110 | { 111 | currentKey = String(key); 112 | } 113 | 114 | void OpenWeatherMapForecast::value(String value) 115 | { 116 | if (currentForecast >= maxForecasts) { 117 | return; 118 | } 119 | // {"dt":1527066000, uint32_t observationTime; 120 | if (currentKey == "dt") { 121 | data[currentForecast].observationTime = value.toInt(); 122 | 123 | if (allowedHoursCount > 0) { 124 | time_t time = data[currentForecast].observationTime; 125 | struct tm* timeInfo; 126 | timeInfo = gmtime(&time); 127 | uint8_t currentHour = timeInfo->tm_hour; 128 | for (uint8_t i = 0; i < allowedHoursCount; i++) { 129 | if (currentHour == allowedHours[i]) { 130 | isCurrentForecastAllowed = true; 131 | return; 132 | } 133 | } 134 | isCurrentForecastAllowed = false; 135 | return; 136 | } 137 | } 138 | if (!isCurrentForecastAllowed) { 139 | return; 140 | } 141 | // "main":{ 142 | // "temp":17.35, float temp; 143 | if (currentKey == "temp") { 144 | data[currentForecast].temp = value.toFloat(); 145 | // initialize potentially empty values: 146 | data[currentForecast].rain = 0; 147 | ; 148 | } 149 | // "temp_min":16.89, float tempMin; 150 | if (currentKey == "temp_min") { 151 | data[currentForecast].tempMin = value.toFloat(); 152 | } 153 | // "temp_max":17.35,float tempMax; 154 | if (currentKey == "temp_max") { 155 | data[currentForecast].tempMax = value.toFloat(); 156 | } 157 | // "pressure":970.8,float pressure; 158 | if (currentKey == "pressure") { 159 | data[currentForecast].pressure = value.toFloat(); 160 | } 161 | // "sea_level":1030.62,float pressureSeaLevel; 162 | if (currentKey == "sea_level") { 163 | data[currentForecast].pressureSeaLevel = value.toFloat(); 164 | } 165 | // "grnd_level":970.8,float pressureGroundLevel; 166 | if (currentKey == "grnd_level") { 167 | data[currentForecast].pressureGroundLevel = value.toFloat(); 168 | } 169 | // "":97,uint8_t humidity; 170 | if (currentKey == "humidity") { 171 | data[currentForecast].humidity = value.toInt(); 172 | } 173 | // "temp_kf":0.46 174 | // },"weather":[{ 175 | 176 | if (currentParent == "weather") { 177 | // "id":802,uint16_t weatherId; 178 | if (currentKey == "id") { 179 | data[currentForecast].weatherId = value.toInt(); 180 | } 181 | 182 | // "main":"Clouds",String main; 183 | if (currentKey == "main") { 184 | data[currentForecast].main = value; 185 | } 186 | // "description":"scattered clouds",String description; 187 | if (currentKey == "description") { 188 | data[currentForecast].description = value; 189 | } 190 | // "icon":"03d" String icon; String iconMeteoCon; 191 | if (currentKey == "icon") { 192 | data[currentForecast].icon = value; 193 | // data[currentForecast].iconMeteoCon = getMeteoconIcon(value); 194 | } 195 | } 196 | // }],"clouds":{"all":44},uint8_t clouds; 197 | if (currentKey == "all") { 198 | data[currentForecast].clouds = value.toInt(); 199 | } 200 | // "wind":{ 201 | // "speed":1.77, float windSpeed; 202 | if (currentKey == "speed") { 203 | data[currentForecast].windSpeed = value.toFloat(); 204 | } 205 | // "deg":207.501 float windDeg; 206 | if (currentKey == "deg") { 207 | data[currentForecast].windDeg = value.toFloat(); 208 | } 209 | // rain: {3h: 0.055}, float rain; 210 | if (currentKey == "3h") { 211 | data[currentForecast].rain = value.toFloat(); 212 | } 213 | // },"sys":{"pod":"d"} 214 | // dt_txt: "2018-05-23 09:00:00" String observationTimeText; 215 | if (currentKey == "dt_txt") { 216 | data[currentForecast].observationTimeText = value; 217 | // this is not super save, if there is no dt_txt item we'll never get all forecasts; 218 | currentForecast++; 219 | } 220 | } 221 | 222 | void OpenWeatherMapForecast::endArray() 223 | { 224 | } 225 | 226 | void OpenWeatherMapForecast::startObject() 227 | { 228 | currentParent = currentKey; 229 | } 230 | 231 | void OpenWeatherMapForecast::endObject() 232 | { 233 | if (currentParent == "weather") { 234 | weatherItemCounter++; 235 | } 236 | currentParent = ""; 237 | } 238 | 239 | void OpenWeatherMapForecast::endDocument() 240 | { 241 | } 242 | 243 | void OpenWeatherMapForecast::startArray() 244 | { 245 | } 246 | --------------------------------------------------------------------------------