1mod settings;
14mod state;
15mod time;
16
17use chrono::SecondsFormat;
18use convert_case::{Case, Casing};
19use core::fmt;
20use futures::channel::oneshot;
21use serde_json::json;
22pub use settings::{RawNightLightSettings, ScheduleType};
23pub use state::{RawNightLightState, TransitionCause};
24use std::{
25 io,
26 ops::Sub,
27 path::Path,
28 thread,
29 time::{Duration, Instant, SystemTime},
30};
31pub use time::{ClockTime, ClockTimeFrame, Meridiem};
32use winreg::{
33 enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE},
34 RegKey,
35};
36
37use crate::{
38 data_conversion::{
39 format::write_table,
40 time::{
41 epoch_duration_to_filetime, utc_epoch_secs_to_local_iso_string,
42 utc_filetime_to_local_date_time, utc_filetime_to_local_iso_string,
43 },
44 ParseError, Strictness,
45 },
46 reg::{
47 delete_reg_value, export_reg_bin_values,
48 monitor::{MonitorLoopError, RegValueMonitor},
49 read_reg_bin_value, write_reg_bin_value, RegValuePath,
50 },
51};
52
53pub struct NightLight {
54 state: RawNightLightState,
55 settings: RawNightLightSettings,
56 sunset_to_sunrise_possible: Option<bool>,
57 uses_12_hour_clock: bool,
58 loaded_instant: Instant,
59 strictness: Strictness,
60}
61
62impl NightLight {
63 pub const MIN_NIGHT_COLOR_TEMP: u16 = 1200;
65 pub const MAX_NIGHT_COLOR_TEMP: u16 = 6500;
66
67 pub const WARMEST_NIGHT_COLOR_TEMP: u16 = Self::MIN_NIGHT_COLOR_TEMP;
69 pub const COLDEST_NIGHT_COLOR_TEMP: u16 = Self::MAX_NIGHT_COLOR_TEMP;
71
72 pub const DEFAULT_NIGHT_COLOR_TEMP: u16 = 4000;
74 pub const DEFAULT_WARMTH: f32 = 1.0
76 - (Self::DEFAULT_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
77 / (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32;
78
79 pub const REASONABLE_INIT_DELAY: Duration = Duration::from_millis(200);
81
82 pub const EXPIRATION_TIMEOUT: Duration = Duration::from_millis(1000);
84
85 pub fn from_reg() -> Result<Self, self::Error> {
86 Self::from_reg_with_strictness(Strictness::Strict)
89 }
90
91 pub fn from_reg_lenient() -> Result<Self, self::Error> {
92 Self::from_reg_with_strictness(Strictness::Lenient)
93 }
94
95 pub fn from_reg_with_strictness(strictness: Strictness) -> Result<Self, self::Error> {
96 Ok(match NightLightBytes::from_reg() {
99 Ok(bytes) => Self::from_bytes_with_strictness(bytes, strictness)?,
100 Err(error) => {
101 if error.kind() == io::ErrorKind::NotFound && strictness.is_lenient() {
102 Self::lenient_fallback()
103 } else {
104 Err(error)?
105 }
106 }
107 })
108 }
109
110 pub fn from_bytes(bytes: NightLightBytes) -> Result<Self, ParseError> {
111 Self::from_bytes_with_strictness(bytes, Strictness::Strict)
112 }
113
114 pub fn from_bytes_lenient(bytes: NightLightBytes) -> Result<Self, ParseError> {
115 Self::from_bytes_with_strictness(bytes, Strictness::Lenient)
116 }
117
118 pub fn from_bytes_with_strictness(
119 bytes: NightLightBytes,
120 strictness: Strictness,
121 ) -> Result<Self, ParseError> {
122 Ok(Self {
123 state: RawNightLightState::from_bytes(bytes.state, strictness)?,
124 settings: RawNightLightSettings::from_bytes(bytes.settings, strictness)?,
125 sunset_to_sunrise_possible: Self::sunset_to_sunrise_possible(),
126 uses_12_hour_clock: false,
127 loaded_instant: Instant::now(),
128 strictness,
129 })
130 }
131
132 pub fn lenient_fallback() -> Self {
133 let now = SystemTime::now();
134 Self {
135 state: RawNightLightState::lenient_fallback(now),
136 settings: RawNightLightSettings::lenient_fallback(now),
137 sunset_to_sunrise_possible: Self::sunset_to_sunrise_possible(),
138 uses_12_hour_clock: false,
139 loaded_instant: Instant::now(),
140 strictness: Strictness::Lenient,
141 }
142 }
143
144 pub fn init(delay: Duration, also_wait_after: bool) -> Result<(), self::Error> {
145 Self::init_with_strictness(delay, also_wait_after, Strictness::Strict)
148 }
149
150 pub fn init_with_strictness(
151 delay: Duration,
152 also_wait_after: bool,
153 strictness: Strictness,
154 ) -> Result<(), self::Error> {
155 let mut inst = Self::from_reg_with_strictness(strictness)?;
162 if !inst.night_preview_active() {
163 let previous_temp = if inst.active() {
165 None
166 } else {
167 let temp = inst.night_color_temp();
168 inst.set_night_color_temp(Some(Self::COLDEST_NIGHT_COLOR_TEMP)); temp
170 };
171 inst.set_night_preview_active(true);
172 inst.write_to_reg()?;
173
174 thread::sleep(delay);
175
176 let mut inst = Self::from_reg_with_strictness(strictness)?;
178 if let Some(temp) = previous_temp {
179 inst.set_night_color_temp(Some(temp));
180 };
181 inst.set_night_preview_active(false);
182 inst.write_to_reg()?;
183
184 if also_wait_after {
185 thread::sleep(delay);
186 }
187 }
188
189 Ok(())
190 }
191
192 pub fn export_reg<T: AsRef<Path>>(file_path: T) -> Result<(), io::Error> {
193 export_reg_bin_values(
196 &[
197 RawNightLightState::REG_VALUE_PATH,
198 RawNightLightSettings::REG_VALUE_PATH,
199 ],
200 file_path,
201 )
202 }
203
204 pub fn delete_reg() -> Result<(), io::Error> {
205 delete_reg_value(&RawNightLightSettings::REG_VALUE_PATH)?;
209 delete_reg_value(&RawNightLightState::REG_VALUE_PATH)?;
210
211 Ok(())
212 }
213
214 pub fn monitor<F, T, E>(
215 stop_receiver: Option<oneshot::Receiver<T>>,
216 mut callback: F,
217 ) -> Result<T, MonitorLoopError<E>>
218 where
219 F: FnMut(RegValueId) -> Option<Result<T, E>>,
220 T: Default,
221 {
222 let mut monitor = RegValueMonitor::new([
223 (RegValueId::State, &RawNightLightState::REG_VALUE_PATH),
224 (RegValueId::Settings, &RawNightLightSettings::REG_VALUE_PATH),
225 ])?;
226
227 monitor.r#loop(stop_receiver, |value_id| callback(value_id))
228 }
229
230 pub fn sunset_to_sunrise_possible() -> Option<bool> {
231 const SUBKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\location";
234 let reg_value_paths = [
235 RegValuePath {
237 hkey: HKEY_LOCAL_MACHINE,
238 subkey_path: SUBKEY_PATH,
239 value_name: "Value",
240 },
241 RegValuePath {
243 hkey: HKEY_CURRENT_USER,
244 subkey_path: SUBKEY_PATH,
245 value_name: "Value",
246 },
247 RegValuePath {
249 hkey: HKEY_CURRENT_USER,
250 subkey_path: &format!(r"{SUBKEY_PATH}\NonPackaged"),
251 value_name: "Value",
252 },
253 ];
254
255 for RegValuePath {
256 hkey,
257 subkey_path,
258 value_name,
259 } in reg_value_paths
260 {
261 if RegKey::predef(hkey)
262 .open_subkey_with_flags(subkey_path, KEY_QUERY_VALUE)
263 .ok()?
264 .get_value::<String, _>(value_name)
265 .ok()?
266 != "Allow"
267 {
268 return Some(false);
269 }
270 }
271
272 Some(true)
273 }
274
275 pub fn active(&self) -> bool {
276 *self.state.active
279 }
280
281 pub fn set_active(&mut self, active: bool) {
282 self.state.active.set(active);
283 }
284
285 pub fn transition_cause(&self) -> TransitionCause {
286 self.state.transition_cause
287 }
288
289 pub fn state_modified_filetime(&self) -> i64 {
290 self.state.modified_filetime
293 }
294
295 pub fn latest_possible_settings_modified_epoch_secs(&self) -> u32 {
296 self.settings.prologue_epoch_secs
299 }
300
301 pub fn schedule_active(&self) -> bool {
302 *self.settings.schedule_active
303 }
304
305 pub fn set_schedule_active(&mut self, schedule_active: bool) {
306 self.settings.schedule_active.set(schedule_active);
307 }
308
309 pub fn schedule_type(&self) -> ScheduleType {
310 *self.settings.schedule_type
313 }
314
315 pub fn effective_schedule_type(&self) -> Option<ScheduleType> {
316 match *self.settings.schedule_type {
319 ScheduleType::SunsetToSunrise => {
320 if self.sunset_to_sunrise_possible? {
321 Some(ScheduleType::SunsetToSunrise)
322 } else {
323 Some(ScheduleType::Explicit)
324 }
325 }
326 ScheduleType::Explicit => Some(ScheduleType::Explicit),
327 }
328 }
329
330 pub fn set_schedule_type(&mut self, schedule_type: ScheduleType) {
331 self.settings.schedule_type.set(schedule_type);
334 }
335
336 pub fn sunset_to_sunrise(&self) -> Option<ClockTimeFrame> {
337 self.settings.sunset_to_sunrise
340 }
341
342 pub fn scheduled_night(&self) -> ClockTimeFrame {
343 *self.settings.scheduled_night
346 }
347
348 pub fn set_scheduled_night(&mut self, scheduled_night: ClockTimeFrame) {
349 self.settings.scheduled_night.set(scheduled_night);
352 }
353
354 pub fn night_color_temp(&self) -> Option<u16> {
355 *self.settings.night_color_temp
358 }
359
360 pub fn night_color_temp_in_range(&self) -> Option<u16> {
361 self.settings
364 .night_color_temp
365 .map(|temp| temp.clamp(Self::MIN_NIGHT_COLOR_TEMP, Self::MAX_NIGHT_COLOR_TEMP))
366 }
367
368 pub fn set_night_color_temp(&mut self, night_color_temp: Option<u16>) {
369 self.settings.night_color_temp.set(night_color_temp);
372 }
373
374 pub fn warmth(&self) -> Option<f32> {
375 self.night_color_temp_in_range().map(|temp| {
378 1.0 - (temp - Self::MIN_NIGHT_COLOR_TEMP) as f32
379 / (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
380 })
381 }
382
383 pub fn set_warmth(&mut self, warmth: Option<f32>) {
384 self.set_night_color_temp(warmth.map(|warmth| {
390 if warmth.is_nan() {
391 panic!("value is NaN");
392 }
393
394 let precise_temp = (Self::MAX_NIGHT_COLOR_TEMP - Self::MIN_NIGHT_COLOR_TEMP) as f32
395 * (1.0 - warmth)
396 + Self::MIN_NIGHT_COLOR_TEMP as f32;
397 precise_temp.round().clamp(0f32, u16::MAX as f32) as u16
398 }));
399 }
400
401 pub fn night_preview_active(&self) -> bool {
402 *self.settings.night_preview_active
405 }
406
407 pub fn set_night_preview_active(&mut self, night_preview_active: bool) {
408 self.settings.night_preview_active.set(night_preview_active);
409 }
410
411 pub fn set_uses_12_hour_clock(&mut self, uses_12_hour_clock: bool) {
412 self.uses_12_hour_clock = uses_12_hour_clock;
415 }
416
417 pub fn to_json(&self) -> String {
418 serde_json::to_string_pretty(&json!({
419 "active": *self.state.active,
420 "transitionCause": format!("{:?}", self.state.transition_cause).to_case(Case::Camel),
421 "stateModifiedTimestamp": utc_filetime_to_local_iso_string(self.state.modified_filetime).expect("`FILETIME` should be valid"),
422
423 "latestPossibleSettingsModifiedTimestamp": utc_epoch_secs_to_local_iso_string(self.settings.prologue_epoch_secs).expect("epoch secs should be valid"),
424 "scheduleActive": *self.settings.schedule_active,
425 "scheduleType": format!("{:?}", *self.settings.schedule_type).to_case(Case::Camel),
426 "sunsetToSunrisePossible": self.sunset_to_sunrise_possible,
427 "effectiveScheduleType": self.effective_schedule_type().map(|r#type| format!("{:?}", r#type).to_case(Case::Camel)),
428 "sunsetToSunrise": self.settings.sunset_to_sunrise,
429 "scheduledNight": *self.settings.scheduled_night,
430 "nightColorTemp": *self.settings.night_color_temp,
431 "warmth": self.warmth(),
432 "nightPreviewActive": *self.settings.night_preview_active,
433 }))
434 .expect("serializing to JSON shouldn't fail")
435 }
436
437 pub fn write_to_reg(mut self) -> Result<(), self::Error> {
438 if self.loaded_instant.elapsed() > Self::EXPIRATION_TIMEOUT {
441 return Err(DataError::Expired.into());
442 }
443
444 let (state_changed, settings_changed) = self.verify_state_and_settings()?;
445
446 let state_bytes = state_changed.then(|| {
447 self.state.transition_cause = TransitionCause::Manual;
449
450 self.state.to_bytes()
451 });
452 let settings_bytes = settings_changed.then(|| self.settings.to_bytes());
453
454 if let Some(settings_bytes) = settings_bytes {
456 write_reg_bin_value(&RawNightLightSettings::REG_VALUE_PATH, &settings_bytes)?;
457 }
459 if let Some(state_bytes) = state_bytes {
460 write_reg_bin_value(&RawNightLightState::REG_VALUE_PATH, &state_bytes)?;
461 }
462
463 Ok(())
464 }
465
466 fn verify_state_and_settings(&mut self) -> Result<(bool, bool), DataError> {
467 let active_changed = self.state.active.changed();
470
471 let schedule_active_changed = self.settings.schedule_active.changed();
472 let schedule_type_changed = self.settings.schedule_type.changed();
473 let scheduled_night_changed = self.settings.scheduled_night.changed();
474 let night_color_temp_changed = self.settings.night_color_temp.changed();
475 let night_preview_active_changed = self.settings.night_preview_active.changed();
476
477 let state_changed = active_changed;
478 let settings_changed = schedule_active_changed
479 || schedule_type_changed
480 || scheduled_night_changed
481 || night_color_temp_changed
482 || night_preview_active_changed;
483 let state_changing_settings_changed =
484 schedule_active_changed || schedule_type_changed || scheduled_night_changed;
485
486 if state_changed && state_changing_settings_changed {
487 return Err(DataError::Irreconcilable(
488 CompetingProps::StateVsStateChangingSettings,
489 ));
490 }
491
492 if (*self.settings.night_preview_active || night_preview_active_changed)
493 && (state_changed || state_changing_settings_changed)
494 {
495 return Err(if night_preview_active_changed {
496 DataError::Irreconcilable(if state_changed {
497 CompetingProps::StateVsNightPreview
498 } else {
499 CompetingProps::StateChangingSettingsVsNightPreview
500 })
501 } else {
502 DataError::NightPreviewInProgress
504 });
505 }
506
507 Ok((state_changed, settings_changed))
508 }
509}
510
511impl fmt::Display for NightLight {
512 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513 let bool_to_yes_no = |flag| if flag { "yes" } else { "no" }.to_string();
514 let parenthesize_if = |flag, string| if flag { format!("({string})") } else { string };
515
516 let effective_schedule_type = self.effective_schedule_type();
517
518 write_table(
519 f,
520 &[
521 Some(("Active", bool_to_yes_no(*self.state.active))),
522 Some((
523 "Transition Cause",
524 format!("{:?}", self.state.transition_cause).to_case(Case::Lower),
525 )),
526 None,
527 Some((
528 "Warmth",
529 self.warmth()
530 .map(|warmth| format!("{warmth:.2}"))
531 .unwrap_or_else(|| format!("default (should be {})", Self::DEFAULT_WARMTH)),
532 )),
533 Some((
534 "Kelvin",
535 self.settings
536 .night_color_temp
537 .map(|temp| temp.to_string())
538 .unwrap_or_else(|| {
539 format!("default (should be {})", Self::DEFAULT_NIGHT_COLOR_TEMP)
540 }),
541 )),
542 Some((
543 "Preview Active",
544 bool_to_yes_no(*self.settings.night_preview_active),
545 )),
546 None,
547 Some((
548 "Schedule Active",
549 bool_to_yes_no(*self.settings.schedule_active),
550 )),
551 Some((
552 "Schedule Type (Effective)",
553 effective_schedule_type
554 .map(|r#type| format!("{:?}", r#type).to_case(Case::Lower))
555 .unwrap_or_else(|| "N/A".to_string()),
556 )),
557 Some((
558 "Sunset to Sunrise",
559 parenthesize_if(
560 effective_schedule_type == Some(ScheduleType::Explicit),
561 self.settings
562 .sunset_to_sunrise
563 .map(|frame| frame.format(self.uses_12_hour_clock))
564 .unwrap_or_else(|| "N/A".to_string()),
565 ),
566 )),
567 Some((
568 "Explicit Night",
569 parenthesize_if(
570 effective_schedule_type == Some(ScheduleType::SunsetToSunrise),
571 self.settings
572 .scheduled_night
573 .format(self.uses_12_hour_clock),
574 ),
575 )),
576 None,
577 Some((
578 "Modified (Latest Possible)",
579 utc_filetime_to_local_date_time(self.state.modified_filetime.max(
580 epoch_duration_to_filetime(Duration::from_secs(
581 self.settings.prologue_epoch_secs as _,
582 )),
583 ))
584 .ok_or(fmt::Error)?
585 .format(if self.uses_12_hour_clock {
586 "%Y-%m-%d, %I:%M:%S %P"
587 } else {
588 "%Y-%m-%d, %H:%M:%S"
589 })
590 .to_string(),
591 )),
592 ],
593 )?;
594
595 Ok(())
596 }
597}
598
599impl fmt::Debug for NightLight {
600 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
601 write_table(
602 f,
603 &[
604 Some((
605 "prologue timestamp (state)",
606 utc_epoch_secs_to_local_iso_string(self.state.prologue_epoch_secs)
607 .ok_or(fmt::Error)?,
608 )),
609 Some(("active (state)", self.state.active.to_string())),
610 Some((
611 "transition cause (state)",
612 format!("{:?}", self.state.transition_cause),
613 )),
614 Some((
615 "modified-`FILETIME` (state)",
616 utc_filetime_to_local_iso_string(self.state.modified_filetime)
617 .ok_or(fmt::Error)?,
618 )),
619 None,
620 Some((
621 "prologue timestamp (settings)",
622 utc_epoch_secs_to_local_iso_string(self.settings.prologue_epoch_secs)
623 .ok_or(fmt::Error)?,
624 )),
625 Some((
626 "schedule active (settings)",
627 self.settings.schedule_active.to_string(),
628 )),
629 Some((
630 "schedule type (settings)",
631 format!("{:?}", *self.settings.schedule_type),
632 )),
633 Some((
634 "sunset-to-sunrise possible (other)",
635 format!("{:?}", self.sunset_to_sunrise_possible),
636 )),
637 Some((
638 "effective schedule type (settings & other)",
639 format!("{:?}", self.effective_schedule_type()),
640 )),
641 Some((
642 "sunset to sunrise (settings)",
643 self.settings
644 .sunset_to_sunrise
645 .map(|frame| frame.format(self.uses_12_hour_clock))
646 .unwrap_or_else(|| format!("{:?}", None::<()>)),
647 )),
648 Some((
649 "scheduled night (settings)",
650 self.settings
651 .scheduled_night
652 .format(self.uses_12_hour_clock),
653 )),
654 Some((
655 "night color temp. (settings)",
656 format!("{:?}", *self.settings.night_color_temp),
657 )),
658 Some((
659 "warmth (settings, processed)",
660 format!("{:?}", self.warmth()),
661 )),
662 Some((
663 "night preview active (settings)",
664 self.settings.night_preview_active.to_string(),
665 )),
666 None,
667 Some((
668 "loaded-`Instant`",
669 chrono::Local::now()
670 .sub(self.loaded_instant.elapsed())
671 .to_rfc3339_opts(SecondsFormat::Millis, true),
672 )),
673 Some(("strictness", format!("{:?}", self.strictness))),
674 ],
675 )?;
676
677 Ok(())
678 }
679}
680
681#[derive(thiserror::Error, Debug)]
682pub enum Error {
683 #[error("IO error: {0}")]
685 IoError(#[from] io::Error),
686 #[error("parse error: {0}")]
688 ParseError(#[from] ParseError),
689 #[error("data error: {0}")]
691 DataError(#[from] DataError),
692}
693
694#[derive(thiserror::Error, Debug)]
695pub enum DataError {
696 #[error("object expired: duration between reading and writing was too long")]
698 Expired,
699 #[error("changed props are irreconcilable with other props: {0}")]
701 Irreconcilable(CompetingProps),
702 #[error("night preview was active while trying to change props irreconcilable with it")]
704 NightPreviewInProgress,
705}
706
707#[derive(Clone, Debug)]
708pub struct NightLightBytes {
709 pub state: Vec<u8>,
710 pub settings: Vec<u8>,
711}
712
713impl NightLightBytes {
714 pub fn from_reg() -> Result<Self, io::Error> {
715 Ok(Self {
716 state: read_reg_bin_value(&RawNightLightState::REG_VALUE_PATH)?,
717 settings: read_reg_bin_value(&RawNightLightSettings::REG_VALUE_PATH)?,
718 })
719 }
720
721 pub fn bytes_of_value(&self, reg_value_id: RegValueId) -> &[u8] {
722 match reg_value_id {
723 RegValueId::State => &*self.state,
724 RegValueId::Settings => &*self.settings,
725 }
726 }
727}
728
729#[derive(Debug)]
730pub enum CompetingProps {
731 StateVsStateChangingSettings,
732 StateVsNightPreview,
733 StateChangingSettingsVsNightPreview,
734}
735
736impl fmt::Display for CompetingProps {
737 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738 match self {
739 CompetingProps::StateVsStateChangingSettings => {
740 write!(f, "state vs. state-changing settings")
741 }
742 CompetingProps::StateVsNightPreview => write!(f, "state vs. night preview"),
743 CompetingProps::StateChangingSettingsVsNightPreview => {
744 write!(f, "state-changing settings vs. night preview")
745 }
746 }
747 }
748}
749
750#[derive(Clone, Copy, Debug)]
751pub enum RegValueId {
752 State,
753 Settings,
754}
755
756#[cfg(test)]
757mod tests {
758 use crate::cloud_store::night_light::NightLight;
759
760 #[ignore]
761 #[test]
762 fn playground() -> Result<(), super::Error> {
763 let mut night_light = NightLight::from_reg()?;
764
765 night_light.set_schedule_active(true);
768 night_light.write_to_reg()?;
780
781 Ok(())
782 }
783
784 #[test]
785 fn from_reg_strict() {
786 let result = NightLight::from_reg();
787 assert!(result.is_ok(), "{result:?}");
788 }
789
790 #[test]
791 fn from_reg_lenient() {
792 let result = NightLight::from_reg_lenient();
793 assert!(result.is_ok(), "{result:?}");
794 }
795
796 #[test]
797 fn compare_strict_with_lenient_reg_result() {
798 let strict_result = NightLight::from_reg();
799 let lenient_result = NightLight::from_reg_lenient();
800
801 if let (Ok(strict), Ok(lenient)) = (strict_result, lenient_result) {
802 assert!(strict.state == lenient.state, "unequal states");
803 assert!(strict.settings == lenient.settings, "unequal settings");
804 } else {
805 panic!("no `NightLight`");
806 }
807 }
808
809 #[test]
810 fn sunset_to_sunrise_possible_is_some() {
811 assert!(NightLight::sunset_to_sunrise_possible().is_some());
812 }
813
814 #[test]
815 fn verify_warmth_setter() -> Result<(), super::Error> {
816 let mut night_light = NightLight::from_reg()?;
817
818 assert_eq!(NightLight::WARMEST_NIGHT_COLOR_TEMP, 1200);
819 assert_eq!(NightLight::COLDEST_NIGHT_COLOR_TEMP, 6500);
820
821 night_light.set_warmth(Some(0.7));
822 assert_eq!(night_light.night_color_temp(), Some(2790));
823
824 Ok(())
825 }
826}