Skip to content

Commit 4068d69

Browse files
sofarclaude
andcommitted
Add quiet hours (scheduled notification silencing)
Automatically switches notification status to Sleep at a configurable start hour and back at a configurable end hour. Includes a new settings screen with enable toggle and start/end hour controls. Disabled by default (21:00-09:00). Also adds moon glyph to the default UI font so it displays in the settings menu. This feature draws on extensive community discussion across several related PRs. The following feedback was reviewed and incorporated: Adopted: - "Quiet hours" naming (kieranc, PR #1461): renamed from "sleep setting" / "auto sleep" to avoid confusion with the existing sleep mode. - Preserve notification state across transitions (Itai-Nelken, PR #1461): quiet hours now saves the user's notification status (On/Off/Sleep) before entering and restores it when exiting, instead of unconditionally forcing On. Uses transient (non-persisted) state following the existing bleRadioEnabled/dfuAndFsEnabledTillReboot pattern. - Previous state was Sleep edge case (FintasticMan, PR #1461): if the previous state was already Sleep when quiet hours began, it is restored faithfully. FintasticMan suggested restoring to Off instead, but preserving the actual state is more predictable and consistent. - Alarm overrides quiet hours (FintasticMan, PR #1461): when an alarm fires, quiet hours are exited so the alarm can wake the user. This ensures alarms are never silenced by scheduled quiet hours. - Disable wrist-lower-to-sleep during sleep mode (kieranc, PR #2415, approved by NeroBurner): wrist-raise wake was already suppressed during sleep mode but wrist-lower-to-sleep was not, which is inconsistent. Moved the lower-wrist check inside the existing != Sleep guard per mark9064's code review suggestion to avoid a duplicate condition check. - Respect explicit user choices (chmeeedalf, PR #2002): if a user manually changes notification status via QuickSettings during quiet hours, that works normally; the original pre-quiet-hours state is still restored when quiet hours end. - Chimes suppressed during quiet hours: the existing chime handlers already gate on notificationStatus != Sleep, so setting Sleep during quiet hours suppresses chimes automatically with no additional code. Not adopted: - Separate auto-start/auto-stop toggles (Boteium, PR #1461): would let a user manually enter sleep early but still auto-wake. Adds UI complexity for a niche use case; a single toggle is simpler and aligns with the InfiniTime vision of "prefer solid defaults over customisability" (mark9064, PR #2230). - Sleep Bluetooth checkbox (escoand, PR #1461): BLE control during sleep is a separate security concern that deserves its own feature, not a quiet hours sub-option (mark9064, PR #2230). - Configurable sleep mode behaviors -- AOD, chimes, notifications, step tracking (JustScott, PR #2230): maintainer mark9064 noted that sleep mode means the user is sleeping, so allowing notifications/chimes/AOD contradicts its purpose. The author agreed this belongs in forks, not mainline. - Red/dim screen during sleep (minacode/lman0, PR #1261): a larger UX change outside the scope of notification scheduling. - Vibration priority system (minacode, PR #1328): a proper priority queue (phone > timer > alarm > notification) would be ideal for centralized DND management, but requires a motor controller rework that is a much larger effort. - 30-minute or 15-minute granularity for quiet hours times (LinuxinaBit, PR #1461; zischknall, PR #2227): hour granularity is sufficient for scheduling sleep/wake times and keeps the UI simple. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad73e1b commit 4068d69

File tree

9 files changed

+278
-6
lines changed

9 files changed

+278
-6
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ list(APPEND SOURCE_FILES
421421
displayapp/screens/settings/SettingShakeThreshold.cpp
422422
displayapp/screens/settings/SettingBluetooth.cpp
423423
displayapp/screens/settings/SettingOTA.cpp
424+
displayapp/screens/settings/SettingQuietHours.cpp
424425

425426
## Watch faces
426427
displayapp/screens/WatchFaceAnalog.cpp

src/components/settings/Settings.h

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,62 @@ namespace Pinetime {
351351
settings.heartRateBackgroundPeriod = newIntervalInSeconds.value();
352352
}
353353

354+
bool GetQuietHoursEnabled() const {
355+
return settings.quietHoursEnabled;
356+
}
357+
358+
void SetQuietHoursEnabled(bool enabled) {
359+
if (enabled != settings.quietHoursEnabled) {
360+
settingsChanged = true;
361+
}
362+
settings.quietHoursEnabled = enabled;
363+
}
364+
365+
uint8_t GetQuietHoursStart() const {
366+
return settings.quietHoursStart;
367+
}
368+
369+
void SetQuietHoursStart(uint8_t hour) {
370+
if (hour != settings.quietHoursStart) {
371+
settingsChanged = true;
372+
}
373+
settings.quietHoursStart = hour;
374+
}
375+
376+
uint8_t GetQuietHoursEnd() const {
377+
return settings.quietHoursEnd;
378+
}
379+
380+
void SetQuietHoursEnd(uint8_t hour) {
381+
if (hour != settings.quietHoursEnd) {
382+
settingsChanged = true;
383+
}
384+
settings.quietHoursEnd = hour;
385+
}
386+
387+
void EnterQuietHours() {
388+
if (!inQuietHours) {
389+
notificationStatusBeforeQuietHours = settings.notificationStatus;
390+
inQuietHours = true;
391+
SetNotificationStatus(Notification::Sleep);
392+
}
393+
}
394+
395+
void ExitQuietHours() {
396+
if (inQuietHours) {
397+
inQuietHours = false;
398+
SetNotificationStatus(notificationStatusBeforeQuietHours);
399+
}
400+
}
401+
402+
bool IsInQuietHours() const {
403+
return inQuietHours;
404+
}
405+
354406
private:
355407
Pinetime::Controllers::FS& fs;
356408

357-
static constexpr uint32_t settingsVersion = 0x000a;
409+
static constexpr uint32_t settingsVersion = 0x000b;
358410

359411
struct SettingsData {
360412
uint32_t version = settingsVersion;
@@ -383,6 +435,10 @@ namespace Pinetime {
383435

384436
bool dfuAndFsEnabledOnBoot = false;
385437
uint16_t heartRateBackgroundPeriod = std::numeric_limits<uint16_t>::max(); // Disabled by default
438+
439+
bool quietHoursEnabled = false;
440+
uint8_t quietHoursStart = 21; // 9 PM
441+
uint8_t quietHoursEnd = 9; // 9 AM
386442
};
387443

388444
SettingsData settings;
@@ -397,6 +453,9 @@ namespace Pinetime {
397453
bool bleRadioEnabled = true;
398454
bool dfuAndFsEnabledTillReboot = false;
399455

456+
Notification notificationStatusBeforeQuietHours = Notification::On;
457+
bool inQuietHours = false;
458+
400459
void LoadSettingsFromFile();
401460
void SaveSettingsToFile();
402461
};

src/displayapp/DisplayApp.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include "displayapp/screens/settings/SettingShakeThreshold.h"
5353
#include "displayapp/screens/settings/SettingBluetooth.h"
5454
#include "displayapp/screens/settings/SettingOTA.h"
55+
#include "displayapp/screens/settings/SettingQuietHours.h"
5556

5657
#include "utility/Math.h"
5758

@@ -634,6 +635,9 @@ void DisplayApp::LoadScreen(Apps app, DisplayApp::FullRefreshDirections directio
634635
case Apps::SettingOTA:
635636
currentScreen = std::make_unique<Screens::SettingOTA>(this, settingsController);
636637
break;
638+
case Apps::SettingQuietHours:
639+
currentScreen = std::make_unique<Screens::SettingQuietHours>(settingsController);
640+
break;
637641
case Apps::BatteryInfo:
638642
currentScreen = std::make_unique<Screens::BatteryInfo>(batteryController);
639643
break;

src/displayapp/apps/Apps.h.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ namespace Pinetime {
4545
SettingShakeThreshold,
4646
SettingBluetooth,
4747
SettingOTA,
48+
SettingQuietHours,
4849
Error
4950
};
5051

src/displayapp/fonts/fonts.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
{
99
"file": "FontAwesome5-Solid+Brands+Regular.woff",
10-
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed"
10+
"range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf186, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a, 0xf3ed"
1111
}
1212
],
1313
"bpp": 1,
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#include "displayapp/screens/settings/SettingQuietHours.h"
2+
#include <lvgl/lvgl.h>
3+
#include "displayapp/DisplayApp.h"
4+
#include "displayapp/screens/Symbols.h"
5+
#include "displayapp/InfiniTimeTheme.h"
6+
7+
using namespace Pinetime::Applications::Screens;
8+
9+
namespace {
10+
void event_handler(lv_obj_t* obj, lv_event_t event) {
11+
auto* screen = static_cast<SettingQuietHours*>(obj->user_data);
12+
screen->UpdateSelected(obj, event);
13+
}
14+
15+
void checkbox_event_handler(lv_obj_t* obj, lv_event_t event) {
16+
if (event == LV_EVENT_VALUE_CHANGED) {
17+
auto* screen = static_cast<SettingQuietHours*>(obj->user_data);
18+
screen->ToggleEnabled();
19+
}
20+
}
21+
}
22+
23+
SettingQuietHours::SettingQuietHours(Pinetime::Controllers::Settings& settingsController) : settingsController {settingsController} {
24+
25+
lv_obj_t* title = lv_label_create(lv_scr_act(), nullptr);
26+
lv_label_set_text_static(title, "Quiet hours");
27+
lv_label_set_align(title, LV_LABEL_ALIGN_CENTER);
28+
lv_obj_align(title, lv_scr_act(), LV_ALIGN_IN_TOP_MID, 15, 15);
29+
30+
lv_obj_t* icon = lv_label_create(lv_scr_act(), nullptr);
31+
lv_obj_set_style_local_text_color(icon, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_ORANGE);
32+
lv_label_set_text_static(icon, Symbols::moon);
33+
lv_label_set_align(icon, LV_LABEL_ALIGN_CENTER);
34+
lv_obj_align(icon, title, LV_ALIGN_OUT_LEFT_MID, -10, 0);
35+
36+
enabledCheckbox = lv_checkbox_create(lv_scr_act(), nullptr);
37+
lv_checkbox_set_text(enabledCheckbox, "Enabled");
38+
lv_checkbox_set_checked(enabledCheckbox, settingsController.GetQuietHoursEnabled());
39+
enabledCheckbox->user_data = this;
40+
lv_obj_set_event_cb(enabledCheckbox, checkbox_event_handler);
41+
lv_obj_align(enabledCheckbox, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 10, 55);
42+
43+
static constexpr uint8_t btnWidth = 50;
44+
static constexpr uint8_t btnHeight = 40;
45+
46+
// Start hour row
47+
lv_obj_t* startLabel = lv_label_create(lv_scr_act(), nullptr);
48+
lv_label_set_text_static(startLabel, "Start");
49+
lv_obj_align(startLabel, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 10, -15);
50+
51+
btnStartMinus = lv_btn_create(lv_scr_act(), nullptr);
52+
btnStartMinus->user_data = this;
53+
lv_obj_set_size(btnStartMinus, btnWidth, btnHeight);
54+
lv_obj_align(btnStartMinus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 70, -15);
55+
lv_obj_set_style_local_bg_color(btnStartMinus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
56+
lv_obj_t* lblStartMinus = lv_label_create(btnStartMinus, nullptr);
57+
lv_label_set_text_static(lblStartMinus, "-");
58+
lv_obj_set_event_cb(btnStartMinus, event_handler);
59+
60+
startValue = lv_label_create(lv_scr_act(), nullptr);
61+
lv_obj_set_style_local_text_font(startValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
62+
lv_label_set_text_fmt(startValue, "%02d:00", settingsController.GetQuietHoursStart());
63+
lv_obj_align(startValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 132, -15);
64+
65+
btnStartPlus = lv_btn_create(lv_scr_act(), nullptr);
66+
btnStartPlus->user_data = this;
67+
lv_obj_set_size(btnStartPlus, btnWidth, btnHeight);
68+
lv_obj_align(btnStartPlus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 195, -15);
69+
lv_obj_set_style_local_bg_color(btnStartPlus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
70+
lv_obj_t* lblStartPlus = lv_label_create(btnStartPlus, nullptr);
71+
lv_label_set_text_static(lblStartPlus, "+");
72+
lv_obj_set_event_cb(btnStartPlus, event_handler);
73+
74+
// End hour row
75+
lv_obj_t* endLabel = lv_label_create(lv_scr_act(), nullptr);
76+
lv_label_set_text_static(endLabel, "End");
77+
lv_obj_align(endLabel, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 10, 40);
78+
79+
btnEndMinus = lv_btn_create(lv_scr_act(), nullptr);
80+
btnEndMinus->user_data = this;
81+
lv_obj_set_size(btnEndMinus, btnWidth, btnHeight);
82+
lv_obj_align(btnEndMinus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 70, 40);
83+
lv_obj_set_style_local_bg_color(btnEndMinus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
84+
lv_obj_t* lblEndMinus = lv_label_create(btnEndMinus, nullptr);
85+
lv_label_set_text_static(lblEndMinus, "-");
86+
lv_obj_set_event_cb(btnEndMinus, event_handler);
87+
88+
endValue = lv_label_create(lv_scr_act(), nullptr);
89+
lv_obj_set_style_local_text_font(endValue, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, &jetbrains_mono_bold_20);
90+
lv_label_set_text_fmt(endValue, "%02d:00", settingsController.GetQuietHoursEnd());
91+
lv_obj_align(endValue, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 132, 40);
92+
93+
btnEndPlus = lv_btn_create(lv_scr_act(), nullptr);
94+
btnEndPlus->user_data = this;
95+
lv_obj_set_size(btnEndPlus, btnWidth, btnHeight);
96+
lv_obj_align(btnEndPlus, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 195, 40);
97+
lv_obj_set_style_local_bg_color(btnEndPlus, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, Colors::bgAlt);
98+
lv_obj_t* lblEndPlus = lv_label_create(btnEndPlus, nullptr);
99+
lv_label_set_text_static(lblEndPlus, "+");
100+
lv_obj_set_event_cb(btnEndPlus, event_handler);
101+
}
102+
103+
SettingQuietHours::~SettingQuietHours() {
104+
lv_obj_clean(lv_scr_act());
105+
settingsController.SaveSettings();
106+
}
107+
108+
void SettingQuietHours::ToggleEnabled() {
109+
bool wasEnabled = settingsController.GetQuietHoursEnabled();
110+
settingsController.SetQuietHoursEnabled(!wasEnabled);
111+
if (wasEnabled && settingsController.IsInQuietHours()) {
112+
settingsController.ExitQuietHours();
113+
}
114+
lv_checkbox_set_checked(enabledCheckbox, settingsController.GetQuietHoursEnabled());
115+
}
116+
117+
void SettingQuietHours::UpdateSelected(lv_obj_t* object, lv_event_t event) {
118+
if (event != LV_EVENT_SHORT_CLICKED && event != LV_EVENT_LONG_PRESSED_REPEAT) {
119+
return;
120+
}
121+
122+
if (object == btnStartPlus) {
123+
uint8_t val = settingsController.GetQuietHoursStart();
124+
val = (val + 1) % 24;
125+
settingsController.SetQuietHoursStart(val);
126+
lv_label_set_text_fmt(startValue, "%02d:00", val);
127+
lv_obj_realign(startValue);
128+
} else if (object == btnStartMinus) {
129+
uint8_t val = settingsController.GetQuietHoursStart();
130+
val = (val + 23) % 24;
131+
settingsController.SetQuietHoursStart(val);
132+
lv_label_set_text_fmt(startValue, "%02d:00", val);
133+
lv_obj_realign(startValue);
134+
} else if (object == btnEndPlus) {
135+
uint8_t val = settingsController.GetQuietHoursEnd();
136+
val = (val + 1) % 24;
137+
settingsController.SetQuietHoursEnd(val);
138+
lv_label_set_text_fmt(endValue, "%02d:00", val);
139+
lv_obj_realign(endValue);
140+
} else if (object == btnEndMinus) {
141+
uint8_t val = settingsController.GetQuietHoursEnd();
142+
val = (val + 23) % 24;
143+
settingsController.SetQuietHoursEnd(val);
144+
lv_label_set_text_fmt(endValue, "%02d:00", val);
145+
lv_obj_realign(endValue);
146+
}
147+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <lvgl/lvgl.h>
5+
#include "components/settings/Settings.h"
6+
#include "displayapp/screens/Screen.h"
7+
8+
namespace Pinetime {
9+
10+
namespace Applications {
11+
namespace Screens {
12+
13+
class SettingQuietHours : public Screen {
14+
public:
15+
SettingQuietHours(Pinetime::Controllers::Settings& settingsController);
16+
~SettingQuietHours() override;
17+
18+
void UpdateSelected(lv_obj_t* object, lv_event_t event);
19+
void ToggleEnabled();
20+
21+
private:
22+
Controllers::Settings& settingsController;
23+
24+
lv_obj_t* enabledCheckbox;
25+
lv_obj_t* startValue;
26+
lv_obj_t* endValue;
27+
lv_obj_t* btnStartPlus;
28+
lv_obj_t* btnStartMinus;
29+
lv_obj_t* btnEndPlus;
30+
lv_obj_t* btnEndMinus;
31+
};
32+
}
33+
}
34+
}

src/displayapp/screens/settings/Settings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ namespace Pinetime {
4949

5050
{Symbols::shieldAlt, "Over-the-air", Apps::SettingOTA},
5151
{Symbols::bluetooth, "Bluetooth", Apps::SettingBluetooth},
52+
{Symbols::moon, "Quiet hours", Apps::SettingQuietHours},
5253
{Symbols::list, "About", Apps::SysInfo},
5354
}};
5455
ScreenList<nScreens> screens;

src/systemtask/SystemTask.cpp

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,22 @@ void SystemTask::Work() {
222222
if (alarmController.IsEnabled()) {
223223
alarmController.ScheduleAlarm();
224224
}
225+
if (settingsController.GetQuietHoursEnabled()) {
226+
uint8_t currentHour = dateTimeController.Hours();
227+
uint8_t start = settingsController.GetQuietHoursStart();
228+
uint8_t end = settingsController.GetQuietHoursEnd();
229+
bool shouldBeInQuietHours;
230+
if (start <= end) {
231+
shouldBeInQuietHours = (currentHour >= start && currentHour < end);
232+
} else {
233+
shouldBeInQuietHours = (currentHour >= start || currentHour < end);
234+
}
235+
if (shouldBeInQuietHours) {
236+
settingsController.EnterQuietHours();
237+
} else {
238+
settingsController.ExitQuietHours();
239+
}
240+
}
225241
break;
226242
case Messages::OnNewNotification:
227243
if (settingsController.GetNotificationStatus() == Pinetime::Controllers::Settings::Notification::On) {
@@ -232,6 +248,7 @@ void SystemTask::Work() {
232248
}
233249
break;
234250
case Messages::SetOffAlarm:
251+
settingsController.ExitQuietHours();
235252
GoToRunning();
236253
displayApp.PushMessage(Pinetime::Applications::Display::Messages::AlarmTriggered);
237254
break;
@@ -343,6 +360,14 @@ void SystemTask::Work() {
343360
GoToRunning();
344361
displayApp.PushMessage(Pinetime::Applications::Display::Messages::Chime);
345362
}
363+
if (settingsController.GetQuietHoursEnabled()) {
364+
uint8_t currentHour = dateTimeController.Hours();
365+
if (currentHour == settingsController.GetQuietHoursStart()) {
366+
settingsController.EnterQuietHours();
367+
} else if (currentHour == settingsController.GetQuietHoursEnd()) {
368+
settingsController.ExitQuietHours();
369+
}
370+
}
346371
break;
347372
case Messages::OnNewHalfHour:
348373
using Pinetime::Controllers::AlarmController;
@@ -462,10 +487,10 @@ void SystemTask::UpdateMotion() {
462487
motionController.CurrentShakeSpeed() > settingsController.GetShakeThreshold())) {
463488
GoToRunning();
464489
}
465-
}
466-
if (settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::LowerWrist) && state == SystemTaskState::Running &&
467-
motionController.ShouldLowerSleep()) {
468-
GoToSleep();
490+
if (settingsController.isWakeUpModeOn(Pinetime::Controllers::Settings::WakeUpMode::LowerWrist) && state == SystemTaskState::Running &&
491+
motionController.ShouldLowerSleep()) {
492+
GoToSleep();
493+
}
469494
}
470495
}
471496

0 commit comments

Comments
 (0)