diff --git a/Source/Clima_Demo/Clima_Demo.csproj b/Source/Clima_Demo/Clima_Demo.csproj
index 0907a6f..fe32342 100644
--- a/Source/Clima_Demo/Clima_Demo.csproj
+++ b/Source/Clima_Demo/Clima_Demo.csproj
@@ -5,6 +5,7 @@
Library
App
10
+ enable
diff --git a/Source/Clima_Demo/MeadowApp.cs b/Source/Clima_Demo/MeadowApp.cs
index 1f91efd..599a554 100644
--- a/Source/Clima_Demo/MeadowApp.cs
+++ b/Source/Clima_Demo/MeadowApp.cs
@@ -8,67 +8,23 @@ namespace Clima_Demo;
public class MeadowApp : App
{
- private IClimaHardware clima;
- private NotificationController notificationController;
- private SensorController sensorController;
- private PowerController powerController;
- private LocationController locationController;
+ private MainController mainController;
public MeadowApp()
{
- Resolver.Services.Add(new CloudController());
+ mainController = new MainController();
}
public override void OnBootFromCrash(IEnumerable crashReports)
{
- Resolver.Services.Get()?.LogAppStartupAfterCrash();
+ mainController.LogAppStartupAfterCrash(crashReports);
}
public override Task Initialize()
{
- Resolver.Log.LogLevel = Meadow.Logging.LogLevel.Information;
-
- Resolver.Log.Info("Initialize hardware...");
-
- clima = Clima.Create();
-
- notificationController = new NotificationController(clima.RgbLed);
- Resolver.Services.Add(notificationController);
-
- notificationController.Starting();
-
- Resolver.Services.Get()?.LogAppStartup(clima.RevisionString);
- Resolver.Log.Info($"Running on Clima Hardware {clima.RevisionString}");
-
- sensorController = new SensorController(clima);
- powerController = new PowerController(clima);
- locationController = new LocationController(clima);
-
var wifi = Device.NetworkAdapters.Primary();
- wifi.NetworkConnected += OnNetworkConnected;
- wifi.NetworkDisconnected += OnNetworkDisconnected;
-
- if (wifi.IsConnected)
- {
- notificationController.NetworkConnected();
- }
- else
- {
- notificationController.NetworkDisconnected();
- }
-
- Resolver.Log.Info("Initialization complete");
+ mainController.Initialize(Clima.Create(), wifi);
return Task.CompletedTask;
}
-
- private void OnNetworkDisconnected(INetworkAdapter sender, NetworkDisconnectionEventArgs args)
- {
- notificationController.NetworkDisconnected();
- }
-
- private void OnNetworkConnected(INetworkAdapter sender, NetworkConnectionEventArgs args)
- {
- notificationController.NetworkConnected();
- }
}
\ No newline at end of file
diff --git a/Source/Meadow.Clima.sln b/Source/Meadow.Clima.sln
index 2812725..3201975 100644
--- a/Source/Meadow.Clima.sln
+++ b/Source/Meadow.Clima.sln
@@ -58,6 +58,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clima_SQLite_Demo", "Additi
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommonContracts", "Additional Samples\CommonContracts\CommonContracts.shproj", "{567267B3-ED96-4FEA-B555-2EE203372EA4}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serialization.MicroJson", "..\..\Meadow.Foundation\Source\Meadow.Foundation.Libraries_and_Frameworks\Serialization.MicroJson\Driver\Serialization.MicroJson.csproj", "{6300EAB4-806F-4C18-8FE0-57C45A2C0C58}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -488,6 +490,18 @@ Global
{494082D7-2C48-45A6-8FF7-DD553D27BC4A}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{494082D7-2C48-45A6-8FF7-DD553D27BC4A}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{494082D7-2C48-45A6-8FF7-DD553D27BC4A}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhone.Build.0 = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -517,6 +531,7 @@ Global
{0B2E742C-9C97-4CE1-8B2A-1390CB3F1B03} = {4AB0FC09-05D2-4F55-9C2D-13C133456E2F}
{494082D7-2C48-45A6-8FF7-DD553D27BC4A} = {4AB0FC09-05D2-4F55-9C2D-13C133456E2F}
{567267B3-ED96-4FEA-B555-2EE203372EA4} = {4AB0FC09-05D2-4F55-9C2D-13C133456E2F}
+ {6300EAB4-806F-4C18-8FE0-57C45A2C0C58} = {2889A476-F914-49E8-9F97-4CC6CA34A901}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CA61E123-F783-4CB3-8EB2-099EE930ADD4}
diff --git a/Source/Meadow.Clima/Constants/CloudEventIds.cs b/Source/Meadow.Clima/Constants/CloudEventIds.cs
index ff15aba..503bd56 100644
--- a/Source/Meadow.Clima/Constants/CloudEventIds.cs
+++ b/Source/Meadow.Clima/Constants/CloudEventIds.cs
@@ -3,5 +3,6 @@
public enum CloudEventIds
{
DeviceStarted = 100,
+ Telemetry = 110,
BootFromCrash = 200
}
diff --git a/Source/Meadow.Clima/Controllers/CloudController.cs b/Source/Meadow.Clima/Controllers/CloudController.cs
index 060a58d..2c25e32 100644
--- a/Source/Meadow.Clima/Controllers/CloudController.cs
+++ b/Source/Meadow.Clima/Controllers/CloudController.cs
@@ -1,6 +1,7 @@
using Meadow;
using Meadow.Cloud;
using System;
+using System.Linq;
namespace Clima_Demo;
@@ -8,15 +9,43 @@ public class CloudController
{
public void LogAppStartupAfterCrash()
{
- LogEvent(CloudEventIds.DeviceStarted, $"Device restarted after crash");
+ SendEvent(CloudEventIds.DeviceStarted, $"Device restarted after crash");
}
public void LogAppStartup(string hardwareRevision)
{
- LogEvent(CloudEventIds.DeviceStarted, $"Device started (hardware {hardwareRevision})");
+ SendEvent(CloudEventIds.DeviceStarted, $"Device started (hardware {hardwareRevision})");
}
- private void LogEvent(CloudEventIds eventId, string message)
+ public void LogWarning(string message)
+ {
+ SendLog(message, "warning");
+ }
+
+ public void LogMessage(string message)
+ {
+ SendLog(message, "information");
+ }
+
+ public void LogTelemetry(SensorData sensorData, PowerData powerData)
+ {
+ var measurements = sensorData
+ .AsTelemetryDictionary()
+ .Concat(powerData.AsTelemetryDictionary())
+ .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+
+ var cloudEvent = new CloudEvent
+ {
+ Description = "Clima Telemetry",
+ Timestamp = DateTime.UtcNow,
+ EventId = (int)CloudEventIds.Telemetry,
+ Measurements = measurements
+ };
+
+ SendEvent(cloudEvent);
+ }
+
+ private void SendLog(string message, string severity)
{
if (Resolver.MeadowCloudService == null)
{
@@ -30,14 +59,50 @@ private void LogEvent(CloudEventIds eventId, string message)
return;
}
- Resolver.Log.Info($"Sending cloud event");
+ Resolver.Log.Info($"Sending cloud log");
- Resolver.MeadowCloudService.SendEvent(
- new CloudEvent
+ Resolver.MeadowCloudService.SendLog(
+ new CloudLog
{
- EventId = (int)eventId,
- Description = message,
+ Message = message,
Timestamp = DateTime.UtcNow,
+ Severity = severity
});
}
+
+ private void SendEvent(CloudEventIds eventId, string message)
+ {
+ SendEvent(new CloudEvent
+ {
+ EventId = (int)eventId,
+ Description = message,
+ Timestamp = DateTime.UtcNow,
+ });
+ }
+
+ private void SendEvent(CloudEvent cloudEvent)
+ {
+ if (Resolver.MeadowCloudService == null)
+ {
+ Resolver.Log.Warn($"CLOUD SERVICE IS NULL");
+ return;
+ }
+
+ if (!Resolver.MeadowCloudService.IsEnabled)
+ {
+ Resolver.Log.Warn($"CLOUD INTEGRATION IS DISABLED");
+ return;
+ }
+
+ Resolver.Log.Info($"Sending cloud event");
+
+ try
+ {
+ Resolver.MeadowCloudService.SendEvent(cloudEvent);
+ }
+ catch (Exception ex)
+ {
+ Resolver.Log.Warn($"Failed to send cloud event: {ex.Message}");
+ }
+ }
}
diff --git a/Source/Meadow.Clima/Controllers/NetworkController.cs b/Source/Meadow.Clima/Controllers/NetworkController.cs
new file mode 100644
index 0000000..89616d2
--- /dev/null
+++ b/Source/Meadow.Clima/Controllers/NetworkController.cs
@@ -0,0 +1,54 @@
+using Meadow.Hardware;
+using System;
+using System.Threading;
+
+namespace Meadow.Devices;
+
+public class NetworkController
+{
+ public event EventHandler? ConnectionStateChanged;
+ public event EventHandler? NetworkDown;
+
+ private readonly INetworkAdapter networkAdapter;
+ private DateTimeOffset? lastDown;
+ private Timer downEventTimer;
+
+ public bool IsConnected => networkAdapter.IsConnected;
+ public TimeSpan DownTime => lastDown == null ? TimeSpan.Zero : DateTime.UtcNow - lastDown.Value;
+ public TimeSpan DownEventPeriod { get; } = TimeSpan.FromSeconds(30);
+
+ public NetworkController(INetworkAdapter networkAdapter)
+ {
+ this.networkAdapter = networkAdapter;
+
+ networkAdapter.NetworkConnected += OnNetworkConnected;
+ networkAdapter.NetworkDisconnected += OnNetworkDisconnected;
+
+ downEventTimer = new Timer(DownEventTimerProc, null, -1, -1);
+ }
+
+ private void DownEventTimerProc(object _)
+ {
+ if (networkAdapter.IsConnected)
+ {
+ downEventTimer.Change(-1, -1);
+ return;
+ }
+
+ NetworkDown?.Invoke(this, DownTime);
+ downEventTimer.Change(DownEventPeriod, TimeSpan.FromMilliseconds(-1));
+ }
+
+ private void OnNetworkDisconnected(INetworkAdapter sender, NetworkDisconnectionEventArgs args)
+ {
+ lastDown = DateTimeOffset.UtcNow;
+ downEventTimer.Change(DownEventPeriod, TimeSpan.FromMilliseconds(-1));
+ ConnectionStateChanged?.Invoke(this, false);
+ }
+
+ private void OnNetworkConnected(INetworkAdapter sender, NetworkConnectionEventArgs args)
+ {
+ lastDown = null;
+ ConnectionStateChanged?.Invoke(this, true);
+ }
+}
diff --git a/Source/Meadow.Clima/Controllers/NotificationController.cs b/Source/Meadow.Clima/Controllers/NotificationController.cs
index fbdb61f..a958daa 100644
--- a/Source/Meadow.Clima/Controllers/NotificationController.cs
+++ b/Source/Meadow.Clima/Controllers/NotificationController.cs
@@ -1,31 +1,58 @@
-using Meadow;
-using Meadow.Peripherals.Leds;
+using Meadow.Peripherals.Leds;
+using System;
namespace Clima_Demo;
public class NotificationController
{
+ [Flags]
+ public enum Warnings
+ {
+ None = 0,
+ NetworkDisconnected = 1 << 0,
+ SolarLoadLow = 1 << 1,
+ BatteryLow = 1 << 2,
+ }
+
private readonly IRgbPwmLed? rgbLed;
+ private Warnings activeWarnings = Warnings.None;
public NotificationController(IRgbPwmLed? rgbLed)
{
this.rgbLed = rgbLed;
}
- public void Starting()
+ public void SystemStarting()
{
rgbLed?.SetColor(RgbLedColors.Red);
}
- public void NetworkConnected()
+ public void SystemUp()
+ {
+ ReportWarnings();
+ }
+
+ public void SetWarning(Warnings warning)
+ {
+ activeWarnings |= warning;
+ ReportWarnings();
+ }
+
+ public void ClearWarning(Warnings warning)
{
- Resolver.Log.Info("Network connected");
- rgbLed?.SetColor(RgbLedColors.Green);
+ activeWarnings &= ~warning;
+ ReportWarnings();
}
- public void NetworkDisconnected()
+ private void ReportWarnings()
{
- Resolver.Log.Info("Network disconnected");
- rgbLed?.SetColor(RgbLedColors.Yellow);
+ if (activeWarnings != Warnings.None)
+ {
+ rgbLed?.SetColor(RgbLedColors.Yellow);
+ }
+ else
+ {
+ rgbLed?.SetColor(RgbLedColors.Green);
+ }
}
}
diff --git a/Source/Meadow.Clima/Controllers/PowerController.cs b/Source/Meadow.Clima/Controllers/PowerController.cs
index 998ef09..04e7550 100644
--- a/Source/Meadow.Clima/Controllers/PowerController.cs
+++ b/Source/Meadow.Clima/Controllers/PowerController.cs
@@ -2,15 +2,35 @@
using Meadow.Devices;
using Meadow.Units;
using System;
+using System.Threading.Tasks;
namespace Clima_Demo;
public class PowerController
{
+ private readonly IClimaHardware clima;
+
+ public event EventHandler? SolarVoltageWarning;
+ public event EventHandler? BatteryVoltageWarning;
+
+ private bool inBatteryWarningState = false;
+ private bool inSolarWarningState = false;
+
public bool LogPowerData { get; set; } = false;
public TimeSpan UpdateInterval { get; } = TimeSpan.FromSeconds(5);
+ public Voltage LowBatteryWarningLevel { get; } = 3.3.Volts();
+ public Voltage LowSolarWarningLevel { get; } = 3.0.Volts();
+ public Voltage WarningDeadband { get; } = 0.25.Volts();
+
+ public PowerController(
+ IClimaHardware clima)
+ {
+ this.clima = clima;
+
+ Initialize();
+ }
- public PowerController(IClimaHardware clima)
+ private void Initialize()
{
if (clima.SolarVoltageInput is { } solarVoltage)
{
@@ -25,13 +45,70 @@ public PowerController(IClimaHardware clima)
}
}
+ public async Task GetPowerData()
+ {
+ return new PowerData
+ {
+ BatteryVoltage = clima.BatteryVoltageInput?.Voltage ?? null,
+ SolarVoltage = clima.SolarVoltageInput?.Voltage ?? null,
+ };
+ }
+
private void SolarVoltageUpdated(object sender, IChangeResult e)
{
Resolver.Log.InfoIf(LogPowerData, $"Solar Voltage: {e.New.Volts:0.#} volts");
+
+ if (e.New < LowSolarWarningLevel)
+ {
+ if (!inSolarWarningState)
+ {
+ SolarVoltageWarning?.Invoke(this, true);
+
+ inSolarWarningState = true;
+ }
+ }
+ else
+ {
+ if (inSolarWarningState)
+ {
+ var resetVoltage = LowBatteryWarningLevel + WarningDeadband;
+
+ if (e.New > resetVoltage)
+ {
+ SolarVoltageWarning?.Invoke(this, false);
+
+ inSolarWarningState = false;
+ }
+ }
+ }
}
private void BatteryVoltageUpdated(object sender, IChangeResult e)
{
- Resolver.Log.InfoIf(LogPowerData, $"Battery Voltage: {e.New.Volts:0.#} volts");
+ Resolver.Log.InfoIf(LogPowerData, $"Battery Voltage: {e.New.Volts:0.#} volts");
+
+ if (e.New < LowBatteryWarningLevel)
+ {
+ if (!inBatteryWarningState)
+ {
+ BatteryVoltageWarning?.Invoke(this, true);
+
+ inBatteryWarningState = true;
+ }
+ }
+ else
+ {
+ if (inBatteryWarningState)
+ {
+ var resetVoltage = LowBatteryWarningLevel + WarningDeadband;
+
+ if (e.New > resetVoltage)
+ {
+ BatteryVoltageWarning?.Invoke(this, false);
+
+ inBatteryWarningState = false;
+ }
+ }
+ }
}
}
diff --git a/Source/Meadow.Clima/Controllers/SensorController.cs b/Source/Meadow.Clima/Controllers/SensorController.cs
index f67c47b..ecb1454 100644
--- a/Source/Meadow.Clima/Controllers/SensorController.cs
+++ b/Source/Meadow.Clima/Controllers/SensorController.cs
@@ -2,16 +2,21 @@
using Meadow.Devices;
using Meadow.Units;
using System;
+using System.Threading.Tasks;
namespace Clima_Demo;
public class SensorController
{
+ private IClimaHardware hardware;
+
public bool LogSensorData { get; set; } = false;
public TimeSpan UpdateInterval { get; } = TimeSpan.FromSeconds(5);
public SensorController(IClimaHardware clima)
{
+ hardware = clima;
+
if (clima.TemperatureSensor is { } temperatureSensor)
{
temperatureSensor.Updated += TemperatureUpdated;
@@ -55,6 +60,20 @@ public SensorController(IClimaHardware clima)
}
}
+ public async Task GetSensorData()
+ {
+ return new SensorData
+ {
+ Temperature = hardware.TemperatureSensor?.Temperature ?? null,
+ Pressure = hardware.BarometricPressureSensor?.Pressure ?? null,
+ Humidity = hardware.HumiditySensor?.Humidity ?? null,
+ Co2Level = hardware.CO2ConcentrationSensor?.CO2Concentration ?? null,
+ WindSpeed = hardware.Anemometer?.WindSpeed ?? null,
+ WindDirection = hardware.WindVane?.WindAzimuth ?? null,
+ Rain = hardware.RainGauge?.RainDepth ?? null,
+ };
+ }
+
private void TemperatureUpdated(object sender, IChangeResult e)
{
Resolver.Log.InfoIf(LogSensorData, $"Temperature: {e.New.Celsius:0.#}C");
diff --git a/Source/Meadow.Clima/MainController.cs b/Source/Meadow.Clima/MainController.cs
new file mode 100644
index 0000000..a8dc7ab
--- /dev/null
+++ b/Source/Meadow.Clima/MainController.cs
@@ -0,0 +1,167 @@
+using Clima_Demo;
+using Meadow.Hardware;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Meadow.Devices;
+
+public class MainController
+{
+ private IClimaHardware hardware;
+ private NotificationController notificationController;
+ private SensorController sensorController;
+ private PowerController powerController;
+ private LocationController locationController;
+ private NetworkController? networkController;
+ private CloudController cloudController;
+ private Timer TelemetryTimer;
+
+ public TimeSpan TelemetryPublicationPeriod { get; } = TimeSpan.FromMinutes(1);
+
+ public Task Initialize(IClimaHardware hardware, INetworkAdapter? networkAdapter)
+ {
+ this.hardware = hardware;
+
+ Resolver.Log.Info("Initialize hardware...");
+
+ notificationController = new NotificationController(hardware.RgbLed);
+ Resolver.Services.Add(notificationController);
+
+ notificationController.SystemStarting();
+
+ cloudController = new CloudController();
+
+ Resolver.Services.Get()?.LogAppStartup(hardware.RevisionString);
+ Resolver.Log.Info($"Running on Clima Hardware {hardware.RevisionString}");
+
+ sensorController = new SensorController(hardware);
+
+ powerController = new PowerController(hardware);
+ powerController.SolarVoltageWarning += OnSolarVoltageWarning;
+ powerController.BatteryVoltageWarning += OnBatteryVoltageWarning;
+
+ locationController = new LocationController(hardware);
+
+ if (networkAdapter == null)
+ {
+ Resolver.Log.Error("No network adapter found!");
+ }
+ else
+ {
+ networkController = new NetworkController(networkAdapter);
+ networkController.ConnectionStateChanged += OnNetworkConnectionStateChanged;
+ networkController.NetworkDown += OnNetworkStillDown;
+
+ if (!networkController.IsConnected)
+ {
+ Resolver.Log.Info("Network is down");
+ notificationController.SetWarning(NotificationController.Warnings.NetworkDisconnected);
+ }
+ }
+
+ notificationController.SystemUp();
+ cloudController.LogAppStartup(hardware.RevisionString);
+
+ TelemetryTimer = new Timer(TelemetryTimerProc, null, 0, -1);
+
+ return Task.CompletedTask;
+ }
+
+ private async void TelemetryTimerProc(object _)
+ {
+ Resolver.Log.Info($"Collecting telemetry");
+
+ try
+ {
+ cloudController.LogTelemetry(
+ await sensorController.GetSensorData(),
+ await powerController.GetPowerData());
+ }
+ catch (Exception ex)
+ {
+ Resolver.Log.Warn($"Failed to log telemetry: {ex.Message}");
+ }
+
+ TelemetryTimer.Change(TelemetryPublicationPeriod, TimeSpan.FromMilliseconds(-1));
+ }
+
+ private void OnNetworkStillDown(object sender, System.TimeSpan e)
+ {
+ Resolver.Log.Info($"Network has been down for {e.TotalSeconds:N0} seconds");
+
+ // TODO: after some period, should we force-restart the device?
+ }
+
+ private void OnNetworkConnectionStateChanged(object sender, bool e)
+ {
+ if (e)
+ {
+ Resolver.Log.Info($"Network connected");
+ notificationController.ClearWarning(NotificationController.Warnings.NetworkDisconnected);
+ }
+ else
+ {
+ Resolver.Log.Info($"Network disconnected");
+ notificationController.SetWarning(NotificationController.Warnings.NetworkDisconnected);
+ }
+ }
+
+ private void OnBatteryVoltageWarning(object sender, bool e)
+ {
+ if (e)
+ {
+ var message = $"Battery voltage dropped below {powerController.LowBatteryWarningLevel.Volts:N1}";
+ Resolver.Log.Warn(message);
+
+ notificationController.SetWarning(NotificationController.Warnings.BatteryLow);
+ cloudController.LogWarning(message);
+ }
+ else
+ {
+ var message = $"Battery voltage is back above minimum";
+ Resolver.Log.Info(message);
+
+ notificationController.ClearWarning(NotificationController.Warnings.BatteryLow);
+ cloudController.LogMessage(message);
+ }
+ }
+
+ private void OnSolarVoltageWarning(object sender, bool e)
+ {
+ if (e)
+ {
+ var message = $"Solar voltage dropped below {powerController.LowSolarWarningLevel.Volts:N1}";
+ Resolver.Log.Warn(message);
+
+ notificationController.SetWarning(NotificationController.Warnings.SolarLoadLow);
+ cloudController.LogWarning(message);
+ }
+ else
+ {
+ var message = $"Solar voltage is back above minimum";
+ Resolver.Log.Info(message);
+
+ notificationController.ClearWarning(NotificationController.Warnings.SolarLoadLow);
+ cloudController.LogMessage(message);
+ }
+ }
+
+ public Task Run()
+ {
+ return Task.CompletedTask;
+ }
+
+ public void LogAppStartupAfterCrash(IEnumerable crashReports)
+ {
+ // the cloud service's health reporter will log this for us automatically, so no need to manually do so
+ Resolver.Log.Warn("Boot after crash!");
+
+ foreach (var report in crashReports)
+ {
+ Resolver.Log.Info(report);
+ }
+ }
+
+}
diff --git a/Source/Meadow.Clima/Models/PowerData.cs b/Source/Meadow.Clima/Models/PowerData.cs
new file mode 100644
index 0000000..2eca7b0
--- /dev/null
+++ b/Source/Meadow.Clima/Models/PowerData.cs
@@ -0,0 +1,25 @@
+using Meadow.Units;
+using System.Collections.Generic;
+
+namespace Clima_Demo;
+
+public record PowerData
+{
+ public Voltage? SolarVoltage { get; set; }
+ public Voltage? BatteryVoltage { get; set; }
+
+ public Dictionary AsTelemetryDictionary()
+ {
+ var d = new Dictionary();
+ if (SolarVoltage != null)
+ {
+ d.Add(nameof(SolarVoltage), SolarVoltage.Value.Volts);
+ }
+ if (BatteryVoltage != null)
+ {
+ d.Add(nameof(BatteryVoltage), BatteryVoltage.Value.Volts);
+ }
+
+ return d;
+ }
+}
diff --git a/Source/Meadow.Clima/Models/SensorData.cs b/Source/Meadow.Clima/Models/SensorData.cs
new file mode 100644
index 0000000..03c36dd
--- /dev/null
+++ b/Source/Meadow.Clima/Models/SensorData.cs
@@ -0,0 +1,50 @@
+using Meadow.Units;
+using System.Collections.Generic;
+
+namespace Clima_Demo;
+
+public record SensorData
+{
+ public Temperature? Temperature { get; set; }
+ public Pressure? Pressure { get; set; }
+ public RelativeHumidity? Humidity { get; set; }
+ public Concentration? Co2Level { get; set; }
+ public Speed? WindSpeed { get; set; }
+ public Azimuth? WindDirection { get; set; }
+ public Length? Rain { get; set; }
+
+ public Dictionary AsTelemetryDictionary()
+ {
+ var d = new Dictionary();
+ if (Temperature != null)
+ {
+ d.Add(nameof(Temperature), Temperature.Value.Celsius);
+ }
+ if (Pressure != null)
+ {
+ d.Add(nameof(Pressure), Pressure.Value.Bar);
+ }
+ if (Humidity != null)
+ {
+ d.Add(nameof(Humidity), Humidity.Value.Percent);
+ }
+ if (Co2Level != null)
+ {
+ d.Add(nameof(Co2Level), Co2Level.Value.PartsPerMillion);
+ }
+ if (WindSpeed != null)
+ {
+ d.Add(nameof(WindSpeed), WindSpeed.Value.KilometersPerHour);
+ }
+ if (WindDirection != null)
+ {
+ d.Add(nameof(WindDirection), WindDirection.Value.DecimalDegrees);
+ }
+ if (Rain != null)
+ {
+ d.Add(nameof(Rain), Rain.Value.Centimeters);
+ }
+
+ return d;
+ }
+}