Ran black code formatter on everything.
[python_utils.git] / waitable_presence.py
1 #!/usr/bin/env python3
2
3 """A PresenceDetector that is waitable.  This is not part of
4 base_presence.py because I do not want to bring these dependencies
5 into that lower-level module (especially state_tracker).
6
7 """
8
9 import datetime
10 import logging
11 from typing import Optional, Tuple
12
13 from overrides import overrides
14
15 import base_presence
16 from type.locations import Location
17 import site_config
18 import state_tracker
19
20 logger = logging.getLogger(__name__)
21
22
23 class WaitablePresenceDetectorWithMemory(
24     state_tracker.WaitableAutomaticStateTracker
25 ):
26     """
27     This is a waitable class that keeps a PresenceDetector internally
28     and periodically polls it to detect changes in presence in a
29     particular location.  Example suggested usage pattern:
30
31         detector = waitable_presence.WaitablePresenceDetectorWithMemory(60.0)
32         while True:
33             changed = detector.wait(timeout=60 * 5)  # or, None for "forever"
34             (someone_is_home, since) = detector.is_someone_home()
35             if changed:
36                 detector.reset()
37             logger.debug(
38                 f'someone_is_home={someone_is_home}, since={since}, changed={changed}'
39             )
40     """
41
42     def __init__(
43         self,
44         override_update_interval_sec: float = 60.0,
45         override_location: Location = site_config.get_location(),
46     ) -> None:
47         self.last_someone_is_home: Optional[bool] = None
48         self.someone_is_home: Optional[bool] = None
49         self.everyone_gone_since: Optional[datetime.datetime] = None
50         self.someone_home_since: Optional[datetime.datetime] = None
51         self.location = override_location
52         self.detector: base_presence.PresenceDetection = (
53             base_presence.PresenceDetection()
54         )
55         super().__init__(
56             {
57                 'poll_presence': override_update_interval_sec,
58                 'check_detector': override_update_interval_sec * 5,
59             }
60         )
61
62     @overrides
63     def update(
64         self,
65         update_id: str,
66         now: datetime.datetime,
67         last_invocation: Optional[datetime.datetime],
68     ) -> None:
69         if update_id == 'poll_presence':
70             self.poll_presence(now)
71         elif update_id == 'check_detector':
72             self.check_detector()
73         else:
74             raise Exception(f'Unknown update type {update_id} in {__file__}')
75
76     def poll_presence(self, now: datetime.datetime) -> None:
77         logger.debug(f'Checking presence in {self.location} now...')
78         self.detector.update()
79         if self.detector.is_anyone_in_location_now(self.location):
80             self.someone_is_home = True
81             self.someone_home_since = now
82         else:
83             self.someone_is_home = False
84             self.everyone_gone_since = now
85         if self.someone_is_home != self.last_someone_is_home:
86             self.something_changed()
87             self.last_someone_is_home = self.someone_is_home
88
89     def check_detector(self) -> None:
90         if len(self.detector.dark_locations) > 0:
91             logger.debug(
92                 'PresenceDetector is incomplete; trying to reinitialize...'
93             )
94             self.detector = base_presence.PresenceDetection()
95
96     def is_someone_home(self) -> Tuple[bool, datetime.datetime]:
97         """Returns a tuple of a bool that indicates whether someone is home
98         and a datetime that indicates how long either someone has been
99         home or no one has been home.
100
101         """
102         if self.someone_is_home is None:
103             raise Exception("Too Soon!")
104         if self.someone_is_home:
105             return (True, self.someone_home_since)
106         else:
107             return (False, self.everyone_gone_since)