Various changes.
authorScott <[email protected]>
Sun, 5 Dec 2021 05:23:11 +0000 (21:23 -0800)
committerScott <[email protected]>
Sun, 5 Dec 2021 05:23:11 +0000 (21:23 -0800)
arper.py
exec_utils.py
executors.py
input_utils.py
presence.py
remote_worker.py
smart_home/cameras.py
smart_home/lights.py
string_utils.py
text_utils.py

index 4d6a3a2baa8900a4e6f5f535ad846766b2d353d7..2171e773088076aefc6f7d43c80fff59abb9b46f 100644 (file)
--- a/arper.py
+++ b/arper.py
@@ -33,7 +33,7 @@ cfg.add_argument(
 cfg.add_argument(
     '--arper_cache_max_staleness',
     type=argparse_utils.valid_duration,
-    default=datetime.timedelta(seconds=60 * 60),
+    default=datetime.timedelta(seconds=60 * 15),
     metavar='DURATION',
     help='Max acceptable age of the kernel arp table cache'
 )
@@ -56,19 +56,44 @@ class Arper(persistent.Persistent):
             self.state = cached_state
         else:
             logger.debug('No usable cached state; calling /usr/sbin/arp')
-            self.update()
+            self.update_from_arp_scan()
+            self.update_from_arp()
+        if len(self.state) < config.config['arper_min_entries_to_be_valid']:
+            raise Exception('Arper didn\'t find enough entries; only got {len(self.state)}.')
 
-    def update(self):
-        output = exec_utils.cmd(
-            '/usr/sbin/arp -a',
-            timeout_seconds=5.0
-        )
+    def update_from_arp_scan(self):
+        network_spec = site_config.get_config().network
+        try:
+            output = exec_utils.cmd(
+                f'/usr/local/bin/arp-scan --retry=6 --timeout 350 --backoff=1.4 --random --numeric --plain --ignoredups {network_spec}',
+                timeout_seconds=10.0
+            )
+        except Exception as e:
+            logger.exception(e)
+            return
+        for line in output.split('\n'):
+            ip = string_utils.extract_ip_v4(line)
+            mac = string_utils.extract_mac_address(line)
+            if ip is not None and mac is not None and mac != 'UNKNOWN' and ip != 'UNKNOWN':
+                mac = mac.lower()
+                logger.debug(f'ARPER: {mac} => {ip}')
+                self.state[mac] = ip
+
+    def update_from_arp(self):
+        try:
+            output = exec_utils.cmd(
+                '/usr/sbin/arp -a',
+                timeout_seconds=10.0
+            )
+        except Exception as e:
+            logger.exception(e)
+            return
         for line in output.split('\n'):
             ip = string_utils.extract_ip_v4(line)
             mac = string_utils.extract_mac_address(line)
-            if ip is not None and mac is not None:
+            if ip is not None and mac is not None and mac != 'UNKNOWN' and ip != 'UNKNOWN':
                 mac = mac.lower()
-                logger.debug(f'    {mac} => {ip}')
+                logger.debug(f'ARPER: {mac} => {ip}')
                 self.state[mac] = ip
 
     def get_ip_by_mac(self, mac: str) -> Optional[str]:
index 89cfbd7bf15f23a7a0effaed24954f7b4480aded..b52f52f0dc785033fc437a638c734d79cf8d5aa8 100644 (file)
@@ -10,6 +10,20 @@ from typing import List, Optional
 logger = logging.getLogger(__file__)
 
 
+def cmd_showing_output(command: str) -> None:
+    p = subprocess.Popen(
+        command,
+        shell=True,
+        bufsize=0,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+    )
+    for line in iter(p.stdout.readline, b''):
+        print(line.decode('utf-8'), end='')
+    p.stdout.close()
+    p.wait()
+
+
 def cmd_with_timeout(command: str, timeout_seconds: Optional[float]) -> int:
     """
     Run a command but do not let it run for more than timeout seconds.
index c11bd546cc3b1b0afcefff8c3215aab2707db147..92c5b3464154259fe4753702ebbd98fc84db11c9 100644 (file)
@@ -1113,7 +1113,7 @@ class DefaultExecutors(object):
                         username = 'scott',
                         machine = 'cheetah.house',
                         weight = 14,
-                        count = 4,
+                        count = 6,
                     ),
                 )
             if self.ping('video.house'):
@@ -1126,12 +1126,12 @@ class DefaultExecutors(object):
                         count = 4,
                     ),
                 )
-            if self.ping('wannabe.house'):
-                logger.info('Found wannabe.house')
+            if self.ping('gorilla.house'):
+                logger.info('Found gorilla.house')
                 pool.append(
                     RemoteWorkerRecord(
                         username = 'scott',
-                        machine = 'wannabe.house',
+                        machine = 'gorilla.house',
                         weight = 2,
                         count = 4,
                     ),
@@ -1156,6 +1156,16 @@ class DefaultExecutors(object):
                         count = 2,
                     ),
                 )
+            if self.ping('hero.house'):
+                logger.info('Found hero.house')
+                pool.append(
+                    RemoteWorkerRecord(
+                        username = 'scott',
+                        machine = 'hero.house',
+                        weight = 30,
+                        count = 10,
+                    ),
+                )
             if self.ping('puma.cabin'):
                 logger.info('Found puma.cabin')
                 pool.append(
@@ -1163,7 +1173,17 @@ class DefaultExecutors(object):
                         username = 'scott',
                         machine = 'puma.cabin',
                         weight = 12,
-                        count = 4,
+                        count = 6,
+                    ),
+                )
+            if self.ping('puma.house'):
+                logger.info('Found puma.house')
+                pool.append(
+                    RemoteWorkerRecord(
+                        username = 'scott',
+                        machine = 'puma.house',
+                        weight = 12,
+                        count = 6,
                     ),
                 )
 
index a989b2d6fe3db428d25a2de5b8baf6baef3c8734..e0b457d5a0e73ffc43d3d83ff09228328e6a641e 100644 (file)
@@ -2,6 +2,7 @@
 
 """Utilities related to user input."""
 
+import logging
 import signal
 import sys
 from typing import List
@@ -11,6 +12,9 @@ import readchar  # type: ignore
 import exceptions
 
 
+logger = logging.getLogger(__file__)
+
+
 def single_keystroke_response(
     valid_responses: List[str],
     *,
@@ -34,6 +38,7 @@ def single_keystroke_response(
         try:
             while True:
                 response = readchar.readchar()
+                logger.debug(f'Keystroke: {ord(response)}')
                 if response in valid_responses:
                     break
                 if ord(response) in os_special_keystrokes:
@@ -50,6 +55,9 @@ def single_keystroke_response(
         response = _single_keystroke_response_internal(
             valid_responses, timeout_seconds
         )
+        if ord(response) == 3:
+            raise KeyboardInterrupt('User pressed ^C in input_utils.')
+
     except exceptions.TimeoutError:
         if default_response is not None:
             response = default_response
index d7db41676a03cff60172ed2b25936f51f3b2cfa0..5fad457aa1251c88ab66b2b693dd25dc00fbbeab 100755 (executable)
@@ -5,6 +5,7 @@ from collections import defaultdict
 import enum
 import logging
 import re
+import sys
 from typing import Dict, List
 
 # Note: this module is fairly early loaded.  Be aware of dependencies.
@@ -81,16 +82,24 @@ class PresenceDetection(object):
         self.update()
 
     def update(self) -> None:
-        from exec_utils import cmd
+        from exec_utils import cmd_with_timeout
         try:
             persisted_macs = config.config['presence_macs_file']
         except KeyError:
             persisted_macs = '/home/scott/cron/persisted_mac_addresses.txt'
         self.read_persisted_macs_file(persisted_macs, Location.HOUSE)
-        raw = cmd(
-            "ssh [email protected] 'cat /home/scott/cron/persisted_mac_addresses.txt'"
-        )
-        self.parse_raw_macs_file(raw, Location.CABIN)
+        try:
+            raw = cmd_with_timeout(
+                "ssh [email protected] 'cat /home/scott/cron/persisted_mac_addresses.txt'",
+                timeout_seconds=10.0,
+            )
+            self.parse_raw_macs_file(raw, Location.CABIN)
+        except Exception as e:
+            logger.exception(e)
+            logger.error(
+                'Unable to fetch MAC Addresses from meerkat; can\'t do proper presence detection.'
+            )
+            sys.exit(1)
 
     def read_persisted_macs_file(
         self, filename: str, location: Location
index bf8de6c66a36767ac267cfdd2bffe38317cbace0..c04ac652449c5cb0e926ac35cc8fbaed7b05d7c4 100755 (executable)
@@ -83,7 +83,8 @@ def main() -> None:
     in_file = config.config['code_file']
     out_file = config.config['result_file']
 
-    (thread, stop_thread) = watch_for_cancel()
+    if config.config['watch_for_cancel']:
+        (thread, stop_thread) = watch_for_cancel()
 
     logger.debug(f'Reading {in_file}.')
     try:
index 40850a9fa1230013bde7782a1b23f26077b93dab..8137012bca0ea09ed2f76a35d8636ebdef8b9a28 100644 (file)
@@ -15,7 +15,7 @@ class BaseCamera(dev.Device):
         'outside_backyard_camera': 'backyard',
         'outside_driveway_camera': 'driveway',
         'outside_doorbell_camera': 'doorbell',
-        'outside_front_door_camera': 'frontdoor',
+        'outside_front_door_camera': 'front_door',
     }
 
     def __init__(self, name: str, mac: str, keywords: str = "") -> None:
@@ -24,4 +24,4 @@ class BaseCamera(dev.Device):
 
     def get_stream_url(self) -> str:
         assert self.camera_name is not None
-        return f'http://10.0.0.56:81/mjpg/{self.camera_name}/video.mjpg?h=1024&q=99'
+        return f'http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/mp4/GKlT2FfiSQ/{self.camera_name}/s.mp4'
index dd211eb13cdb23361cc17e9129bfcf613dbb154e..1c4081c750fe9fb3d6ea2a472fb66240b7299f55 100644 (file)
@@ -334,6 +334,7 @@ class TPLinkLight(BaseLight):
     def get_info(self) -> Optional[Dict]:
         cmd = self.get_cmdline() + "-c info"
         out = subprocess.getoutput(cmd)
+        logger.debug(f'RAW OUT> {out}')
         out = re.sub("Sent:.*\n", "", out)
         out = re.sub("Received: *", "", out)
         try:
index 9a38d25c49cccddceec4da06ee8bbfe8133749aa..aca4a5e3bfd9f49efa9a329b06addd9af5ffaa0a 100644 (file)
@@ -1503,12 +1503,16 @@ def from_bitstring(bits: str, encoding='utf-8', errors='surrogatepass') -> str:
     return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode(encoding, errors) or '\0'
 
 
-def ip_v4_sort_key(txt: str) -> str:
+def ip_v4_sort_key(txt: str) -> Tuple[int]:
     """Turn an IPv4 address into a tuple for sorting purposes.
 
     >>> ip_v4_sort_key('10.0.0.18')
     (10, 0, 0, 18)
 
+    >>> ips = ['10.0.0.10', '100.0.0.1', '1.2.3.4', '10.0.0.9']
+    >>> sorted(ips, key=lambda x: ip_v4_sort_key(x))
+    ['1.2.3.4', '10.0.0.9', '10.0.0.10', '100.0.0.1']
+
     """
     if not is_ip_v4(txt):
         print(f"not IP: {txt}")
@@ -1516,6 +1520,21 @@ def ip_v4_sort_key(txt: str) -> str:
     return tuple([int(x) for x in txt.split('.')])
 
 
+def path_ancestors_before_descendants_sort_key(volume: str) -> Tuple[str]:
+    """Chunk up a file path so that parent/ancestor paths sort before
+    children/descendant paths.
+
+    >>> path_ancestors_before_descendants_sort_key('/usr/local/bin')
+    ('usr', 'local', 'bin')
+
+    >>> paths = ['/usr/local', '/usr/local/bin', '/usr']
+    >>> sorted(paths, key=lambda x: path_ancestors_before_descendants_sort_key(x))
+    ['/usr', '/usr/local', '/usr/local/bin']
+
+    """
+    return tuple([x for x in volume.split('/') if len(x) > 0])
+
+
 if __name__ == '__main__':
     import doctest
     doctest.testmod()
index 36cfe2fd720d9e0cb479992495a70d66161ae132..9a9eb54702b25783abdc8100124b173790c00c5b 100644 (file)
@@ -3,6 +3,7 @@
 """Utilities for dealing with "text"."""
 
 from collections import defaultdict
+import logging
 import math
 import sys
 from typing import List, NamedTuple, Optional
@@ -10,6 +11,9 @@ from typing import List, NamedTuple, Optional
 from ansi import fg, reset
 
 
+logger = logging.getLogger(__file__)
+
+
 class RowsColumns(NamedTuple):
     rows: int
     columns: int
@@ -18,8 +22,15 @@ class RowsColumns(NamedTuple):
 def get_console_rows_columns() -> RowsColumns:
     """Returns the number of rows/columns on the current console."""
 
-    from exec_utils import cmd
-    rows, columns = cmd("stty size").split()
+    from exec_utils import cmd_with_timeout
+    try:
+        rows, columns = cmd_with_timeout(
+            "stty size",
+            timeout_seconds=5.0,
+        ).split()
+    except Exception as e:
+        logger.exception(e)
+        raise Exception('Can\'t determine console size?!')
     return RowsColumns(int(rows), int(columns))