From bda96dc595aecb56739cc02de7e7d2d825927b7f Mon Sep 17 00:00:00 2001
From: Avamander <avamander@gmail.com>
Date: Thu, 10 Jun 2021 00:44:49 +0300
Subject: [PATCH] Initial Weather service skeleton

---
 .gitmodules                                   |   3 +
 src/components/ble/NimbleController.cpp       |   2 +
 src/components/ble/NimbleController.h         |   2 +
 src/components/ble/weather/WeatherData.h      | 338 ++++++++++++++++++
 src/components/ble/weather/WeatherService.cpp | 208 +++++++++++
 src/components/ble/weather/WeatherService.h   | 139 +++++++
 src/displayapp/screens/Weather.cpp            | 246 +++++++++++++
 src/displayapp/screens/Weather.h              |  54 +++
 8 files changed, 992 insertions(+)
 create mode 100644 src/components/ble/weather/WeatherData.h
 create mode 100644 src/components/ble/weather/WeatherService.cpp
 create mode 100644 src/components/ble/weather/WeatherService.h
 create mode 100644 src/displayapp/screens/Weather.cpp
 create mode 100644 src/displayapp/screens/Weather.h

diff --git a/.gitmodules b/.gitmodules
index 815fc022..8d302ae7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "src/libs/littlefs"]
 	path = src/libs/littlefs
 	url = https://github.com/littlefs-project/littlefs.git
+[submodule "src/libs/QCBOR"]
+	path = src/libs/QCBOR
+	url = https://github.com/laurencelundblade/QCBOR.git
diff --git a/src/components/ble/NimbleController.cpp b/src/components/ble/NimbleController.cpp
index 43a8b0d6..9ef2d057 100644
--- a/src/components/ble/NimbleController.cpp
+++ b/src/components/ble/NimbleController.cpp
@@ -36,6 +36,7 @@ NimbleController::NimbleController(Pinetime::System::SystemTask& systemTask,
     alertNotificationClient {systemTask, notificationManager},
     currentTimeService {dateTimeController},
     musicService {systemTask},
+    weatherService {systemTask, dateTimeController},
     navService {systemTask},
     batteryInformationService {batteryController},
     immediateAlertService {systemTask, notificationManager},
@@ -77,6 +78,7 @@ void NimbleController::Init() {
   currentTimeClient.Init();
   currentTimeService.Init();
   musicService.Init();
+  weatherService.Init();
   navService.Init();
   anService.Init();
   dfuService.Init();
diff --git a/src/components/ble/NimbleController.h b/src/components/ble/NimbleController.h
index 895b87f2..a21cbe81 100644
--- a/src/components/ble/NimbleController.h
+++ b/src/components/ble/NimbleController.h
@@ -20,6 +20,7 @@
 #include "components/ble/ServiceDiscovery.h"
 #include "components/ble/HeartRateService.h"
 #include "components/ble/MotionService.h"
+#include "components/ble/weather/WeatherService.h"
 
 namespace Pinetime {
   namespace Drivers {
@@ -93,6 +94,7 @@ namespace Pinetime {
       AlertNotificationClient alertNotificationClient;
       CurrentTimeService currentTimeService;
       MusicService musicService;
+      WeatherService weatherService;
       NavigationService navService;
       BatteryInformationService batteryInformationService;
       ImmediateAlertService immediateAlertService;
diff --git a/src/components/ble/weather/WeatherData.h b/src/components/ble/weather/WeatherData.h
new file mode 100644
index 00000000..c1d53f4e
--- /dev/null
+++ b/src/components/ble/weather/WeatherData.h
@@ -0,0 +1,338 @@
+/*  Copyright (C) 2021 Avamander
+
+    This file is part of InfiniTime.
+
+    InfiniTime 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.
+
+    InfiniTime 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/>.
+*/
+#pragma once
+
+/**
+ * Different weather events, weather data structures used by {@link WeatherService.h}
+ *
+ *
+ * Implemented based on and other material:
+ * https://en.wikipedia.org/wiki/METAR
+ * https://www.weather.gov/jetstream/obscurationtypes
+ * http://www.faraim.org/aim/aim-4-03-14-493.html
+ */
+
+namespace Pinetime {
+  namespace Controllers {
+    class WeatherData {
+    public:
+      /**
+       * Visibility obscuration types
+       */
+      enum class obscurationtype {
+        /** No obscuration */
+        None = 0,
+        /** Water particles suspended in the air; low visibility; does not fall */
+        Fog = 1,
+        /** Extremely small, dry particles in the air; invisible to the eye; opalescent */
+        Haze = 2,
+        /** Small fire-created particles suspended in the air */
+        Smoke = 3,
+        /** Fine rock powder, from for example volcanoes */
+        Ash = 4,
+        /** Fine particles of earth suspended in the air by the wind */
+        Dust = 5,
+        /** Fine particles of sand suspended in the air by the wind */
+        Sand = 6,
+        /** Water particles suspended in the air; low-ish visibility; temperature is near dewpoint */
+        Mist = 7,
+      };
+
+      /**
+       * Types of precipitation
+       */
+      enum class precipitationtype {
+        /**
+         * No precipitation
+         *
+         * Theoretically we could just _not_ send the event, but then
+         * how do we differentiate between no precipitation and
+         * no information about precipitation
+         */
+        None = 0,
+        /** Drops larger than a drizzle; also widely separated drizzle */
+        Rain = 1,
+        /** Fairly uniform rain consisting of fine drops */
+        Drizzle = 2,
+        /** Rain that freezes upon contact with objects and ground */
+        FreezingRain = 3,
+        /** Rain + hail; ice pellets; small translucent frozen raindrops */
+        Sleet = 4,
+        /** Larger ice pellets; falling separately or in irregular clumps */
+        Hail = 5,
+        /** Hail with smaller grains of ice; mini-snowballs */
+        SmallHail = 6,
+        /** Snow... */
+        Snow = 7,
+        /** Frozen drizzle; very small snow crystals */
+        SnowGrains = 8,
+        /** Needles; columns or plates of ice. Sometimes described as "diamond dust". In very cold regions */
+        IceCrystals = 9
+      };
+
+      /**
+       * These are special events that can "enhance" the "experience" of existing weather events
+       */
+      enum class specialtype {
+        /** Strong wind with a sudden onset that lasts at least a minute */
+        Squall = 0,
+        /** Series of waves in a water body caused by the displacement of a large volume of water */
+        Tsunami = 1,
+        /** Violent; rotating column of air */
+        Tornado = 2,
+        /** Unplanned; unwanted; uncontrolled fire in an area */
+        Fire = 3,
+        /** Thunder and/or lightning */
+        Thunder = 4,
+      };
+
+      /**
+       * These are used for weather timeline manipulation
+       * that isn't just adding to the stack of weather events
+       */
+      enum class controlcodes {
+        /** How much is stored already */
+        GetLength = 0,
+        /** This wipes the entire timeline */
+        DelTimeline = 1,
+        /** There's a currently valid timeline event with the given type */
+        HasValidEvent = 3
+      };
+
+      /**
+       * Events have types
+       * then they're easier to parse after sending them over the air
+       */
+      enum class eventtype {
+        /** @see obscuration */
+        Obscuration = 0,
+        /** @see precipitation */
+        Precipitation = 1,
+        /** @see wind */
+        Wind = 2,
+        /** @see temperature */
+        Temperature = 3,
+        /** @see airquality */
+        AirQuality = 4,
+        /** @see special */
+        Special = 5,
+        /** @see pressure */
+        Pressure = 6,
+        /** @see location */
+        Location = 7,
+        /** @see cloud */
+        Clouds = 8,
+      };
+
+      /**
+       * Valid event query
+       */
+      class valideventquery {
+      public:
+        static constexpr controlcodes code = controlcodes::HasValidEvent;
+        eventtype eventType;
+      };
+
+      /** The header used for further parsing */
+      class timelineheader {
+      public:
+        /** UNIX timestamp */
+        uint64_t timestamp;
+        /**
+         * Time in seconds until the event expires
+         *
+         * 32 bits ought to be enough for everyone
+         *
+         * If there's a newer event of the same type then it overrides this one, even if it hasn't expired
+         */
+        uint32_t expires;
+        /**
+         * What type of weather-related event
+         */
+        eventtype eventType;
+      };
+
+      /** Specifies how cloudiness is stored */
+      class clouds : public timelineheader {
+      public:
+        /** Cloud coverage in percentage, 0-100% */
+        uint8_t amount;
+      };
+
+      /** Specifies how obscuration is stored */
+      class obscuration : public timelineheader {
+      public:
+        /** Type */
+        obscurationtype type;
+        /** Visibility distance in meters */
+        uint8_t amount;
+      };
+
+      /** Specifies how precipitation is stored */
+      class precipitation : public timelineheader {
+      public:
+        /** Type */
+        precipitationtype type;
+        /** How much is it going to rain? In millimeters */
+        uint8_t amount;
+      };
+
+      /**
+       * How wind speed is stored
+       *
+       * In order to represent bursts of wind instead of constant wind,
+       * you have minimum and maximum speeds.
+       *
+       * As direction can fluctuate wildly and some watchfaces might wish to display it nicely,
+       * we're following the aerospace industry weather report option of specifying a range.
+       */
+      class wind : public timelineheader {
+      public:
+        /** Meters per second */
+        uint8_t speedMin;
+        /** Meters per second */
+        uint8_t speedMax;
+        /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */
+        uint8_t directionMin;
+        /** Unitless direction between 0-255; approximately 1 unit per 0.71 degrees */
+        uint8_t directionMax;
+      };
+
+      /**
+       * How temperature is stored
+       *
+       * As it's annoying to figure out the dewpoint on the watch,
+       * please send it from the companion
+       *
+       * We don't do floats, microdegrees are not useful. Make sure to multiply.
+       */
+      class temperature : public timelineheader {
+      public:
+        /** Temperature °C but multiplied by 100 (e.g. -12.50°C becomes -1250) */
+        int16_t temperature;
+        /** Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250) */
+        int16_t dewPoint;
+      };
+
+      /**
+       * How location info is stored
+       *
+       * This can be mostly static with long expiration,
+       * as it usually is, but it could change during a trip for ex.
+       * so we allow changing it dynamically.
+       *
+       * Location info can be for some kind of map watchface
+       * or daylight calculations, should those be required.
+       *
+       */
+      class location : public timelineheader {
+      public:
+        /** Location name */
+        std::string location;
+        /** Altitude relative to sea level in meters */
+        int16_t altitude;
+        /** Latitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */
+        int32_t latitude;
+        /** Longitude, EPSG:3857 (Google Maps, Openstreetmaps datum) */
+        int32_t longitude;
+      };
+
+      /**
+       * How humidity is stored
+       */
+      class humidity : public timelineheader {
+      public:
+        /** Relative humidity, 0-100% */
+        uint8_t humidity;
+      };
+
+      /**
+       * How air pressure is stored
+       */
+      class pressure : public timelineheader {
+      public:
+        /** Air pressure in hectopascals (hPa) */
+        int16_t pressure;
+      };
+
+      /**
+       * How special events are stored
+       */
+      class special : public timelineheader {
+      public:
+        /** Special event's type */
+        specialtype type;
+      };
+
+      /**
+       * How air quality is stored
+       *
+       * These events are a bit more complex because the topic is not simple,
+       * the intention is to heavy-lift the annoying preprocessing from the watch
+       * this allows watchface or watchapp makers to generate accurate alerts and graphics
+       *
+       * If this needs further enforced standardization, pull requests are welcome
+       */
+      class airquality : public timelineheader {
+      public:
+        /**
+         * The name of the pollution
+         *
+         * for the sake of better compatibility with watchapps
+         * that might want to use this data for say visuals
+         * don't localize the name.
+         *
+         * Ideally watchapp itself localizes the name, if it's at all needed.
+         *
+         * E.g.
+         * For generic ones use "PM0.1", "PM5", "PM10"
+         * For chemical compounds use the molecular formula e.g. "NO2", "CO2", "O3"
+         * For pollen use the genus, e.g. "Betula" for birch or "Alternaria" for that mold's spores
+         */
+        std::string polluter;
+        /**
+         * Amount of the pollution in SI units,
+         * otherwise it's going to be difficult to create UI, alerts
+         * and so on and for.
+         *
+         * See more:
+         * https://ec.europa.eu/environment/air/quality/standards.htm
+         * http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf
+         *
+         * Example units:
+         * count/m³ for pollen
+         * µgC/m³ for micrograms of organic carbon
+         * µg/m³ sulfates, PM0.1, PM1, PM2, PM10 and so on, dust
+         * mg/m³ CO2, CO
+         * ng/m³ for heavy metals
+         *
+         * List is not comprehensive, should be improved.
+         * The current ones are what watchapps assume.
+         *
+         * Note: ppb and ppm to concentration should be calculated on the companion, using
+         * the correct formula (taking into account temperature and air pressure)
+         *
+         * Note2: The amount is off by times 100, for two decimal places of precision.
+         * E.g. 54.32µg/m³ is 5432
+         *
+         */
+        uint32_t amount;
+      };
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/components/ble/weather/WeatherService.cpp b/src/components/ble/weather/WeatherService.cpp
new file mode 100644
index 00000000..006fc6c1
--- /dev/null
+++ b/src/components/ble/weather/WeatherService.cpp
@@ -0,0 +1,208 @@
+/*  Copyright (C) 2021 Avamander
+
+    This file is part of InfiniTime.
+
+    InfiniTime 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.
+
+    InfiniTime 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/>.
+*/
+#include <qcbor/qcbor_spiffy_decode.h>
+#include "WeatherService.h"
+#include "libs/QCBOR/inc/qcbor/qcbor.h"
+#include "systemtask/SystemTask.h"
+
+int WeatherCallback(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg) {
+  return static_cast<Pinetime::Controllers::WeatherService*>(arg)->OnCommand(conn_handle, attr_handle, ctxt);
+}
+
+namespace Pinetime {
+  namespace Controllers {
+    WeatherService::WeatherService(System::SystemTask& system, DateTime& dateTimeController)
+      : system(system), dateTimeController(dateTimeController) {
+    }
+
+    void WeatherService::Init() {
+      uint8_t res = 0;
+      res = ble_gatts_count_cfg(serviceDefinition);
+      ASSERT(res == 0)
+
+      res = ble_gatts_add_svcs(serviceDefinition);
+      ASSERT(res == 0);
+    }
+
+    int WeatherService::OnCommand(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt) {
+      if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
+        getCurrentPressure();
+        tidyTimeline();
+        getTimelineLength();
+        const auto packetLen = OS_MBUF_PKTLEN(ctxt->om);
+        if (packetLen <= 0) {
+          return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+        }
+        // Decode
+        QCBORDecodeContext decodeContext;
+        UsefulBufC EncodedCBOR;
+        // TODO: Check uninit fine
+        QCBORDecode_Init(&decodeContext, EncodedCBOR, QCBOR_DECODE_MODE_NORMAL);
+        QCBORDecode_EnterMap(&decodeContext, nullptr);
+        WeatherData::timelineheader timelineHeader {};
+        // Always encodes to the smallest number of bytes based on the value
+        QCBORDecode_GetInt64InMapSZ(&decodeContext, "Timestamp", reinterpret_cast<int64_t*>(&(timelineHeader.timestamp)));
+        QCBORDecode_GetInt64InMapSZ(&decodeContext, "Expires", reinterpret_cast<int64_t*>(&(timelineHeader.expires)));
+        QCBORDecode_GetInt64InMapSZ(&decodeContext, "EventType", reinterpret_cast<int64_t*>(&(timelineHeader.eventType)));
+        switch (timelineHeader.eventType) {
+            // TODO: Populate
+          case WeatherData::eventtype::AirQuality: {
+            break;
+          }
+          case WeatherData::eventtype::Obscuration: {
+            break;
+          }
+          case WeatherData::eventtype::Precipitation: {
+            break;
+          }
+          case WeatherData::eventtype::Wind: {
+            break;
+          }
+          case WeatherData::eventtype::Temperature: {
+            break;
+          }
+          case WeatherData::eventtype::Special: {
+            break;
+          }
+          case WeatherData::eventtype::Pressure: {
+            break;
+          }
+          case WeatherData::eventtype::Location: {
+            break;
+          }
+          case WeatherData::eventtype::Clouds: {
+            break;
+          }
+          default: {
+            break;
+          }
+        }
+        QCBORDecode_ExitMap(&decodeContext);
+
+        auto uErr = QCBORDecode_Finish(&decodeContext);
+        if (uErr != 0) {
+          return BLE_ATT_ERR_INSUFFICIENT_RES;
+        }
+      } else if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) {
+        // TODO: Detect control messages
+
+        // Encode
+        uint8_t buffer[64];
+        QCBOREncodeContext encodeContext;
+        QCBOREncode_Init(&encodeContext, UsefulBuf_FROM_BYTE_ARRAY(buffer));
+        QCBOREncode_OpenMap(&encodeContext);
+        QCBOREncode_AddTextToMap(&encodeContext, "test", UsefulBuf_FROM_SZ_LITERAL("test"));
+        QCBOREncode_AddInt64ToMap(&encodeContext, "test", 1ul);
+        QCBOREncode_CloseMap(&encodeContext);
+
+        UsefulBufC encodedEvent;
+        auto uErr = QCBOREncode_Finish(&encodeContext, &encodedEvent);
+        if (uErr != 0) {
+          return BLE_ATT_ERR_INSUFFICIENT_RES;
+        }
+        auto res = os_mbuf_append(ctxt->om, &buffer, sizeof(buffer));
+        if (res == 0) {
+          return BLE_ATT_ERR_INSUFFICIENT_RES;
+        }
+
+        return 0;
+      }
+      return 0;
+    }
+
+    WeatherData::location WeatherService::getCurrentLocation() const {
+      return WeatherData::location();
+    }
+    WeatherData::clouds WeatherService::getCurrentClouds() const {
+      return WeatherData::clouds();
+    }
+    WeatherData::obscuration WeatherService::getCurrentObscuration() const {
+      return WeatherData::obscuration();
+    }
+    WeatherData::precipitation WeatherService::getCurrentPrecipitation() const {
+      return WeatherData::precipitation();
+    }
+    WeatherData::wind WeatherService::getCurrentWind() const {
+      return WeatherData::wind();
+    }
+    WeatherData::temperature WeatherService::getCurrentTemperature() const {
+      return WeatherData::temperature();
+    }
+    WeatherData::humidity WeatherService::getCurrentHumidity() const {
+      return WeatherData::humidity();
+    }
+    WeatherData::pressure WeatherService::getCurrentPressure() const {
+      uint64_t currentTimestamp = getCurrentUNIXTimestamp();
+      for (auto&& header : timeline) {
+        if (header->eventType == WeatherData::eventtype::Pressure && header->timestamp + header->expires <= currentTimestamp) {
+          return WeatherData::pressure();
+        }
+      }
+      return WeatherData::pressure();
+    }
+
+    WeatherData::airquality WeatherService::getCurrentQuality() const {
+      return WeatherData::airquality();
+    }
+
+    size_t WeatherService::getTimelineLength() const {
+      return timeline.size();
+    }
+
+    bool WeatherService::addEventToTimeline(std::unique_ptr<WeatherData::timelineheader> event) {
+      if (timeline.size() == timeline.max_size()) {
+        return false;
+      }
+
+      timeline.push_back(std::move(event));
+      return true;
+    }
+
+    bool WeatherService::hasTimelineEventOfType(const WeatherData::eventtype type) const {
+      uint64_t currentTimestamp = getCurrentUNIXTimestamp();
+      for (auto&& header : timeline) {
+        if (header->eventType == type && header->timestamp + header->expires <= currentTimestamp) {
+          // TODO: Check if its currently valid
+          return true;
+        }
+      }
+      return false;
+    }
+
+    void WeatherService::tidyTimeline() {
+      uint64_t timeCurrent = 0;
+      timeline.erase(std::remove_if(std::begin(timeline),
+                                    std::end(timeline),
+                                    [&](std::unique_ptr<WeatherData::timelineheader> const& header) {
+                                      return header->timestamp + header->expires > timeCurrent;
+                                    }),
+                     std::end(timeline));
+
+      std::sort(std::begin(timeline), std::end(timeline), compareTimelineEvents);
+    }
+
+    bool WeatherService::compareTimelineEvents(const std::unique_ptr<WeatherData::timelineheader>& first,
+                                               const std::unique_ptr<WeatherData::timelineheader>& second) {
+      return first->timestamp > second->timestamp;
+    }
+
+    uint64_t WeatherService::getCurrentUNIXTimestamp() const {
+      return std::chrono::duration_cast<std::chrono::seconds>(dateTimeController.CurrentDateTime().time_since_epoch()).count();
+    }
+  }
+}
diff --git a/src/components/ble/weather/WeatherService.h b/src/components/ble/weather/WeatherService.h
new file mode 100644
index 00000000..ef99db86
--- /dev/null
+++ b/src/components/ble/weather/WeatherService.h
@@ -0,0 +1,139 @@
+/*  Copyright (C) 2021 Avamander
+
+    This file is part of InfiniTime.
+
+    InfiniTime 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.
+
+    InfiniTime 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/>.
+*/
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+#include <memory>
+
+#define min // workaround: nimble's min/max macros conflict with libstdc++
+#define max
+#include <host/ble_gap.h>
+#include <host/ble_uuid.h>
+#undef max
+#undef min
+
+#include "WeatherData.h"
+#include <components/datetime/DateTimeController.h>
+
+// 00030000-78fc-48fe-8e23-433b3a1942d0
+#define WEATHER_SERVICE_UUID_BASE                                                                                                          \
+  { 0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, 0x00, 0x00, 0x03, 0x00 }
+#define WEATHER_SERVICE_CHAR_UUID(y, x)                                                                                                    \
+  { 0xd0, 0x42, 0x19, 0x3a, 0x3b, 0x43, 0x23, 0x8e, 0xfe, 0x48, 0xfc, 0x78, (x), (y), 0x03, 0x00 }
+
+int WeatherCallback(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt, void* arg);
+
+namespace Pinetime {
+  namespace System {
+    class SystemTask;
+  }
+  namespace Controllers {
+
+    class WeatherService {
+    public:
+      explicit WeatherService(System::SystemTask& system, DateTime& dateTimeController);
+
+      void Init();
+
+      int OnCommand(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt* ctxt);
+
+      /*
+       * Helper functions for quick access to currently valid data
+       */
+      WeatherData::location getCurrentLocation() const;
+      WeatherData::clouds getCurrentClouds() const;
+      WeatherData::obscuration getCurrentObscuration() const;
+      WeatherData::precipitation getCurrentPrecipitation() const;
+      WeatherData::wind getCurrentWind() const;
+      WeatherData::temperature getCurrentTemperature() const;
+      WeatherData::humidity getCurrentHumidity() const;
+      WeatherData::pressure getCurrentPressure() const;
+      WeatherData::airquality getCurrentQuality() const;
+
+      /*
+       * Management functions
+       */
+      /**
+       * Adds an event to the timeline
+       * @return
+       */
+      bool addEventToTimeline(std::unique_ptr<WeatherData::timelineheader> event);
+      /**
+       * Gets the current timeline length
+       */
+      size_t getTimelineLength() const;
+      /**
+       * Checks if an event of a certain type exists in the timeline
+       * @return
+       */
+      bool hasTimelineEventOfType(WeatherData::eventtype type) const;
+
+    private:
+      ble_uuid128_t msUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_UUID_BASE};
+
+      /**
+       * Just write timeline data here
+       */
+      ble_uuid128_t wDataCharUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_CHAR_UUID(0x00, 0x01)};
+      /**
+       * This doesn't take timeline data
+       * but provides some control over it
+       */
+      ble_uuid128_t wControlCharUuid {.u = {.type = BLE_UUID_TYPE_128}, .value = WEATHER_SERVICE_CHAR_UUID(0x00, 0x02)};
+
+      const struct ble_gatt_chr_def characteristicDefinition[2] = {{.uuid = reinterpret_cast<ble_uuid_t*>(&wDataCharUuid),
+                                                                    .access_cb = WeatherCallback,
+                                                                    .arg = this,
+                                                                    .flags = BLE_GATT_CHR_F_NOTIFY,
+                                                                    .val_handle = &eventHandle},
+                                                                   {.uuid = reinterpret_cast<ble_uuid_t*>(&wControlCharUuid),
+                                                                    .access_cb = WeatherCallback,
+                                                                    .arg = this,
+                                                                    .flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_READ}};
+      const struct ble_gatt_svc_def serviceDefinition[2] = {
+        {.type = BLE_GATT_SVC_TYPE_PRIMARY, .uuid = reinterpret_cast<ble_uuid_t*>(&msUuid), .characteristics = characteristicDefinition},
+        {0}};
+
+      uint16_t eventHandle {};
+
+      Pinetime::System::SystemTask& system;
+      Pinetime::Controllers::DateTime& dateTimeController;
+
+      std::vector<std::unique_ptr<WeatherData::timelineheader>> timeline;
+
+      /**
+       * Cleans up the timeline of expired events
+       * @return result code
+       */
+      void tidyTimeline();
+
+      /**
+       * Compares two timeline events
+       */
+      static bool compareTimelineEvents(const std::unique_ptr<WeatherData::timelineheader>& first,
+                                        const std::unique_ptr<WeatherData::timelineheader>& second);
+
+      /**
+       *
+       */
+      uint64_t getCurrentUNIXTimestamp() const;
+    };
+  }
+}
diff --git a/src/displayapp/screens/Weather.cpp b/src/displayapp/screens/Weather.cpp
new file mode 100644
index 00000000..014761bf
--- /dev/null
+++ b/src/displayapp/screens/Weather.cpp
@@ -0,0 +1,246 @@
+#include "Weather.h"
+#include <lvgl/lvgl.h>
+#include "../DisplayApp.h"
+#include "Label.h"
+#include "Version.h"
+#include "components/battery/BatteryController.h"
+#include "components/ble/BleController.h"
+#include "components/brightness/BrightnessController.h"
+#include "components/datetime/DateTimeController.h"
+#include "drivers/Watchdog.h"
+#include "components/ble/weather/WeatherData.h"
+
+using namespace Pinetime::Applications::Screens;
+
+Weather::Weather(Pinetime::Applications::DisplayApp* app,
+                 Pinetime::Controllers::DateTime& dateTimeController,
+                 Pinetime::Controllers::Battery& batteryController,
+                 Pinetime::Controllers::BrightnessController& brightnessController,
+                 Pinetime::Controllers::Ble& bleController,
+                 Pinetime::Drivers::WatchdogView& watchdog)
+  : Screen(app),
+    dateTimeController {dateTimeController},
+    batteryController {batteryController},
+    brightnessController {brightnessController},
+    bleController {bleController},
+    watchdog {watchdog},
+    screens {app,
+             0,
+             {[this]() -> std::unique_ptr<Screen> {
+                return CreateScreen1();
+              },
+              [this]() -> std::unique_ptr<Screen> {
+                return CreateScreen2();
+              },
+              [this]() -> std::unique_ptr<Screen> {
+                return CreateScreen3();
+              },
+              [this]() -> std::unique_ptr<Screen> {
+                return CreateScreen4();
+              },
+              [this]() -> std::unique_ptr<Screen> {
+                return CreateScreen5();
+              }},
+             Screens::ScreenListModes::UpDown} {
+}
+
+Weather::~Weather() {
+  lv_obj_clean(lv_scr_act());
+}
+
+bool Weather::Refresh() {
+  if (running) {
+    screens.Refresh();
+  }
+  return running;
+}
+
+bool Weather::OnButtonPushed() {
+  running = false;
+  return true;
+}
+
+bool Weather::OnTouchEvent(Pinetime::Applications::TouchEvents event) {
+  return screens.OnTouchEvent(event);
+}
+
+std::unique_ptr<Screen> Weather::CreateScreen1() {
+  lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+  lv_label_set_recolor(label, true);
+  lv_label_set_text_fmt(label,
+                        "#FFFF00 InfiniTime#\n\n"
+                        "#444444 Version# %ld.%ld.%ld\n\n"
+                        "#444444 Build date#\n"
+                        "%s\n"
+                        "%s\n",
+                        Version::Major(),
+                        Version::Minor(),
+                        Version::Patch(),
+                        __DATE__,
+                        __TIME__);
+  lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
+  lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+  return std::unique_ptr<Screen>(new Screens::Label(0, 5, app, label));
+}
+
+std::unique_ptr<Screen> Weather::CreateScreen2() {
+  auto batteryPercent = static_cast<uint8_t>(batteryController.PercentRemaining());
+  float batteryVoltage = batteryController.Voltage();
+
+  auto resetReason = [this]() {
+    switch (watchdog.ResetReason()) {
+      case Drivers::Watchdog::ResetReasons::Watchdog:
+        return "wtdg";
+      case Drivers::Watchdog::ResetReasons::HardReset:
+        return "hardr";
+      case Drivers::Watchdog::ResetReasons::NFC:
+        return "nfc";
+      case Drivers::Watchdog::ResetReasons::SoftReset:
+        return "softr";
+      case Drivers::Watchdog::ResetReasons::CpuLockup:
+        return "cpulock";
+      case Drivers::Watchdog::ResetReasons::SystemOff:
+        return "off";
+      case Drivers::Watchdog::ResetReasons::LpComp:
+        return "lpcomp";
+      case Drivers::Watchdog::ResetReasons::DebugInterface:
+        return "dbg";
+      case Drivers::Watchdog::ResetReasons::ResetPin:
+        return "rst";
+      default:
+        return "?";
+    }
+  }();
+
+  // uptime
+  static constexpr uint32_t secondsInADay = 60 * 60 * 24;
+  static constexpr uint32_t secondsInAnHour = 60 * 60;
+  static constexpr uint32_t secondsInAMinute = 60;
+  uint32_t uptimeSeconds = dateTimeController.Uptime().count();
+  uint32_t uptimeDays = (uptimeSeconds / secondsInADay);
+  uptimeSeconds = uptimeSeconds % secondsInADay;
+  uint32_t uptimeHours = uptimeSeconds / secondsInAnHour;
+  uptimeSeconds = uptimeSeconds % secondsInAnHour;
+  uint32_t uptimeMinutes = uptimeSeconds / secondsInAMinute;
+  uptimeSeconds = uptimeSeconds % secondsInAMinute;
+  // TODO handle more than 100 days of uptime
+
+  if (batteryPercent == -1)
+    batteryPercent = 0;
+
+  // hack to not use the flot functions from printf
+  uint8_t batteryVoltageBytes[2];
+  batteryVoltageBytes[1] = static_cast<uint8_t>(batteryVoltage); // truncate whole numbers
+  batteryVoltageBytes[0] =
+    static_cast<uint8_t>((batteryVoltage - batteryVoltageBytes[1]) * 100); // remove whole part of flt and shift 2 places over
+  //
+
+  lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+  lv_label_set_recolor(label, true);
+  lv_label_set_text_fmt(label,
+                        "#444444 Date# %02d/%02d/%04d\n"
+                        "#444444 Time# %02d:%02d:%02d\n"
+                        "#444444 Uptime#\n %02lud %02lu:%02lu:%02lu\n"
+                        "#444444 Battery# %d%%/%1i.%02iv\n"
+                        "#444444 Backlight# %s\n"
+                        "#444444 Last reset# %s\n",
+                        dateTimeController.Day(),
+                        static_cast<uint8_t>(dateTimeController.Month()),
+                        dateTimeController.Year(),
+                        dateTimeController.Hours(),
+                        dateTimeController.Minutes(),
+                        dateTimeController.Seconds(),
+                        uptimeDays,
+                        uptimeHours,
+                        uptimeMinutes,
+                        uptimeSeconds,
+                        batteryPercent,
+                        batteryVoltageBytes[1],
+                        batteryVoltageBytes[0],
+                        brightnessController.ToString(),
+                        resetReason);
+  lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+  return std::unique_ptr<Screen>(new Screens::Label(1, 4, app, label));
+}
+
+std::unique_ptr<Screen> Weather::CreateScreen3() {
+  lv_mem_monitor_t mon;
+  lv_mem_monitor(&mon);
+
+  lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+  lv_label_set_recolor(label, true);
+  auto& bleAddr = bleController.Address();
+  lv_label_set_text_fmt(label,
+                        "#444444 BLE MAC#\n"
+                        " %02x:%02x:%02x:%02x:%02x:%02x"
+                        "\n"
+                        "#444444 Memory#\n"
+                        " #444444 used# %d (%d%%)\n"
+                        " #444444 frag# %d%%\n"
+                        " #444444 free# %d"
+                        "\n"
+                        "#444444 Steps# %li",
+                        bleAddr[5],
+                        bleAddr[4],
+                        bleAddr[3],
+                        bleAddr[2],
+                        bleAddr[1],
+                        bleAddr[0],
+                        (int) mon.total_size - mon.free_size,
+                        mon.used_pct,
+                        mon.frag_pct,
+                        (int) mon.free_biggest_size,
+                        0);
+  lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+  return std::unique_ptr<Screen>(new Screens::Label(2, 5, app, label));
+}
+
+bool sortById(const TaskStatus_t& lhs, const TaskStatus_t& rhs) {
+  return lhs.xTaskNumber < rhs.xTaskNumber;
+}
+
+std::unique_ptr<Screen> Weather::CreateScreen4() {
+  TaskStatus_t tasksStatus[7];
+  lv_obj_t* infoTask = lv_table_create(lv_scr_act(), NULL);
+  lv_table_set_col_cnt(infoTask, 3);
+  lv_table_set_row_cnt(infoTask, 8);
+  lv_obj_set_pos(infoTask, 10, 10);
+
+  lv_table_set_cell_value(infoTask, 0, 0, "#");
+  lv_table_set_col_width(infoTask, 0, 50);
+  lv_table_set_cell_value(infoTask, 0, 1, "Task");
+  lv_table_set_col_width(infoTask, 1, 80);
+  lv_table_set_cell_value(infoTask, 0, 2, "Free");
+  lv_table_set_col_width(infoTask, 2, 90);
+
+  auto nb = uxTaskGetSystemState(tasksStatus, 7, nullptr);
+  std::sort(tasksStatus, tasksStatus + nb, sortById);
+  for (uint8_t i = 0; i < nb; i++) {
+
+    lv_table_set_cell_value(infoTask, i + 1, 0, std::to_string(tasksStatus[i].xTaskNumber).c_str());
+    lv_table_set_cell_value(infoTask, i + 1, 1, tasksStatus[i].pcTaskName);
+    if (tasksStatus[i].usStackHighWaterMark < 20) {
+      std::string str1 = std::to_string(tasksStatus[i].usStackHighWaterMark) + " low";
+      lv_table_set_cell_value(infoTask, i + 1, 2, str1.c_str());
+    } else {
+      lv_table_set_cell_value(infoTask, i + 1, 2, std::to_string(tasksStatus[i].usStackHighWaterMark).c_str());
+    }
+  }
+  return std::unique_ptr<Screen>(new Screens::Label(3, 5, app, infoTask));
+}
+
+std::unique_ptr<Screen> Weather::CreateScreen5() {
+  lv_obj_t* label = lv_label_create(lv_scr_act(), nullptr);
+  lv_label_set_recolor(label, true);
+  lv_label_set_text_static(label,
+                           "Software Licensed\n"
+                           "under the terms of\n"
+                           "the GNU General\n"
+                           "Public License v3\n"
+                           "#444444 Source code#\n"
+                           "#FFFF00 https://github.com/#\n"
+                           "#FFFF00 JF002/InfiniTime#");
+  lv_label_set_align(label, LV_LABEL_ALIGN_CENTER);
+  lv_obj_align(label, lv_scr_act(), LV_ALIGN_CENTER, 0, 0);
+  return std::unique_ptr<Screen>(new Screens::Label(4, 5, app, label));
+}
diff --git a/src/displayapp/screens/Weather.h b/src/displayapp/screens/Weather.h
new file mode 100644
index 00000000..8b393ca1
--- /dev/null
+++ b/src/displayapp/screens/Weather.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <memory>
+#include "Screen.h"
+#include "ScreenList.h"
+
+namespace Pinetime {
+  namespace Controllers {
+    class DateTime;
+    class Battery;
+    class BrightnessController;
+    class Ble;
+  }
+
+  namespace Drivers {
+    class WatchdogView;
+  }
+
+  namespace Applications {
+    class DisplayApp;
+
+    namespace Screens {
+      class Weather : public Screen {
+      public:
+        explicit Weather(DisplayApp* app,
+                         Pinetime::Controllers::DateTime& dateTimeController,
+                         Pinetime::Controllers::Battery& batteryController,
+                         Pinetime::Controllers::BrightnessController& brightnessController,
+                         Pinetime::Controllers::Ble& bleController,
+                         Pinetime::Drivers::WatchdogView& watchdog);
+        ~Weather() override;
+        bool Refresh() override;
+        bool OnButtonPushed() override;
+        bool OnTouchEvent(TouchEvents event) override;
+
+      private:
+        bool running = true;
+
+        Pinetime::Controllers::DateTime& dateTimeController;
+        Pinetime::Controllers::Battery& batteryController;
+        Pinetime::Controllers::BrightnessController& brightnessController;
+        Pinetime::Controllers::Ble& bleController;
+        Pinetime::Drivers::WatchdogView& watchdog;
+
+        ScreenList<5> screens;
+        std::unique_ptr<Screen> CreateScreen1();
+        std::unique_ptr<Screen> CreateScreen2();
+        std::unique_ptr<Screen> CreateScreen3();
+        std::unique_ptr<Screen> CreateScreen4();
+        std::unique_ptr<Screen> CreateScreen5();
+      };
+    }
+  }
+}
\ No newline at end of file