diff --git a/PowerControlHub/PowerControlHubApp.cpp b/PowerControlHub/PowerControlHubApp.cpp
index 3ef5f91..a72357a 100644
--- a/PowerControlHub/PowerControlHubApp.cpp
+++ b/PowerControlHub/PowerControlHubApp.cpp
@@ -337,6 +337,7 @@ void PowerControlHubApp::setup(RemoteSensor** remoteSensors, uint8_t remoteSenso
#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
_systemCommandHandler.setOtaManager(&_otaManager);
+ _systemNetworkHandler.setOtaManager(&_otaManager);
_otaManager.begin();
#endif
diff --git a/PowerControlHub/SystemNetworkHandler.cpp b/PowerControlHub/SystemNetworkHandler.cpp
index 541c378..e5a863d 100644
--- a/PowerControlHub/SystemNetworkHandler.cpp
+++ b/PowerControlHub/SystemNetworkHandler.cpp
@@ -23,6 +23,10 @@
#include "SystemFunctions.h"
#include "FirmwareVersion.h"
+#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
+#include "OtaManager.h"
+#endif
+
#if defined(SD_CARD_SUPPORT)
#include "MicroSdDriver.h"
#endif
@@ -84,6 +88,128 @@ CommandResult SystemNetworkHandler::handleRequest(const char* method,
return CommandResult::ok();
}
+ #if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
+ else if (SystemFunctions::commandMatches(command, SystemCheckForUpdate))
+ {
+ if (!_otaManager)
+ {
+ snprintf(responseBuffer, bufferSize, "\"success\":false,\"error\":\"OTA not available\"");
+ return CommandResult::ok();
+ }
+
+ const char* applyStr = nullptr;
+
+ for (uint8_t i = 0; i < paramCount; ++i)
+ {
+ if (strcmp(params[i].key, "apply") == 0)
+ {
+ applyStr = params[i].value;
+ break;
+ }
+ }
+
+ bool applyNow = applyStr && (applyStr[0] == '1');
+ _otaManager->triggerCheck(applyNow);
+
+ const char* stateStr = "idle";
+
+ switch (_otaManager->getState())
+ {
+ case OtaState::Idle:
+ stateStr = "triggered";
+ break;
+
+ case OtaState::Checking:
+ stateStr = "checking";
+ break;
+
+ case OtaState::UpdateAvailable:
+ stateStr = "available";
+ break;
+
+ case OtaState::Downloading:
+ stateStr = "downloading";
+ break;
+
+ case OtaState::Rebooting:
+ stateStr = "rebooting";
+ break;
+
+ case OtaState::Failed:
+ stateStr = "failed";
+ break;
+
+ case OtaState::UpToDate:
+ stateStr = "uptodate";
+ break;
+ }
+
+ snprintf(responseBuffer, bufferSize,
+ "\"success\":true,\"command\":\"%s\",\"v\":\"v%u.%u.%u.%u\",\"av\":\"%s\",\"s\":\"%s\"",
+ command,
+ FirmwareMajor, FirmwareMinor, FirmwarePatch, FirmwareBuild,
+ _otaManager->getAvailableVersion(),
+ stateStr);
+
+ return CommandResult::ok();
+ }
+ else if (SystemFunctions::commandMatches(command, SystemOtaStatus))
+ {
+ const char* stateStr = "disabled";
+ const char* avVersion = "";
+ char autoApply = '0';
+
+ if (_otaManager)
+ {
+ switch (_otaManager->getState())
+ {
+ case OtaState::Idle:
+ stateStr = "idle";
+ break;
+
+ case OtaState::Checking:
+ stateStr = "checking";
+ break;
+
+ case OtaState::UpdateAvailable:
+ stateStr = "available";
+ break;
+
+ case OtaState::Downloading:
+ stateStr = "downloading";
+ break;
+
+ case OtaState::Rebooting:
+ stateStr = "rebooting";
+ break;
+
+ case OtaState::Failed:
+ stateStr = "failed";
+ break;
+
+ case OtaState::UpToDate:
+ stateStr = "uptodate";
+ break;
+ }
+
+ avVersion = _otaManager->getAvailableVersion();
+
+ SystemHeader* hdr = ConfigManager::getHeaderPtr();
+ if (hdr && (hdr->reserved[0] & OtaFlagAutoApply))
+ autoApply = '1';
+ }
+
+ snprintf(responseBuffer, bufferSize,
+ "\"success\":true,\"command\":\"%s\",\"v\":\"v%u.%u.%u.%u\",\"av\":\"%s\",\"s\":\"%s\",\"auto\":\"%c\"",
+ command,
+ FirmwareMajor, FirmwareMinor, FirmwarePatch, FirmwareBuild,
+ avVersion,
+ stateStr,
+ autoApply);
+
+ return CommandResult::ok();
+ }
+#endif // OTA_AUTO_UPDATE
else
{
return CommandResult::error(InvalidCommandParameters);
diff --git a/PowerControlHub/SystemNetworkHandler.h b/PowerControlHub/SystemNetworkHandler.h
index ccf309a..6c27209 100644
--- a/PowerControlHub/SystemNetworkHandler.h
+++ b/PowerControlHub/SystemNetworkHandler.h
@@ -25,6 +25,9 @@
#include "SdCardLogger.h"
#endif
+#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
+class OtaManager;
+#endif
class SystemNetworkHandler : public INetworkCommandHandler
{
@@ -35,6 +38,10 @@ class SystemNetworkHandler : public INetworkCommandHandler
SdCardLogger* _sdCardLogger;
#endif
+#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
+ OtaManager* _otaManager{ nullptr };
+#endif
+
public:
explicit SystemNetworkHandler(WifiController* wifiController);
@@ -45,6 +52,13 @@ class SystemNetworkHandler : public INetworkCommandHandler
}
#endif
+#if defined(OTA_AUTO_UPDATE) && defined(ESP32) && defined(WIFI_SUPPORT)
+ void setOtaManager(OtaManager* otaManager)
+ {
+ _otaManager = otaManager;
+ }
+#endif
+
const char* getRoute() const override { return "/api/system"; }
void formatWifiStatusJson(IWifiClient* client) override;
diff --git a/PowerControlHubApp/Internal/Constants.cs b/PowerControlHubApp/Internal/Constants.cs
index 859d20a..e8c78c8 100644
--- a/PowerControlHubApp/Internal/Constants.cs
+++ b/PowerControlHubApp/Internal/Constants.cs
@@ -33,6 +33,10 @@ internal static class Constants
public const string MessageDeviceUnreachable = "Device unreachable";
public const string MessageToggleFailed = "Toggle failed — see log";
public const string LogOtaTrigger = "OTA: triggering firmware install…";
+ public const string OtaTriggerFailed = "OTA trigger failed";
+ public const string OtaDialogTitle = "Install Firmware";
+ public const string OtaDialogMessage = "An OTA update to {0} is available. Download and install now?";
+ public const string OtaDialogAccept = "Install";
public const int RelayCount = 8;
public const int UnconfiguredPin = 255;
// Dashboard poller log messages
@@ -195,6 +199,7 @@ internal static class Constants
public const string UserAgentValue = "PowerControlHub/1.0";
public const string PowerHubNotConfigured = "PowerHubService is not configured. Call Configure() first.";
public const string ErrorKey = "error";
+ public const char QuoteChar = '"';
public const string PreferenceKey = "app_theme";
public const string ThemeLight = "Light";
public const string ThemeDark = "Dark";
diff --git a/PowerControlHubApp/ViewModels/DashboardViewModel.cs b/PowerControlHubApp/ViewModels/DashboardViewModel.cs
index 17130b0..27b6d9e 100644
--- a/PowerControlHubApp/ViewModels/DashboardViewModel.cs
+++ b/PowerControlHubApp/ViewModels/DashboardViewModel.cs
@@ -1,6 +1,7 @@
using PowerControlHubApp.Models;
using PowerControlHubApp.Models.Json;
using PowerControlHubApp.Services;
+using Microsoft.Maui.Controls;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
@@ -36,6 +37,7 @@ public class DashboardViewModel : INotifyPropertyChanged
public ICommand RefreshCommand { get; }
public ICommand ToggleRelayCommand { get; }
public ICommand InstallUpdateCommand { get; }
+ public ICommand FirmwareLabelTappedCommand { get; }
/// True when the firmware reported an update is available.
public bool UpdateAvailable => _otaStatus?.UpdateAvailable == true;
@@ -90,7 +92,6 @@ private set
{
_systemTime = value;
OnPropertyChanged();
- OnPropertyChanged(nameof(SystemFirmwareAndTime));
}
}
@@ -102,7 +103,6 @@ private set
{
_systemFirmware = value;
OnPropertyChanged();
- OnPropertyChanged(nameof(SystemFirmwareAndTime));
}
}
@@ -116,14 +116,13 @@ private set
{
_systemUptime = value;
OnPropertyChanged();
- OnPropertyChanged(nameof(SystemFirmwareAndTime));
}
}
///
- /// Combined firmware and time display used by the status bar.
+ /// Colour for the firmware label in the status bar. Changes when update available.
///
- public string SystemFirmwareAndTime => $"FW: {SystemFirmware} • {SystemUptime}";
+ public Color SystemFirmwareColor => UpdateAvailable ? Color.FromArgb(ColorWarning) : Color.FromArgb(ColorAsHex1);
/// Label shown in the update banner.
public string OtaBannerLabel => _otaStatus?.BannerLabel ?? string.Empty;
@@ -212,6 +211,7 @@ public DashboardViewModel(PowerHubService service, LogService log, RelayStore re
RefreshCommand = new Command(async () => await RefreshAsync());
ToggleRelayCommand = new Command(async relay => await ToggleRelayAsync(relay));
InstallUpdateCommand = new Command(async () => await InstallUpdateAsync(), () => CanInstallUpdate);
+ FirmwareLabelTappedCommand = new Command(async () => await Shell.Current.GoToAsync(RouteSettingsPage));
_relayStore.Relays.CollectionChanged += (_, _) =>
{
@@ -393,6 +393,7 @@ private void NotifyOtaProperties()
OnPropertyChanged(nameof(OtaBannerLabel));
OnPropertyChanged(nameof(ShowOtaBanner));
OnPropertyChanged(nameof(OtaBannerColor));
+ OnPropertyChanged(nameof(SystemFirmwareColor));
OnPropertyChanged(nameof(CanInstallUpdate));
((Command)InstallUpdateCommand).ChangeCanExecute();
}
@@ -404,6 +405,7 @@ private void UpdateSystem(SystemModel system)
// Populate firmware and uptime so the status bar can show them
SystemFirmware = string.IsNullOrEmpty(system.Fw) ? DoubleDash : system.Fw;
SystemUptime = string.IsNullOrEmpty(system.Uptime) ? DoubleDash : system.Uptime;
+ OnPropertyChanged(nameof(SystemFirmwareColor));
}
private void UpdateRelays(IReadOnlyList incoming)
diff --git a/PowerControlHubApp/ViewModels/SettingsViewModel.cs b/PowerControlHubApp/ViewModels/SettingsViewModel.cs
index 5c58828..f274ff2 100644
--- a/PowerControlHubApp/ViewModels/SettingsViewModel.cs
+++ b/PowerControlHubApp/ViewModels/SettingsViewModel.cs
@@ -1,7 +1,8 @@
+using PowerControlHubApp.Models.Json;
+using PowerControlHubApp.Services;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
-using PowerControlHubApp.Services;
using static PowerControlHubApp.Internal.Constants;
namespace PowerControlHubApp.ViewModels;
@@ -17,14 +18,17 @@ public class SettingsViewModel : INotifyPropertyChanged
private string _pinsInUseText = string.Empty;
private bool _isPinsRefreshing;
private int _pinsCount;
+ private OtaStatusModel _otaStatus;
+ private bool _isCheckingOta;
public string IpAddress
{
get => _ipAddress;
- set
- { _ipAddress = value;
- OnPropertyChanged();
+ set
+ {
+ _ipAddress = value;
+ OnPropertyChanged();
}
}
@@ -32,10 +36,10 @@ public string Port
{
get => _port;
- set
- {
- _port = value;
- OnPropertyChanged();
+ set
+ {
+ _port = value;
+ OnPropertyChanged();
}
}
@@ -43,11 +47,11 @@ public string StatusMessage
{
get => _statusMessage;
- set
- {
- _statusMessage = value;
- OnPropertyChanged();
- OnPropertyChanged(nameof(HasStatusMessage));
+ set
+ {
+ _statusMessage = value;
+ OnPropertyChanged();
+ OnPropertyChanged(nameof(HasStatusMessage));
}
}
@@ -109,6 +113,8 @@ public bool IsPinsRefreshing
public ICommand SaveCommand { get; }
public ICommand RefreshPinsCommand { get; }
+ public ICommand CheckForUpdateCommand { get; }
+ public ICommand InstallFirmwareCommand { get; }
public SettingsViewModel(PowerHubService service, ThemeService themeService)
{
@@ -116,12 +122,94 @@ public SettingsViewModel(PowerHubService service, ThemeService themeService)
_themeService = themeService;
SaveCommand = new Command(Save);
RefreshPinsCommand = new Command(async () => await RefreshPinsAsync());
+ CheckForUpdateCommand = new Command(async () => await CheckForUpdateAsync());
+ InstallFirmwareCommand = new Command(async () => await InstallFirmwareAsync(), () => CanInstallFirmware);
IpAddress = Preferences.Get(KeyDeviceIpAddress, string.Empty);
Port = Preferences.Get(KeyDeviceIpPort, DefaultDeviceIpPort);
_selectedTheme = ThemeService.Current;
}
+ public string CurrentFirmwareVersion => _otaStatus?.CurrentVersion ?? DoubleDash;
+
+ public string AvailableFirmwareVersion => string.IsNullOrEmpty(_otaStatus?.AvailableVersion) ? DoubleDash : _otaStatus.AvailableVersion;
+
+ public bool IsUpdateAvailable => _otaStatus?.UpdateAvailable == true;
+
+ public bool IsOtaBusy => (_otaStatus?.IsBusy == true) || _isCheckingOta;
+
+ public bool CanInstallFirmware => _otaStatus?.UpdateAvailable == true && !_isCheckingOta;
+
+ public async Task CheckForUpdateAsync()
+ {
+ if (!_service.IsConfigured || _isCheckingOta)
+ return;
+
+ _isCheckingOta = true;
+ try
+ {
+ var ota = await _service.GetOtaStatusAsync();
+ _otaStatus = ota;
+ NotifyOtaProperties();
+ }
+ catch
+ {
+ StatusMessage = MessageDeviceUnreachable;
+ }
+ finally
+ {
+ _isCheckingOta = false;
+ OnPropertyChanged(nameof(IsOtaBusy));
+ ((Command)InstallFirmwareCommand).ChangeCanExecute();
+ }
+ }
+
+ public async Task InstallFirmwareAsync()
+ {
+ if (!CanInstallFirmware)
+ return;
+
+ string message = string.Format(OtaDialogMessage, AvailableFirmwareVersion);
+ bool confirmed = await Application.Current!.MainPage!.DisplayAlertAsync(
+ OtaDialogTitle,
+ message,
+ OtaDialogAccept,
+ MsgCancel);
+
+ if (!confirmed)
+ return;
+
+ try
+ {
+ var ok = await _service.TriggerOtaInstallAsync();
+
+ if (ok)
+ {
+ // give the device a moment then refresh status
+ await Task.Delay(TimeSpan.FromSeconds(SecondsTwo));
+ await CheckForUpdateAsync();
+ }
+ else
+ {
+ StatusMessage = OtaTriggerFailed;
+ }
+ }
+ catch
+ {
+ StatusMessage = OtaTriggerFailed;
+ }
+ }
+
+ private void NotifyOtaProperties()
+ {
+ OnPropertyChanged(nameof(CurrentFirmwareVersion));
+ OnPropertyChanged(nameof(AvailableFirmwareVersion));
+ OnPropertyChanged(nameof(IsUpdateAvailable));
+ OnPropertyChanged(nameof(IsOtaBusy));
+ OnPropertyChanged(nameof(CanInstallFirmware));
+ ((Command)InstallFirmwareCommand).ChangeCanExecute();
+ }
+
public async Task RefreshPinsAsync()
{
if (!_service.IsConfigured || _isPinsRefreshing)
diff --git a/PowerControlHubApp/Views/DashboardPage.xaml b/PowerControlHubApp/Views/DashboardPage.xaml
index 184ec07..22cd8a8 100644
--- a/PowerControlHubApp/Views/DashboardPage.xaml
+++ b/PowerControlHubApp/Views/DashboardPage.xaml
@@ -247,13 +247,18 @@
-
+
-
+
+
diff --git a/PowerControlHubApp/Views/SettingsPage.xaml b/PowerControlHubApp/Views/SettingsPage.xaml
index f035e12..c875a12 100644
--- a/PowerControlHubApp/Views/SettingsPage.xaml
+++ b/PowerControlHubApp/Views/SettingsPage.xaml
@@ -35,6 +35,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+