summaryrefslogtreecommitdiff
path: root/smart_home
diff options
context:
space:
mode:
Diffstat (limited to 'smart_home')
-rw-r--r--smart_home/chromecasts.py88
-rw-r--r--smart_home/device.py13
-rw-r--r--smart_home/lights.py73
-rw-r--r--smart_home/registry.py (renamed from smart_home/config.py)96
4 files changed, 204 insertions, 66 deletions
diff --git a/smart_home/chromecasts.py b/smart_home/chromecasts.py
new file mode 100644
index 0000000..08290e5
--- /dev/null
+++ b/smart_home/chromecasts.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+
+"""Utilities for dealing with the webcams."""
+
+import logging
+import time
+
+import pychromecast
+
+from decorator_utils import memoized
+import smart_home.device as dev
+
+logger = logging.getLogger(__name__)
+
+
+class BaseChromecast(dev.Device):
+ def __init__(self, name: str, mac: str, keywords: str = "") -> None:
+ super().__init__(name.strip(), mac.strip(), keywords)
+ ip = self.get_ip()
+ self.cast = pychromecast.Chromecast(ip)
+ self.cast.wait()
+ time.sleep(0.1)
+
+ def is_idle(self):
+ return self.cast.is_idle
+
+ @memoized
+ def get_uuid(self):
+ return self.cast.uuid
+
+ @memoized
+ def get_friendly_name(self):
+ return self.cast.name
+
+ def get_uri(self):
+ return self.cast.url
+
+ @memoized
+ def get_model_name(self):
+ return self.cast.model_name
+
+ @memoized
+ def get_cast_type(self):
+ return self.cast.cast_type
+
+ @memoized
+ def app_id(self):
+ return self.cast.app_id
+
+ def get_app_display_name(self):
+ return self.cast.app_display_name
+
+ def get_media_controller(self):
+ return self.cast.media_controller
+
+ def status(self):
+ if self.is_idle():
+ return 'idle'
+ app = self.get_app_display_name()
+ mc = self.get_media_controller()
+ status = mc.status
+ return f'{app} / {status.title}'
+
+ def start_app(self, app_id, force_launch=False):
+ """Start an app on the Chromecast."""
+ self.cast.start_app(app_id, force_launch)
+
+ def quit_app(self):
+ """Tells the Chromecast to quit current app_id."""
+ self.cast.quit_app()
+
+ def volume_up(self, delta=0.1):
+ """Increment volume by 0.1 (or delta) unless it is already maxed.
+ Returns the new volume.
+ """
+ return self.cast.volume_up(delta)
+
+ def volume_down(self, delta=0.1):
+ """Decrement the volume by 0.1 (or delta) unless it is already 0.
+ Returns the new volume.
+ """
+ return self.cast.volume_down(delta)
+
+ def __repr__(self):
+ return (
+ f"Chromecast({self.cast.socket_client.host!r}, port={self.cast.socket_client.port!r}, "
+ f"device={self.cast.device!r})"
+ )
diff --git a/smart_home/device.py b/smart_home/device.py
index 04b0bfe..9675b7c 100644
--- a/smart_home/device.py
+++ b/smart_home/device.py
@@ -28,9 +28,20 @@ class Device(object):
def get_mac(self) -> str:
return self.mac
- def get_ip(self) -> str:
+ def get_ip(self) -> Optional[str]:
return self.arper.get_ip_by_mac(self.mac)
+ def has_static_ip(self) -> bool:
+ for kw in self.kws:
+ m = re.search(r'static:([\d\.]+)', kw)
+ if m is not None:
+ ip = m.group(1)
+ assert self.get_ip() == ip
+ return True
+ return False
+
+ # Add command -> URL logic here.
+
def get_keywords(self) -> Optional[List[str]]:
return self.kws
diff --git a/smart_home/lights.py b/smart_home/lights.py
index 5446722..76b1500 100644
--- a/smart_home/lights.py
+++ b/smart_home/lights.py
@@ -10,11 +10,14 @@ import os
import re
import subprocess
import sys
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
+from overrides import overrides
import tinytuya as tt
+import ansi
import argparse_utils
+import arper
import config
import logging_utils
import smart_home.device as dev
@@ -23,11 +26,11 @@ from decorator_utils import timeout, memoized
logger = logging.getLogger(__name__)
-parser = config.add_commandline_args(
+args = config.add_commandline_args(
f"Smart Lights ({__file__})",
"Args related to smart lights.",
)
-parser.add_argument(
+args.add_argument(
'--smart_lights_tplink_location',
default='/home/scott/bin/tplink.py',
metavar='FILENAME',
@@ -60,6 +63,20 @@ class BaseLight(dev.Device):
def __init__(self, name: str, mac: str, keywords: str = "") -> None:
super().__init__(name.strip(), mac.strip(), keywords)
+ @staticmethod
+ def parse_color_string(color: str) -> Optional[Tuple[int, int, int]]:
+ m = re.match(
+ 'r#?([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])',
+ color
+ )
+ if m is not None and len(m.group) == 3:
+ red = int(m.group(0), 16)
+ green = int(m.group(1), 16)
+ blue = int(m.group(2), 16)
+ return (red, green, blue)
+ color = color.lower()
+ return ansi.COLOR_NAMES_TO_RGB.get(color, None)
+
@abstractmethod
def turn_on(self) -> bool:
pass
@@ -101,25 +118,30 @@ class GoogleLight(BaseLight):
def parse_google_response(response: GoogleResponse) -> bool:
return response.success
+ @overrides
def turn_on(self) -> bool:
return GoogleLight.parse_google_response(
ask_google(f"turn {self.goog_name()} on")
)
+ @overrides
def turn_off(self) -> bool:
return GoogleLight.parse_google_response(
ask_google(f"turn {self.goog_name()} off")
)
+ @overrides
def is_on(self) -> bool:
r = ask_google(f"is {self.goog_name()} on?")
if not r.success:
return False
return 'is on' in r.audio_transcription
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
+ @overrides
def get_dimmer_level(self) -> Optional[int]:
if not self.has_keyword("dimmer"):
return False
@@ -136,6 +158,7 @@ class GoogleLight(BaseLight):
return 0
return None
+ @overrides
def set_dimmer_level(self, level: int) -> bool:
if not self.has_keyword("dimmer"):
return False
@@ -149,6 +172,7 @@ class GoogleLight(BaseLight):
return True
return False
+ @overrides
def make_color(self, color: str) -> bool:
return GoogleLight.parse_google_response(
ask_google(f"make {self.goog_name()} {color}")
@@ -174,50 +198,55 @@ class TuyaLight(BaseLight):
}
def __init__(self, name: str, mac: str, keywords: str = "") -> None:
- from subprocess import Popen, PIPE
super().__init__(name, mac, keywords)
mac = mac.upper()
if mac not in TuyaLight.ids_by_mac or mac not in TuyaLight.keys_by_mac:
raise Exception(f'{mac} is unknown; add it to ids_by_mac and keys_by_mac')
self.devid = TuyaLight.ids_by_mac[mac]
self.key = TuyaLight.keys_by_mac[mac]
- try:
- pid = Popen(['maclookup', mac], stdout=PIPE)
- ip = pid.communicate()[0]
- ip = ip[:-1]
- except Exception:
- ip = '0.0.0.0'
+ self.arper = arper.Arper()
+ ip = self.get_ip()
self.bulb = tt.BulbDevice(self.devid, ip, local_key=self.key)
+ def get_status(self) -> Dict[str, Any]:
+ return self.bulb.status()
+
+ @overrides
def turn_on(self) -> bool:
self.bulb.turn_on()
return True
+ @overrides
def turn_off(self) -> bool:
self.bulb.turn_off()
return True
- def get_status(self) -> Dict[str, Any]:
- return self.bulb.status()
-
+ @overrides
def is_on(self) -> bool:
s = self.get_status()
return s['dps']['1']
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
+ @overrides
def get_dimmer_level(self) -> Optional[int]:
s = self.get_status()
return s['dps']['3']
+ @overrides
def set_dimmer_level(self, level: int) -> bool:
self.bulb.set_brightness(level)
return True
+ @overrides
def make_color(self, color: str) -> bool:
- self.bulb.set_colour(255,0,0)
- return True
+ rgb = BaseLight.parse_color_string(color)
+ if rgb is not None:
+ self.bulb.set_colour(rgb[0], rgb[1], rgb[2])
+ return True
+ return False
class TPLinkLight(BaseLight):
@@ -260,18 +289,23 @@ class TPLinkLight(BaseLight):
logger.debug(f'About to execute {cmd}')
return tplink_light_command(cmd)
+ @overrides
def turn_on(self, child: str = None) -> bool:
return self.command("on", child)
+ @overrides
def turn_off(self, child: str = None) -> bool:
return self.command("off", child)
+ @overrides
def is_on(self) -> bool:
return self.get_on_duration_seconds() > 0
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
+ @overrides
def make_color(self, color: str) -> bool:
raise NotImplementedError
@@ -308,13 +342,7 @@ class TPLinkLight(BaseLight):
return int(chi.get("on_time", "0"))
return 0
- def get_on_limit_seconds(self) -> Optional[int]:
- for kw in self.kws:
- m = re.search(r"timeout:(\d+)", kw)
- if m is not None:
- return int(m.group(1)) * 60
- return None
-
+ @overrides
def get_dimmer_level(self) -> Optional[int]:
if not self.has_keyword("dimmer"):
return False
@@ -323,6 +351,7 @@ class TPLinkLight(BaseLight):
return None
return int(self.info.get("brightness", "0"))
+ @overrides
def set_dimmer_level(self, level: int) -> bool:
if not self.has_keyword("dimmer"):
return False
diff --git a/smart_home/config.py b/smart_home/registry.py
index a28caa7..2d23981 100644
--- a/smart_home/config.py
+++ b/smart_home/registry.py
@@ -10,15 +10,16 @@ import file_utils
import logical_search
import smart_home.device as device
import smart_home.cameras as cameras
+import smart_home.chromecasts as chromecasts
import smart_home.lights as lights
import smart_home.outlets as outlets
-parser = config.add_commandline_args(
- f"Smart Home Config ({__file__})",
- "Args related to the smart home config."
+args = config.add_commandline_args(
+ f"Smart Home Registry ({__file__})",
+ "Args related to the smart home configuration registry."
)
-parser.add_argument(
- '--smart_home_config_file_location',
+args.add_argument(
+ '--smart_home_registry_file_location',
default='/home/scott/bin/network_mac_addresses.txt',
metavar='FILENAME',
help='The location of network_mac_addresses.txt',
@@ -29,10 +30,10 @@ parser.add_argument(
logger = logging.getLogger(__file__)
-class SmartHomeConfig(object):
+class SmartHomeRegistry(object):
def __init__(
self,
- config_file: Optional[str] = None,
+ registry_file: Optional[str] = None,
filters: List[str] = ['smart'],
) -> None:
self._macs_by_name = {}
@@ -42,13 +43,13 @@ class SmartHomeConfig(object):
self._corpus = logical_search.Corpus()
# Read the disk config file...
- if config_file is None:
- config_file = config.config[
- 'smart_home_config_file_location'
+ if registry_file is None:
+ registry_file = config.config[
+ 'smart_home_registry_file_location'
]
- assert file_utils.does_file_exist(config_file)
- logger.debug(f'Reading {config_file}')
- with open(config_file, "r") as f:
+ assert file_utils.does_file_exist(registry_file)
+ logger.debug(f'Reading {registry_file}')
+ with open(registry_file, "r") as f:
contents = f.readlines()
# Parse the contents...
@@ -58,7 +59,7 @@ class SmartHomeConfig(object):
line = line.strip()
if line == "":
continue
- logger.debug(f'> {line}')
+ logger.debug(f'SH-CONFIG> {line}')
(mac, name, keywords) = line.split(",")
mac = mac.strip()
name = name.strip()
@@ -139,38 +140,47 @@ class SmartHomeConfig(object):
name = self._names_by_mac[mac]
kws = self._keywords_by_mac[mac]
logger.debug(f'Found {name} -> {mac} ({kws})')
- if 'light' in kws.lower():
- if 'tplink' in kws.lower():
- logger.debug(' ...a TPLinkLight')
- return lights.TPLinkLight(name, mac, kws)
- elif 'tuya' in kws.lower():
- logger.debug(' ...a TuyaLight')
- return lights.TuyaLight(name, mac, kws)
- elif 'goog' in kws.lower():
- logger.debug(' ...a GoogleLight')
- return lights.GoogleLight(name, mac, kws)
- else:
- raise Exception(f'Unknown light device: {name}, {mac}, {kws}')
- elif 'outlet' in kws.lower():
- if 'tplink' in kws.lower():
- if 'children' in kws.lower():
- logger.debug(' ...a TPLinkOutletWithChildren')
- return outlets.TPLinkOutletWithChildren(name, mac, kws)
+ try:
+ if 'light' in kws.lower():
+ if 'tplink' in kws.lower():
+ logger.debug(' ...a TPLinkLight')
+ return lights.TPLinkLight(name, mac, kws)
+ elif 'tuya' in kws.lower():
+ logger.debug(' ...a TuyaLight')
+ return lights.TuyaLight(name, mac, kws)
+ elif 'goog' in kws.lower():
+ logger.debug(' ...a GoogleLight')
+ return lights.GoogleLight(name, mac, kws)
+ else:
+ raise Exception(f'Unknown light device: {name}, {mac}, {kws}')
+ elif 'outlet' in kws.lower():
+ if 'tplink' in kws.lower():
+ if 'children' in kws.lower():
+ logger.debug(' ...a TPLinkOutletWithChildren')
+ return outlets.TPLinkOutletWithChildren(name, mac, kws)
+ else:
+ logger.debug(' ...a TPLinkOutlet')
+ return outlets.TPLinkOutlet(name, mac, kws)
+ elif 'goog' in kws.lower():
+ logger.debug(' ...a GoogleOutlet')
+ return outlets.GoogleOutlet(name, mac, kws)
else:
- logger.debug(' ...a TPLinkOutlet')
- return outlets.TPLinkOutlet(name, mac, kws)
- elif 'goog' in kws.lower():
- logger.debug(' ...a GoogleOutlet')
- return outlets.GoogleOutlet(name, mac, kws)
+ raise Exception(f'Unknown outlet device: {name}, {mac}, {kws}')
+ elif 'camera' in kws.lower():
+ logger.debug(' ...a BaseCamera')
+ return cameras.BaseCamera(name, mac, kws)
+ elif 'ccast' in kws.lower():
+ logger.debug(' ...a Chromecast')
+ return chromecasts.BaseChromecast(name, mac, kws)
else:
- raise Exception(f'Unknown outlet device: {name}, {mac}, {kws}')
- elif 'camera' in kws.lower():
- logger.debug(' ...a BaseCamera')
- return cameras.BaseCamera(name, mac, kws)
- else:
- logger.debug(' ...an unknown device (should this be here?)')
+ logger.debug(' ...an unknown device (should this be here?)')
+ return device.Device(name, mac, kws)
+ except Exception as e:
+ logger.warning(
+ f'Got exception {e} while trying to communicate with device {name}/{mac}.'
+ )
return device.Device(name, mac, kws)
- logger.warning(f'{mac} is not known, returning None')
+ logger.warning(f'{mac} is not a known smart home device, returning None')
return None
def query(self, query: str) -> List[device.Device]: