summaryrefslogtreecommitdiff
path: root/smart_home
diff options
context:
space:
mode:
authorScott <[email protected]>2022-01-06 12:13:34 -0800
committerScott <[email protected]>2022-01-06 12:13:34 -0800
commit5f75cf834725ac26b289cc5f157af0cb71cd5f0e (patch)
treef31baf4247a7d29eb1457a74f75d373d10539237 /smart_home
parentba223f821df1e9b8abbb6f6d23d5ba92c5a70b05 (diff)
A bunch of changes...
Diffstat (limited to 'smart_home')
-rw-r--r--smart_home/cameras.py9
-rw-r--r--smart_home/lights.py5
-rw-r--r--smart_home/outlets.py87
-rw-r--r--smart_home/registry.py3
-rw-r--r--smart_home/thermometers.py50
5 files changed, 150 insertions, 4 deletions
diff --git a/smart_home/cameras.py b/smart_home/cameras.py
index 8137012..51a95e9 100644
--- a/smart_home/cameras.py
+++ b/smart_home/cameras.py
@@ -16,6 +16,7 @@ class BaseCamera(dev.Device):
'outside_driveway_camera': 'driveway',
'outside_doorbell_camera': 'doorbell',
'outside_front_door_camera': 'front_door',
+ 'crawlspace_camera': 'crawlspace',
}
def __init__(self, name: str, mac: str, keywords: str = "") -> None:
@@ -23,5 +24,9 @@ class BaseCamera(dev.Device):
self.camera_name = BaseCamera.camera_mapping.get(name, None)
def get_stream_url(self) -> str:
- assert self.camera_name is not None
- return f'http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/mp4/GKlT2FfiSQ/{self.camera_name}/s.mp4'
+ name = self.camera_name
+ assert name is not None
+ if name == 'driveway':
+ return f'http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/mjpeg/GKlT2FfiSQ/driveway'
+ else:
+ return f'http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/mp4/GKlT2FfiSQ/{name}/s.mp4'
diff --git a/smart_home/lights.py b/smart_home/lights.py
index 1c4081c..e23569a 100644
--- a/smart_home/lights.py
+++ b/smart_home/lights.py
@@ -318,7 +318,10 @@ class TPLinkLight(BaseLight):
@overrides
def is_on(self) -> bool:
- return self.get_on_duration_seconds() > 0
+ self.info = self.get_info()
+ if self.info is None:
+ raise Exception('Unable to get info?')
+ return self.info.get("relay_state", "0") == "1"
@overrides
def is_off(self) -> bool:
diff --git a/smart_home/outlets.py b/smart_home/outlets.py
index f34d574..68dfd2b 100644
--- a/smart_home/outlets.py
+++ b/smart_home/outlets.py
@@ -3,6 +3,8 @@
"""Utilities for dealing with the smart outlets."""
from abc import abstractmethod
+import asyncio
+import atexit
import datetime
import json
import logging
@@ -10,11 +12,16 @@ import os
import re
import subprocess
import sys
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
+
+from meross_iot.http_api import MerossHttpClient
+from meross_iot.manager import MerossManager
import argparse_utils
import config
+import decorator_utils
import logging_utils
+import scott_secrets
import smart_home.device as dev
from google_assistant import ask_google, GoogleResponse
from decorator_utils import timeout, memoized
@@ -227,3 +234,81 @@ class GoogleOutlet(BaseOutlet):
def is_off(self) -> bool:
return not self.is_on()
+
+
+@decorator_utils.singleton
+class MerossWrapper(object):
+ """Note that instantiating this class causes HTTP traffic with an
+ external Meross server. Meross blocks customers who hit their
+ servers too aggressively so MerossOutlet is lazy about creating
+ instances of this class.
+
+ """
+
+ def __init__(self):
+ self.loop = asyncio.get_event_loop()
+ self.email = os.environ.get('MEROSS_EMAIL') or scott_secrets.MEROSS_EMAIL
+ self.password = os.environ.get('MEROSS_PASSWORD') or scott_secrets.MEROSS_PASSWORD
+ self.devices = self.loop.run_until_complete(self.find_meross_devices())
+ atexit.register(self.loop.close)
+
+ async def find_meross_devices(self) -> List[Any]:
+ http_api_client = await MerossHttpClient.async_from_user_password(
+ email=self.email, password=self.password
+ )
+
+ # Setup and start the device manager
+ manager = MerossManager(http_client=http_api_client)
+ await manager.async_init()
+
+ # Discover devices
+ await manager.async_device_discovery()
+ devices = manager.find_devices()
+ for device in devices:
+ await device.async_update()
+ return devices
+
+ def get_meross_device_by_name(self, name: str) -> Optional[Any]:
+ name = name.lower()
+ name = name.replace('_', ' ')
+ for device in self.devices:
+ if device.name.lower() == name:
+ return device
+ return None
+
+
+class MerossOutlet(BaseOutlet):
+ def __init__(self, name: str, mac: str, keywords: str = '') -> None:
+ super().__init__(name, mac, keywords)
+ self.meross_wrapper = None
+ self.device = None
+
+ def lazy_initialize_device(self):
+ """If we make too many calls to Meross they will block us; only talk
+ to them when someone actually wants to control a device."""
+ if self.meross_wrapper is None:
+ self.meross_wrapper = MerossWrapper()
+ self.device = self.meross_wrapper.get_meross_device_by_name(self.name)
+ if self.device is None:
+ raise Exception(f'{self.name} is not a known Meross device?!')
+
+ def turn_on(self) -> bool:
+ self.lazy_initialize_device()
+ self.meross_wrapper.loop.run_until_complete(
+ self.device.async_turn_on()
+ )
+ return True
+
+ def turn_off(self) -> bool:
+ self.lazy_initialize_device()
+ self.meross_wrapper.loop.run_until_complete(
+ self.device.async_turn_off()
+ )
+ return True
+
+ def is_on(self) -> bool:
+ self.lazy_initialize_device()
+ return self.device.is_on()
+
+ def is_off(self) -> bool:
+ return not self.is_on()
diff --git a/smart_home/registry.py b/smart_home/registry.py
index ae57a73..23584e1 100644
--- a/smart_home/registry.py
+++ b/smart_home/registry.py
@@ -165,6 +165,9 @@ class SmartHomeRegistry(object):
else:
logger.debug(' ...a TPLinkOutlet')
return outlets.TPLinkOutlet(name, mac, kws)
+ elif 'meross' in kws.lower():
+ logger.debug(' ...a MerossOutlet')
+ return outlets.MerossOutlet(name, mac, kws)
elif 'goog' in kws.lower():
logger.debug(' ...a GoogleOutlet')
return outlets.GoogleOutlet(name, mac, kws)
diff --git a/smart_home/thermometers.py b/smart_home/thermometers.py
new file mode 100644
index 0000000..fe5eed1
--- /dev/null
+++ b/smart_home/thermometers.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+import logging
+from typing import Optional
+import urllib.request
+
+logger = logging.getLogger()
+
+
+class ThermometerRegistry(object):
+ def __init__(self):
+ self.thermometers = {
+ 'house_outside': ('10.0.0.75', 'outside_temp'),
+ 'house_inside_downstairs': ('10.0.0.75', 'inside_downstairs_temp'),
+ 'house_inside_upstairs': ('10.0.0.75', 'inside_upstairs_temp'),
+ 'house_computer_closet': ('10.0.0.75', 'computer_closet_temp'),
+ 'house_crawlspace': ('10.0.0.75', 'crawlspace_temp'),
+ 'cabin_outside': ('192.168.0.107', 'outside_temp'),
+ 'cabin_inside': ('192.168.0.107', 'inside_temp'),
+ 'cabin_crawlspace': ('192.168.0.107', 'crawlspace_temp'),
+ 'cabin_hottub': ('192.168.0.107', 'hottub_temp'),
+ }
+
+ def read_temperature(
+ self, location: str, *, convert_to_fahrenheit=False
+ ) -> Optional[float]:
+ record = self.thermometers.get(location, None)
+ if record is None:
+ logger.error(
+ f'Location {location} is not known. Valid locations are {self.thermometers.keys()}.'
+ )
+ return None
+ url = f'http://{record[0]}/~pi/{record[1]}'
+ logger.debug(f'Constructed URL: {url}')
+ try:
+ www = urllib.request.urlopen(url, timeout=3)
+ temp = www.read().decode('utf-8')
+ temp = float(temp)
+ if convert_to_fahrenheit:
+ temp *= (9/5)
+ temp += 32.0
+ temp = round(temp)
+ except Exception as e:
+ logger.exception(e)
+ logger.error(f'Failed to read temperature at URL: {url}')
+ temp = None
+ finally:
+ if www is not None:
+ www.close()
+ return temp