summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cached/weather_data.py78
-rw-r--r--cached/weather_forecast.py48
-rw-r--r--collect/bst.py36
-rw-r--r--collect/shared_dict.py4
-rwxr-xr-xdateparse/dateparse_utils.py42
-rw-r--r--datetime_utils.py2
-rw-r--r--ml/model_trainer.py29
-rw-r--r--smart_home/chromecasts.py29
-rw-r--r--smart_home/device.py10
-rw-r--r--smart_home/lights.py39
-rw-r--r--smart_home/outlets.py71
-rw-r--r--smart_home/registry.py18
-rw-r--r--type/centcount.py45
-rw-r--r--type/money.py62
-rw-r--r--type/rate.py17
15 files changed, 258 insertions, 272 deletions
diff --git a/cached/weather_data.py b/cached/weather_data.py
index 45b6e6e..7b86d02 100644
--- a/cached/weather_data.py
+++ b/cached/weather_data.py
@@ -1,12 +1,12 @@
#!/usr/bin/env python3
-from dataclasses import dataclass
import datetime
import json
import logging
import os
-from typing import Any, List
import urllib.request
+from dataclasses import dataclass
+from typing import Any, List
from overrides import overrides
@@ -27,32 +27,31 @@ cfg.add_argument(
type=str,
default=f'{os.environ["HOME"]}/cache/.weather_summary_cache',
metavar='FILENAME',
- help='File in which to cache weather data'
+ help='File in which to cache weather data',
)
cfg.add_argument(
'--weather_data_stalest_acceptable',
type=argparse_utils.valid_duration,
- default=datetime.timedelta(seconds=7200), # 2 hours
+ default=datetime.timedelta(seconds=7200), # 2 hours
metavar='DURATION',
- help='Maximum acceptable age of cached data. If zero, forces a refetch'
+ help='Maximum acceptable age of cached data. If zero, forces a refetch',
)
@dataclass
class WeatherData:
- date: datetime.date # The date
- high: float # The predicted high in F
- low: float # The predicted low in F
- precipitation_inches: float # Number of inches of precipitation / day
- conditions: List[str] # Conditions per ~3h window
- most_common_condition: str # The most common condition
- icon: str # An icon to represent it
+ date: datetime.date # The date
+ high: float # The predicted high in F
+ low: float # The predicted low in F
+ precipitation_inches: float # Number of inches of precipitation / day
+ conditions: List[str] # Conditions per ~3h window
+ most_common_condition: str # The most common condition
+ icon: str # An icon to represent it
[email protected]_autoloaded_singleton()
[email protected]_autoloaded_singleton() # type: ignore
class CachedWeatherData(persistent.Persistent):
- def __init__(self,
- weather_data = None):
+ def __init__(self, weather_data=None):
if weather_data is not None:
self.weather_data = weather_data
return
@@ -72,7 +71,7 @@ class CachedWeatherData(persistent.Persistent):
"Sand": "🏜️",
"Ash": "🌋",
"Squall": "🌬",
- "Tornado": "🌪️"
+ "Tornado": "🌪️",
}
now = datetime.datetime.now()
dates = set()
@@ -80,7 +79,7 @@ class CachedWeatherData(persistent.Persistent):
lows = {}
conditions = {}
precip = {}
- param = "id=5786882" # Bellevue, WA
+ param = "id=5786882" # Bellevue, WA
key = "c0b160c49743622f62a9cd3cda0270b3"
www = urllib.request.urlopen(
f'http://api.openweathermap.org/data/2.5/weather?zip=98005,us&APPID={key}&units=imperial'
@@ -107,13 +106,13 @@ class CachedWeatherData(persistent.Persistent):
if dt == now.date() and now.hour > 18 and condition == 'Clear':
icon = '🌙'
self.weather_data[dt] = WeatherData(
- date = dt,
- high = float(parsed_json["main"]["temp_max"]),
- low = float(parsed_json["main"]["temp_min"]),
- precipitation_inches = p / 25.4,
- conditions = [condition],
- most_common_condition = condition,
- icon = icon,
+ date=dt,
+ high=float(parsed_json["main"]["temp_max"]),
+ low=float(parsed_json["main"]["temp_min"]),
+ precipitation_inches=p / 25.4,
+ conditions=[condition],
+ most_common_condition=condition,
+ icon=icon,
)
www = urllib.request.urlopen(
@@ -134,9 +133,9 @@ class CachedWeatherData(persistent.Persistent):
lows[dt] = None
conditions[dt] = []
for temp in (
- data["main"]["temp"],
- data['main']['temp_min'],
- data['main']['temp_max'],
+ data["main"]["temp"],
+ data['main']['temp_min'],
+ data['main']['temp_max'],
):
if highs[dt] is None or temp > highs[dt]:
highs[dt] = temp
@@ -160,10 +159,7 @@ class CachedWeatherData(persistent.Persistent):
for dt in sorted(dates):
if dt == today:
high = highs.get(dt, None)
- if (
- high is not None and
- self.weather_data[today].high < high
- ):
+ if high is not None and self.weather_data[today].high < high:
self.weather_data[today].high = high
continue
most_common_condition = list_utils.most_common(conditions[dt])
@@ -171,23 +167,24 @@ class CachedWeatherData(persistent.Persistent):
if dt == now.date() and now.hour > 18 and condition == 'Clear':
icon = '🌙'
self.weather_data[dt] = WeatherData(
- date = dt,
- high = highs[dt],
- low = lows[dt],
- precipitation_inches = precip[dt] / 25.4,
- conditions = conditions[dt],
- most_common_condition = most_common_condition,
- icon = icon
+ date=dt,
+ high=highs[dt],
+ low=lows[dt],
+ precipitation_inches=precip[dt] / 25.4,
+ conditions=conditions[dt],
+ most_common_condition=most_common_condition,
+ icon=icon,
)
@classmethod
@overrides
def load(cls) -> Any:
if persistent.was_file_written_within_n_seconds(
- config.config['weather_data_cachefile'],
- config.config['weather_data_stalest_acceptable'].total_seconds(),
+ config.config['weather_data_cachefile'],
+ config.config['weather_data_stalest_acceptable'].total_seconds(),
):
import pickle
+
with open(config.config['weather_data_cachefile'], 'rb') as rf:
weather_data = pickle.load(rf)
return cls(weather_data)
@@ -196,6 +193,7 @@ class CachedWeatherData(persistent.Persistent):
@overrides
def save(self) -> bool:
import pickle
+
with open(config.config['weather_data_cachefile'], 'wb') as wf:
pickle.dump(
self.weather_data,
diff --git a/cached/weather_forecast.py b/cached/weather_forecast.py
index b343938..58f53c3 100644
--- a/cached/weather_forecast.py
+++ b/cached/weather_forecast.py
@@ -1,60 +1,59 @@
#!/usr/bin/env python3
-from dataclasses import dataclass
import datetime
import logging
import os
-from typing import Any
import urllib.request
+from dataclasses import dataclass
+from typing import Any
import astral # type: ignore
+import pytz
from astral.sun import sun # type: ignore
from bs4 import BeautifulSoup # type: ignore
from overrides import overrides
-import pytz
import argparse_utils
import config
-import datetime_utils
import dateparse.dateparse_utils as dp
+import datetime_utils
import persistent
-import text_utils
import smart_home.thermometers as temps
-
+import text_utils
logger = logging.getLogger(__name__)
cfg = config.add_commandline_args(
f'Cached Weather Forecast ({__file__})',
- 'Arguments controlling detailed weather rendering'
+ 'Arguments controlling detailed weather rendering',
)
cfg.add_argument(
'--weather_forecast_cachefile',
type=str,
default=f'{os.environ["HOME"]}/cache/.weather_forecast_cache',
metavar='FILENAME',
- help='File in which to cache weather data'
+ help='File in which to cache weather data',
)
cfg.add_argument(
'--weather_forecast_stalest_acceptable',
type=argparse_utils.valid_duration,
- default=datetime.timedelta(seconds=7200), # 2 hours
+ default=datetime.timedelta(seconds=7200), # 2 hours
metavar='DURATION',
- help='Maximum acceptable age of cached data. If zero, forces a refetch'
+ help='Maximum acceptable age of cached data. If zero, forces a refetch',
)
@dataclass
class WeatherForecast:
- date: datetime.date # The date
- sunrise: datetime.datetime # Sunrise datetime
- sunset: datetime.datetime # Sunset datetime
- description: str # Textual description of weather
+ date: datetime.date # The date
+ sunrise: datetime.datetime # Sunrise datetime
+ sunset: datetime.datetime # Sunset datetime
+ description: str # Textual description of weather
[email protected]_autoloaded_singleton()
[email protected]_autoloaded_singleton() # type: ignore
class CachedDetailedWeatherForecast(persistent.Persistent):
- def __init__(self, forecasts = None):
+ def __init__(self, forecasts=None):
if forecasts is not None:
self.forecasts = forecasts
return
@@ -82,8 +81,7 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
last_dt = now
dt = now
for (day, txt) in zip(
- forecast.find_all('b'),
- forecast.find_all(class_='col-sm-10 forecast-text')
+ forecast.find_all('b'), forecast.find_all(class_='col-sm-10 forecast-text')
):
last_dt = dt
try:
@@ -112,20 +110,21 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
self.forecasts[dt.date()].description += '\n' + blurb
else:
self.forecasts[dt.date()] = WeatherForecast(
- date = dt,
- sunrise = sunrise,
- sunset = sunset,
- description = blurb,
+ date=dt,
+ sunrise=sunrise,
+ sunset=sunset,
+ description=blurb,
)
@classmethod
@overrides
def load(cls) -> Any:
if persistent.was_file_written_within_n_seconds(
- config.config['weather_forecast_cachefile'],
- config.config['weather_forecast_stalest_acceptable'].total_seconds(),
+ config.config['weather_forecast_cachefile'],
+ config.config['weather_forecast_stalest_acceptable'].total_seconds(),
):
import pickle
+
with open(config.config['weather_forecast_cachefile'], 'rb') as rf:
weather_data = pickle.load(rf)
return cls(weather_data)
@@ -134,6 +133,7 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
@overrides
def save(self) -> bool:
import pickle
+
with open(config.config['weather_forecast_cachefile'], 'wb') as wf:
pickle.dump(
self.forecasts,
diff --git a/collect/bst.py b/collect/bst.py
index 72a3b77..9d65259 100644
--- a/collect/bst.py
+++ b/collect/bst.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-from typing import Any, Optional, List
+from typing import Any, Generator, List, Optional
class Node(object):
@@ -9,8 +9,8 @@ class Node(object):
Note: value can be anything as long as it is comparable.
Check out @functools.total_ordering.
"""
- self.left = None
- self.right = None
+ self.left: Optional[Node] = None
+ self.right: Optional[Node] = None
self.value = value
@@ -84,16 +84,18 @@ class BinarySearchTree(object):
"""Find helper"""
if value == node.value:
return node
- elif (value < node.value and node.left is not None):
+ elif value < node.value and node.left is not None:
return self._find(value, node.left)
- elif (value > node.value and node.right is not None):
+ elif value > node.value and node.right is not None:
return self._find(value, node.right)
return None
- def _parent_path(self, current: Node, target: Node):
+ def _parent_path(
+ self, current: Optional[Node], target: Node
+ ) -> List[Optional[Node]]:
if current is None:
return [None]
- ret = [current]
+ ret: List[Optional[Node]] = [current]
if target.value == current.value:
return ret
elif target.value < current.value:
@@ -104,7 +106,7 @@ class BinarySearchTree(object):
ret.extend(self._parent_path(current.right, target))
return ret
- def parent_path(self, node: Node) -> Optional[List[Node]]:
+ def parent_path(self, node: Node) -> List[Optional[Node]]:
"""Return a list of nodes representing the path from
the tree's root to the node argument. If the node does
not exist in the tree for some reason, the last element
@@ -456,7 +458,7 @@ class BinarySearchTree(object):
if node.right is not None:
yield from self._iterate_by_depth(node.right, depth - 1)
- def iterate_nodes_by_depth(self, depth: int):
+ def iterate_nodes_by_depth(self, depth: int) -> Generator[Node, None, None]:
"""
Iterate only the leaf nodes in the tree.
@@ -518,13 +520,16 @@ class BinarySearchTree(object):
return x
path = self.parent_path(node)
+ assert path[-1]
assert path[-1] == node
path = path[:-1]
path.reverse()
for ancestor in path:
+ assert ancestor
if node != ancestor.right:
return ancestor
node = ancestor
+ raise Exception()
def _depth(self, node: Node, sofar: int) -> int:
depth_left = sofar + 1
@@ -569,7 +574,9 @@ class BinarySearchTree(object):
def height(self):
return self.depth()
- def repr_traverse(self, padding: str, pointer: str, node: Node, has_right_sibling: bool) -> str:
+ def repr_traverse(
+ self, padding: str, pointer: str, node: Optional[Node], has_right_sibling: bool
+ ) -> str:
if node is not None:
viz = f'\n{padding}{pointer}{node.value}'
if has_right_sibling:
@@ -583,7 +590,9 @@ class BinarySearchTree(object):
else:
pointer_left = "└──"
- viz += self.repr_traverse(padding, pointer_left, node.left, node.right is not None)
+ viz += self.repr_traverse(
+ padding, pointer_left, node.left, node.right is not None
+ )
viz += self.repr_traverse(padding, pointer_right, node.right, False)
return viz
return ""
@@ -619,11 +628,14 @@ class BinarySearchTree(object):
else:
pointer_left = "├──"
- ret += self.repr_traverse('', pointer_left, self.root.left, self.root.left is not None)
+ ret += self.repr_traverse(
+ '', pointer_left, self.root.left, self.root.left is not None
+ )
ret += self.repr_traverse('', pointer_right, self.root.right, False)
return ret
if __name__ == '__main__':
import doctest
+
doctest.testmod()
diff --git a/collect/shared_dict.py b/collect/shared_dict.py
index 0d8e7c2..7c84c14 100644
--- a/collect/shared_dict.py
+++ b/collect/shared_dict.py
@@ -30,14 +30,14 @@ This class is based on https://github.com/luizalabs/shared-memory-dict
import pickle
from contextlib import contextmanager
from functools import wraps
-from multiprocessing import shared_memory, RLock
+from multiprocessing import RLock, shared_memory
from typing import (
Any,
Dict,
Generator,
- KeysView,
ItemsView,
Iterator,
+ KeysView,
Optional,
ValuesView,
)
diff --git a/dateparse/dateparse_utils.py b/dateparse/dateparse_utils.py
index be5e1b5..7ca3cf3 100755
--- a/dateparse/dateparse_utils.py
+++ b/dateparse/dateparse_utils.py
@@ -7,7 +7,6 @@ Parse dates in a variety of formats.
import datetime
import functools
-import holidays # type: ignore
import logging
import re
import sys
@@ -16,21 +15,21 @@ from typing import Any, Callable, Dict, Optional
import antlr4 # type: ignore
import dateutil.easter
import dateutil.tz
+import holidays # type: ignore
import pytz
import acl
import bootstrap
+import decorator_utils
+from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
+from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
+from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
from datetime_utils import (
TimeUnit,
- n_timeunits_from_base,
- datetime_to_date,
date_to_datetime,
+ datetime_to_date,
+ n_timeunits_from_base,
)
-from dateparse.dateparse_utilsLexer import dateparse_utilsLexer # type: ignore
-from dateparse.dateparse_utilsListener import dateparse_utilsListener # type: ignore
-from dateparse.dateparse_utilsParser import dateparse_utilsParser # type: ignore
-import decorator_utils
-
logger = logging.getLogger(__name__)
@@ -225,8 +224,9 @@ class DateParser(dateparse_utilsListener):
to timezone naive (i.e. tzinfo = None).
"""
dt = self.datetime
- if tz is not None:
- dt = dt.replace(tzinfo=None).astimezone(tz=tz)
+ if dt is not None:
+ if tz is not None:
+ dt = dt.replace(tzinfo=None).astimezone(tz=tz)
return dt
# -- helpers --
@@ -376,25 +376,25 @@ class DateParser(dateparse_utilsListener):
# Try pytz
try:
- tz = pytz.timezone(txt)
- if tz is not None:
- return tz
+ tz1 = pytz.timezone(txt)
+ if tz1 is not None:
+ return tz1
except Exception:
pass
# Try dateutil
try:
- tz = dateutil.tz.gettz(txt)
- if tz is not None:
- return tz
+ tz2 = dateutil.tz.gettz(txt)
+ if tz2 is not None:
+ return tz2
except Exception:
pass
# Try constructing an offset in seconds
try:
- sign = txt[0]
- if sign == '-' or sign == '+':
- sign = +1 if sign == '+' else -1
+ txt_sign = txt[0]
+ if txt_sign == '-' or txt_sign == '+':
+ sign = +1 if txt_sign == '+' else -1
hour = int(txt[1:3])
minute = int(txt[-2:])
offset = sign * (hour * 60 * 60) + sign * (minute * 60)
@@ -441,6 +441,7 @@ class DateParser(dateparse_utilsListener):
# Apply resudual adjustments to times here when we have a
# datetime.
self.datetime = self.datetime + self.timedelta
+ assert self.datetime
self.time = datetime.time(
self.datetime.hour,
self.datetime.minute,
@@ -547,7 +548,7 @@ class DateParser(dateparse_utilsListener):
elif unit == TimeUnit.HOURS:
self.timedelta = datetime.timedelta(hours=count)
else:
- raise ParseException()
+ raise ParseException(f'Invalid Unit: "{unit}"')
def exitDeltaPlusMinusExpr(
self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
@@ -1027,6 +1028,7 @@ def main() -> None:
logger.exception(e)
print("Unrecognized.")
else:
+ assert dt
print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
sys.exit(0)
diff --git a/datetime_utils.py b/datetime_utils.py
index 6f504f6..fb85971 100644
--- a/datetime_utils.py
+++ b/datetime_utils.py
@@ -229,7 +229,7 @@ def datetime_to_time(dt: datetime.datetime) -> datetime.time:
return datetime_to_date_and_time(dt)[1]
-class TimeUnit(enum.Enum):
+class TimeUnit(enum.IntEnum):
"""An enum to represent units with which we can compute deltas."""
MONDAYS = 0
diff --git a/ml/model_trainer.py b/ml/model_trainer.py
index 213a181..6fc0da0 100644
--- a/ml/model_trainer.py
+++ b/ml/model_trainer.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-from abc import ABC, abstractmethod
import datetime
import glob
import logging
@@ -10,20 +9,21 @@ import os
import pickle
import random
import sys
+import warnings
+from abc import ABC, abstractmethod
from types import SimpleNamespace
from typing import Any, List, NamedTuple, Optional, Set, Tuple
-import warnings
import numpy as np
from sklearn.model_selection import train_test_split # type:ignore
from sklearn.preprocessing import MinMaxScaler # type: ignore
-from ansi import bold, reset
import argparse_utils
import config
-from decorator_utils import timed
import executors
import parallelize as par
+from ansi import bold, reset
+from decorator_utils import timed
logger = logging.getLogger(__file__)
@@ -81,8 +81,8 @@ class OutputSpec(NamedTuple):
model_filename: Optional[str]
model_info_filename: Optional[str]
scaler_filename: Optional[str]
- training_score: float
- test_score: float
+ training_score: np.float64
+ test_score: np.float64
class TrainingBlueprint(ABC):
@@ -131,9 +131,9 @@ class TrainingBlueprint(ABC):
modelid_to_params[model.get_id()] = str(params)
best_model = None
- best_score = None
- best_test_score = None
- best_training_score = None
+ best_score: Optional[np.float64] = None
+ best_test_score: Optional[np.float64] = None
+ best_training_score: Optional[np.float64] = None
best_params = None
for model in smart_future.wait_any(models):
params = modelid_to_params[model.get_id()]
@@ -170,6 +170,9 @@ class TrainingBlueprint(ABC):
print(msg)
logger.info(msg)
+ assert best_training_score
+ assert best_test_score
+ assert best_params
(
scaler_filename,
model_filename,
@@ -369,14 +372,14 @@ Testing set score: {test_score:.2f}%"""
and input_utils.yn_response("Write the model? [y,n]: ") == "y"
):
scaler_filename = f"{self.spec.basename}_scaler.sav"
- with open(scaler_filename, "wb") as f:
- pickle.dump(scaler, f)
+ with open(scaler_filename, "wb") as fb:
+ pickle.dump(scaler, fb)
msg = f"Wrote {scaler_filename}"
print(msg)
logger.info(msg)
model_filename = f"{self.spec.basename}_model.sav"
- with open(model_filename, "wb") as f:
- pickle.dump(model, f)
+ with open(model_filename, "wb") as fb:
+ pickle.dump(model, fb)
msg = f"Wrote {model_filename}"
print(msg)
logger.info(msg)
diff --git a/smart_home/chromecasts.py b/smart_home/chromecasts.py
index a5db86f..bd2a80c 100644
--- a/smart_home/chromecasts.py
+++ b/smart_home/chromecasts.py
@@ -6,17 +6,18 @@ import atexit
import datetime
import logging
import threading
+from typing import Any, List
import pychromecast
-from decorator_utils import memoized
import smart_home.device as dev
+from decorator_utils import memoized
logger = logging.getLogger(__name__)
class BaseChromecast(dev.Device):
- ccasts = []
+ ccasts: List[Any] = []
refresh_ts = None
browser = None
lock = threading.Lock()
@@ -25,31 +26,32 @@ class BaseChromecast(dev.Device):
super().__init__(name.strip(), mac.strip(), keywords)
ip = self.get_ip()
now = datetime.datetime.now()
- with BaseChromecast.lock as l:
+ with BaseChromecast.lock:
if (
- BaseChromecast.refresh_ts is None
- or (now - BaseChromecast.refresh_ts).total_seconds() > 60
+ BaseChromecast.refresh_ts is None
+ or (now - BaseChromecast.refresh_ts).total_seconds() > 60
):
logger.debug('Refreshing the shared chromecast info list')
if BaseChromecast.browser is not None:
BaseChromecast.browser.stop_discovery()
- BaseChromecast.ccasts, BaseChromecast.browser = pychromecast.get_chromecasts(
- timeout=15.0
- )
+ (
+ BaseChromecast.ccasts,
+ BaseChromecast.browser,
+ ) = pychromecast.get_chromecasts(timeout=15.0)
+ assert BaseChromecast.browser
atexit.register(BaseChromecast.browser.stop_discovery)
BaseChromecast.refresh_ts = now
self.cast = None
for cc in BaseChromecast.ccasts:
- if (
- cc.cast_info.host == ip
- and cc.cast_info.cast_type != 'group'
- ):
+ if cc.cast_info.host == ip and cc.cast_info.cast_type != 'group':
logger.debug(f'Found chromecast at {ip}: {cc}')
self.cast = cc
self.cast.wait(timeout=1.0)
if self.cast is None:
- raise Exception(f'Can\'t find ccast device at {ip}, is that really a ccast device?')
+ raise Exception(
+ f'Can\'t find ccast device at {ip}, is that really a ccast device?'
+ )
def is_idle(self):
return self.cast.is_idle
@@ -116,4 +118,3 @@ class BaseChromecast(dev.Device):
f"Chromecast({self.cast.socket_client.host!r}, port={self.cast.socket_client.port!r}, "
f"device={self.cast.cast_info.friendly_name!r})"
)
-
diff --git a/smart_home/device.py b/smart_home/device.py
index 9675b7c..02717a3 100644
--- a/smart_home/device.py
+++ b/smart_home/device.py
@@ -8,17 +8,17 @@ import arper
class Device(object):
def __init__(
- self,
- name: str,
- mac: str,
- keywords: Optional[List[str]],
+ self,
+ name: str,
+ mac: str,
+ keywords: Optional[List[str]],
):
self.name = name
self.mac = mac
self.keywords = keywords
self.arper = arper.Arper()
if keywords is not None:
- self.kws = keywords.split()
+ self.kws = keywords
else:
self.kws = []
diff --git a/smart_home/lights.py b/smart_home/lights.py
index 64f2105..240e7da 100644
--- a/smart_home/lights.py
+++ b/smart_home/lights.py
@@ -2,7 +2,6 @@
"""Utilities for dealing with the smart lights."""
-from abc import abstractmethod
import datetime
import json
import logging
@@ -10,10 +9,11 @@ import os
import re
import subprocess
import sys
+from abc import abstractmethod
from typing import Any, Dict, List, Optional, Tuple
-from overrides import overrides
import tinytuya as tt
+from overrides import overrides
import ansi
import argparse_utils
@@ -21,8 +21,8 @@ import arper
import config
import logging_utils
import smart_home.device as dev
-from google_assistant import ask_google, GoogleResponse
-from decorator_utils import timeout, memoized
+from decorator_utils import memoized, timeout
+from google_assistant import GoogleResponse, ask_google
logger = logging.getLogger(__name__)
@@ -39,9 +39,7 @@ args.add_argument(
)
-@timeout(
- 5.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-)
+@timeout(5.0, use_signals=False, error_message="Timed out waiting for tplink.py")
def tplink_light_command(command: str) -> bool:
result = os.system(command)
signal = result & 0xFF
@@ -69,9 +67,9 @@ class BaseLight(dev.Device):
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
+ color,
)
- if m is not None and len(m.group) == 3:
+ if m is not None and len(m.groups()) == 3:
red = int(m.group(0), 16)
green = int(m.group(1), 16)
blue = int(m.group(2), 16)
@@ -147,7 +145,9 @@ class GoogleLight(BaseLight):
r = ask_google(f"is {self.goog_name()} on?")
if not r.success:
return False
- return 'is on' in r.audio_transcription
+ if r.audio_transcription is not None:
+ return 'is on' in r.audio_transcription
+ raise Exception("Can't reach Google?!")
@overrides
def is_off(self) -> bool:
@@ -163,11 +163,12 @@ class GoogleLight(BaseLight):
# the bookcase one is set to 40% bright
txt = r.audio_transcription
- m = re.search(r"(\d+)% bright", txt)
- if m is not None:
- return int(m.group(1))
- if "is off" in txt:
- return 0
+ if txt is not None:
+ m = re.search(r"(\d+)% bright", txt)
+ if m is not None:
+ return int(m.group(1))
+ if "is off" in txt:
+ return 0
return None
@overrides
@@ -301,9 +302,7 @@ class TPLinkLight(BaseLight):
def get_children(self) -> List[str]:
return self.children
- def command(
- self, cmd: str, child: str = None, extra_args: str = None
- ) -> bool:
+ def command(self, cmd: str, child: str = None, extra_args: str = None) -> bool:
cmd = self.get_cmdline(child) + f"-c {cmd}"
if extra_args is not None:
cmd += f" {extra_args}"
@@ -333,9 +332,7 @@ class TPLinkLight(BaseLight):
def make_color(self, color: str) -> bool:
raise NotImplementedError
- @timeout(
- 10.0, use_signals=False, error_message="Timed out waiting for tplink.py"
- )
+ @timeout(10.0, use_signals=False, error_message="Timed out waiting for tplink.py")
def get_info(self) -> Optional[Dict]:
cmd = self.get_cmdline() + "-c info"
out = subprocess.getoutput(cmd)
diff --git a/smart_home/outlets.py b/smart_home/outlets.py
index c079cfd..d4a4886 100644
--- a/smart_home/outlets.py
+++ b/smart_home/outlets.py
@@ -2,7 +2,6 @@
"""Utilities for dealing with the smart outlets."""
-from abc import abstractmethod
import asyncio
import atexit
import datetime
@@ -12,10 +11,12 @@ import os
import re
import subprocess
import sys
+from abc import abstractmethod
from typing import Any, Dict, List, Optional
from meross_iot.http_api import MerossHttpClient
from meross_iot.manager import MerossManager
+from overrides import overrides
import argparse_utils
import config
@@ -23,8 +24,8 @@ 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
+from decorator_utils import memoized, timeout
+from google_assistant import GoogleResponse, ask_google
logger = logging.getLogger(__name__)
@@ -41,9 +42,7 @@ parser.add_argument(
)
-@timeout(
- 5.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-)
+@timeout(5.0, use_signals=False, error_message="Timed out waiting for tplink.py")
def tplink_outlet_command(command: str) -> bool:
result = os.system(command)
signal = result & 0xFF
@@ -66,7 +65,6 @@ def tplink_outlet_command(command: str) -> bool:
class BaseOutlet(dev.Device):
def __init__(self, name: str, mac: str, keywords: str = "") -> None:
super().__init__(name.strip(), mac.strip(), keywords)
- self.info = None
@abstractmethod
def turn_on(self) -> bool:
@@ -105,27 +103,29 @@ class TPLinkOutlet(BaseOutlet):
)
return cmd
- def command(self, cmd: str, extra_args: str = None) -> bool:
+ def command(self, cmd: str, extra_args: str = None, **kwargs) -> bool:
cmd = self.get_cmdline() + f"-c {cmd}"
if extra_args is not None:
cmd += f" {extra_args}"
return tplink_outlet_command(cmd)
+ @overrides
def turn_on(self) -> bool:
return self.command('on')
+ @overrides
def turn_off(self) -> bool:
return self.command('off')
+ @overrides
def is_on(self) -> bool:
return self.get_on_duration_seconds() > 0
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
- @timeout(
- 10.0, use_signals=False, error_message="Timed out waiting for tplink.py"
- )
+ @timeout(10.0, use_signals=False, error_message="Timed out waiting for tplink.py")
def get_info(self) -> Optional[Dict]:
cmd = self.get_cmdline() + "-c info"
out = subprocess.getoutput(cmd)
@@ -161,7 +161,7 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
for child in self.info["children"]:
self.children.append(child["id"])
- # override
+ @overrides
def get_cmdline(self, child: Optional[str] = None) -> str:
cmd = (
f"{config.config['smart_outlets_tplink_location']} -m {self.mac} "
@@ -171,10 +171,9 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
cmd += f"-x {child} "
return cmd
- # override
- def command(
- self, cmd: str, child: str = None, extra_args: str = None
- ) -> bool:
+ @overrides
+ def command(self, cmd: str, extra_args: str = None, **kwargs) -> bool:
+ child: Optional[str] = kwargs.get('child', None)
cmd = self.get_cmdline(child) + f"-c {cmd}"
if extra_args is not None:
cmd += f" {extra_args}"
@@ -184,9 +183,11 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
def get_children(self) -> List[str]:
return self.children
+ @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)
@@ -218,22 +219,28 @@ class GoogleOutlet(BaseOutlet):
def parse_google_response(response: GoogleResponse) -> bool:
return response.success
+ @overrides
def turn_on(self) -> bool:
return GoogleOutlet.parse_google_response(
- ask_google('turn {self.goog_name()} on')
+ ask_google(f'turn {self.goog_name()} on')
)
+ @overrides
def turn_off(self) -> bool:
return GoogleOutlet.parse_google_response(
- ask_google('turn {self.goog_name()} off')
+ 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
+ if r.audio_transcription is not None:
+ return 'is on' in r.audio_transcription
+ raise Exception('Can\'t talk to Google right now!?')
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
@@ -247,10 +254,13 @@ class MerossWrapper(object):
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.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)
@@ -282,8 +292,8 @@ class MerossWrapper(object):
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
+ self.meross_wrapper: Optional[MerossWrapper] = None
+ self.device: Optional[Any] = None
def lazy_initialize_device(self):
"""If we make too many calls to Meross they will block us; only talk
@@ -294,23 +304,28 @@ class MerossOutlet(BaseOutlet):
if self.device is None:
raise Exception(f'{self.name} is not a known Meross device?!')
+ @overrides
def turn_on(self) -> bool:
self.lazy_initialize_device()
- self.meross_wrapper.loop.run_until_complete(
- self.device.async_turn_on()
- )
+ assert self.meross_wrapper
+ assert self.device
+ self.meross_wrapper.loop.run_until_complete(self.device.async_turn_on())
return True
+ @overrides
def turn_off(self) -> bool:
self.lazy_initialize_device()
- self.meross_wrapper.loop.run_until_complete(
- self.device.async_turn_off()
- )
+ assert self.meross_wrapper
+ assert self.device
+ self.meross_wrapper.loop.run_until_complete(self.device.async_turn_off())
return True
+ @overrides
def is_on(self) -> bool:
self.lazy_initialize_device()
+ assert self.device
return self.device.is_on()
+ @overrides
def is_off(self) -> bool:
return not self.is_on()
diff --git a/smart_home/registry.py b/smart_home/registry.py
index 7349081..16e18ba 100644
--- a/smart_home/registry.py
+++ b/smart_home/registry.py
@@ -8,15 +8,15 @@ import argparse_utils
import config
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.device as device
import smart_home.lights as lights
import smart_home.outlets as outlets
args = config.add_commandline_args(
f"Smart Home Registry ({__file__})",
- "Args related to the smart home configuration registry."
+ "Args related to the smart home configuration registry.",
)
args.add_argument(
'--smart_home_registry_file_location',
@@ -32,9 +32,9 @@ logger = logging.getLogger(__file__)
class SmartHomeRegistry(object):
def __init__(
- self,
- registry_file: Optional[str] = None,
- filters: List[str] = ['smart'],
+ self,
+ registry_file: Optional[str] = None,
+ filters: List[str] = ['smart'],
) -> None:
self._macs_by_name = {}
self._keywords_by_name = {}
@@ -44,13 +44,11 @@ class SmartHomeRegistry(object):
# Read the disk config file...
if registry_file is None:
- registry_file = config.config[
- 'smart_home_registry_file_location'
- ]
+ registry_file = config.config['smart_home_registry_file_location']
assert file_utils.does_file_exist(registry_file)
logger.debug(f'Reading {registry_file}')
- with open(registry_file, "r") as f:
- contents = f.readlines()
+ with open(registry_file, "r") as rf:
+ contents = rf.readlines()
# Parse the contents...
for line in contents:
diff --git a/type/centcount.py b/type/centcount.py
index 4e5b8a6..13f14b7 100644
--- a/type/centcount.py
+++ b/type/centcount.py
@@ -1,27 +1,18 @@
#!/usr/bin/env python3
import re
-from typing import Optional, TypeVar, Tuple
+from typing import Optional, Tuple, TypeVar
import math_utils
-T = TypeVar('T', bound='CentCount')
-
-
class CentCount(object):
"""A class for representing monetary amounts potentially with
different currencies meant to avoid floating point rounding
issues by treating amount as a simple integral count of cents.
"""
- def __init__ (
- self,
- centcount,
- currency: str = 'USD',
- *,
- strict_mode = False
- ):
+ def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
self.strict_mode = strict_mode
if isinstance(centcount, str):
ret = CentCount._parse(centcount)
@@ -37,7 +28,7 @@ class CentCount(object):
if not currency:
self.currency: Optional[str] = None
else:
- self.currency: Optional[str] = currency
+ self.currency = currency
def __repr__(self):
a = float(self.centcount)
@@ -59,8 +50,7 @@ class CentCount(object):
if isinstance(other, CentCount):
if self.currency == other.currency:
return CentCount(
- centcount = self.centcount + other.centcount,
- currency = self.currency
+ centcount=self.centcount + other.centcount, currency=self.currency
)
else:
raise TypeError('Incompatible currencies in add expression')
@@ -74,8 +64,7 @@ class CentCount(object):
if isinstance(other, CentCount):
if self.currency == other.currency:
return CentCount(
- centcount = self.centcount - other.centcount,
- currency = self.currency
+ centcount=self.centcount - other.centcount, currency=self.currency
)
else:
raise TypeError('Incompatible currencies in add expression')
@@ -90,8 +79,7 @@ class CentCount(object):
raise TypeError('can not multiply monetary quantities')
else:
return CentCount(
- centcount = int(self.centcount * float(other)),
- currency = self.currency
+ centcount=int(self.centcount * float(other)), currency=self.currency
)
def __truediv__(self, other):
@@ -99,8 +87,8 @@ class CentCount(object):
raise TypeError('can not divide monetary quantities')
else:
return CentCount(
- centcount = int(float(self.centcount) / float(other)),
- currency = self.currency
+ centcount=int(float(self.centcount) / float(other)),
+ currency=self.currency,
)
def __int__(self):
@@ -125,8 +113,7 @@ class CentCount(object):
if isinstance(other, CentCount):
if self.currency == other.currency:
return CentCount(
- centcount = other.centcount - self.centcount,
- currency = self.currency
+ centcount=other.centcount - self.centcount, currency=self.currency
)
else:
raise TypeError('Incompatible currencies in sub expression')
@@ -135,8 +122,7 @@ class CentCount(object):
raise TypeError('In strict_mode only two moneys can be added')
else:
return CentCount(
- centcount = int(other) - self.centcount,
- currency = self.currency
+ centcount=int(other) - self.centcount, currency=self.currency
)
__rmul__ = __mul__
@@ -148,10 +134,7 @@ class CentCount(object):
if other is None:
return False
if isinstance(other, CentCount):
- return (
- self.centcount == other.centcount and
- self.currency == other.currency
- )
+ return self.centcount == other.centcount and self.currency == other.currency
if self.strict_mode:
raise TypeError("In strict mode only two CentCounts can be compared")
else:
@@ -196,8 +179,8 @@ class CentCount(object):
def __hash__(self):
return self.__repr__
- CENTCOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
- CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
+ CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
+ CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
@classmethod
def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
@@ -220,7 +203,7 @@ class CentCount(object):
return None
@classmethod
- def parse(cls, s: str) -> T:
+ def parse(cls, s: str) -> 'CentCount':
chunks = CentCount._parse(s)
if chunks is not None:
return CentCount(chunks[0], chunks[1])
diff --git a/type/money.py b/type/money.py
index 290c2c8..d7e6ffa 100644
--- a/type/money.py
+++ b/type/money.py
@@ -1,26 +1,23 @@
#!/usr/bin/env python3
-from decimal import Decimal
import re
-from typing import Optional, TypeVar, Tuple
+from decimal import Decimal
+from typing import Optional, Tuple, TypeVar
import math_utils
-T = TypeVar('T', bound='Money')
-
-
class Money(object):
"""A class for representing monetary amounts potentially with
different currencies.
"""
- def __init__ (
- self,
- amount: Decimal = Decimal("0"),
- currency: str = 'USD',
- *,
- strict_mode = False
+ def __init__(
+ self,
+ amount: Decimal = Decimal("0"),
+ currency: str = 'USD',
+ *,
+ strict_mode=False,
):
self.strict_mode = strict_mode
if isinstance(amount, str):
@@ -35,7 +32,7 @@ class Money(object):
if not currency:
self.currency: Optional[str] = None
else:
- self.currency: Optional[str] = currency
+ self.currency = currency
def __repr__(self):
a = float(self.amount)
@@ -55,10 +52,7 @@ class Money(object):
def __add__(self, other):
if isinstance(other, Money):
if self.currency == other.currency:
- return Money(
- amount = self.amount + other.amount,
- currency = self.currency
- )
+ return Money(amount=self.amount + other.amount, currency=self.currency)
else:
raise TypeError('Incompatible currencies in add expression')
else:
@@ -66,17 +60,13 @@ class Money(object):
raise TypeError('In strict_mode only two moneys can be added')
else:
return Money(
- amount = self.amount + Decimal(float(other)),
- currency = self.currency
+ amount=self.amount + Decimal(float(other)), currency=self.currency
)
def __sub__(self, other):
if isinstance(other, Money):
if self.currency == other.currency:
- return Money(
- amount = self.amount - other.amount,
- currency = self.currency
- )
+ return Money(amount=self.amount - other.amount, currency=self.currency)
else:
raise TypeError('Incompatible currencies in add expression')
else:
@@ -84,8 +74,7 @@ class Money(object):
raise TypeError('In strict_mode only two moneys can be added')
else:
return Money(
- amount = self.amount - Decimal(float(other)),
- currency = self.currency
+ amount=self.amount - Decimal(float(other)), currency=self.currency
)
def __mul__(self, other):
@@ -93,8 +82,7 @@ class Money(object):
raise TypeError('can not multiply monetary quantities')
else:
return Money(
- amount = self.amount * Decimal(float(other)),
- currency = self.currency
+ amount=self.amount * Decimal(float(other)), currency=self.currency
)
def __truediv__(self, other):
@@ -102,8 +90,7 @@ class Money(object):
raise TypeError('can not divide monetary quantities')
else:
return Money(
- amount = self.amount / Decimal(float(other)),
- currency = self.currency
+ amount=self.amount / Decimal(float(other)), currency=self.currency
)
def __float__(self):
@@ -124,10 +111,7 @@ class Money(object):
def __rsub__(self, other):
if isinstance(other, Money):
if self.currency == other.currency:
- return Money(
- amount = other.amount - self.amount,
- currency = self.currency
- )
+ return Money(amount=other.amount - self.amount, currency=self.currency)
else:
raise TypeError('Incompatible currencies in sub expression')
else:
@@ -135,8 +119,7 @@ class Money(object):
raise TypeError('In strict_mode only two moneys can be added')
else:
return Money(
- amount = Decimal(float(other)) - self.amount,
- currency = self.currency
+ amount=Decimal(float(other)) - self.amount, currency=self.currency
)
__rmul__ = __mul__
@@ -148,10 +131,7 @@ class Money(object):
if other is None:
return False
if isinstance(other, Money):
- return (
- self.amount == other.amount and
- self.currency == other.currency
- )
+ return self.amount == other.amount and self.currency == other.currency
if self.strict_mode:
raise TypeError("In strict mode only two Moneys can be compared")
else:
@@ -196,8 +176,8 @@ class Money(object):
def __hash__(self):
return self.__repr__
- AMOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
- CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
+ AMOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
+ CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
@classmethod
def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
@@ -220,7 +200,7 @@ class Money(object):
return None
@classmethod
- def parse(cls, s: str) -> T:
+ def parse(cls, s: str) -> 'Money':
chunks = Money._parse(s)
if chunks is not None:
return Money(chunks[0], chunks[1])
diff --git a/type/rate.py b/type/rate.py
index 3161131..64a4726 100644
--- a/type/rate.py
+++ b/type/rate.py
@@ -5,11 +5,11 @@ from typing import Optional
class Rate(object):
def __init__(
- self,
- multiplier: Optional[float] = None,
- *,
- percentage: Optional[float] = None,
- percent_change: Optional[float] = None,
+ self,
+ multiplier: Optional[float] = None,
+ *,
+ percentage: Optional[float] = None,
+ percent_change: Optional[float] = None,
):
count = 0
if multiplier is not None:
@@ -17,7 +17,7 @@ class Rate(object):
multiplier = multiplier.replace('%', '')
m = float(multiplier)
m /= 100
- self.multiplier = m
+ self.multiplier: float = m
else:
self.multiplier = multiplier
count += 1
@@ -78,10 +78,7 @@ class Rate(object):
def __hash__(self):
return self.multiplier
- def __repr__(self,
- *,
- relative=False,
- places=3):
+ def __repr__(self, *, relative=False, places=3):
if relative:
percentage = (self.multiplier - 1.0) * 100.0
else: