Skip to content

Commit 0f79c42

Browse files
committed
Refactored to a more reasonable CLI
1 parent 0f7cb6b commit 0f79c42

File tree

4 files changed

+91
-174
lines changed

4 files changed

+91
-174
lines changed

nest/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33

44
from .nest import Nest
55

6-
from .utils import CELSIUS
7-
from .utils import FAHRENHEIT
8-
96
logging.getLogger(__name__).addHandler(logging.NullHandler())
107

11-
__all__ = ['CELSIUS', 'FAHRENHEIT', 'Nest']
8+
__all__ = ['Nest']

nest/command_line.py

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import time
1515
import sys
1616
import errno
17+
import json
1718

1819
from . import nest
19-
from . import utils
2020
from . import helpers
2121

2222
# use six for python2/python3 compatibility
@@ -63,9 +63,6 @@ def get_parser():
6363
help='keep showing update received from stream API '
6464
'in show and camera-show commands')
6565

66-
parser.add_argument('-c', '--celsius', dest='celsius', action='store_true',
67-
help='use celsius instead of farenheit')
68-
6966
parser.add_argument('-n', '--name', dest='name',
7067
help='optional, specify name of nest '
7168
'thermostat to talk to')
@@ -84,23 +81,26 @@ def get_parser():
8481

8582
subparsers = parser.add_subparsers(dest='command',
8683
help='command help')
87-
temp = subparsers.add_parser('temp', help='show/set temperature')
88-
89-
temp.add_argument('temperature', nargs='*', type=float,
90-
help='target temperature to set device to')
84+
show_trait = subparsers.add_parser('show_trait', help='show a trait')
85+
show_trait.add_argument('trait_name',
86+
help='name of trait to show')
9187

92-
subparsers.add_parser('target', help='show current temp target')
93-
subparsers.add_parser('humid', help='show current humidity')
88+
cmd = subparsers.add_parser('cmd', help='send a cmd')
89+
cmd.add_argument('cmd_name',
90+
help='name of cmd to send')
91+
cmd.add_argument('cmd_params',
92+
help='json for cmd params')
9493

9594
subparsers.add_parser('show', help='show everything')
9695

97-
9896
parser.set_defaults(**defaults)
9997
return parser
10098

99+
101100
def reautherize_callback(authorization_url):
102-
print('Please go to %s and authorize access.' % authorization_url)
103-
return input('Enter the full callback URL: ')
101+
print('Please go to %s and authorize access.' % authorization_url)
102+
return input('Enter the full callback URL: ')
103+
104104

105105
def main():
106106
parser = get_parser()
@@ -111,8 +111,8 @@ def main():
111111
logger.setLevel(logging.DEBUG)
112112
console_handler = logging.StreamHandler()
113113
formatter = logging.Formatter(
114-
"%(asctime)s %(levelname)s (%(threadName)s) "
115-
"[%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
114+
"%(asctime)s %(levelname)s (%(threadName)s) "
115+
"[%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
116116
console_handler.setFormatter(formatter)
117117
console_handler.setLevel(logging.DEBUG)
118118
logger.addHandler(console_handler)
@@ -158,32 +158,21 @@ def _identity(x):
158158
reautherize_callback=reautherize_callback) as napi:
159159

160160
if args.name:
161-
devices = napi.get_devices([args.name])
161+
devices = napi.get_devices(args.name)
162162

163163
elif args.structure:
164164
devices = napi.get_devices(None, args.structure)
165165

166166
else:
167167
devices = napi.get_devices()
168168

169-
170-
if not args.celsius:
171-
display_temp = utils.c_to_f
172-
173-
if cmd == 'temp':
174-
if args.temperature:
175-
devices[args.index].heat_setpoint = args.temperature[0]
176-
177-
print('%0.1f' % display_temp(devices[args.index].temperature))
178-
179-
elif cmd == 'humid':
180-
print(devices[args.index].humidity)
181-
182-
elif cmd == 'target':
183-
target = devices[args.index].heat_setpoint
184-
185-
print('%0.1f' % display_temp(target))
186-
169+
if cmd == 'show_trait':
170+
devices = nest.Device.filter_for_trait(devices, args.trait_name)
171+
print(devices[args.index].traits[args.trait_name])
172+
elif cmd == 'cmd':
173+
devices = nest.Device.filter_for_cmd(devices, args.cmd_name)
174+
print(devices[args.index].send_cmd(
175+
args.cmd_name, json.loads(args.cmd_params)))
187176
elif cmd == 'show':
188177
try:
189178
while True:
@@ -196,5 +185,6 @@ def _identity(x):
196185
except KeyboardInterrupt:
197186
return
198187

188+
199189
if __name__ == '__main__':
200190
main()

nest/nest.py

Lines changed: 66 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,9 @@
2727
REDIRECT_URI = 'https://www.google.com'
2828
SCOPE = ['https://www.googleapis.com/auth/sdm.service']
2929

30-
STRUCTURES = 'structures'
31-
THERMOSTAT_TYPE = 'sdm.devices.types.THERMOSTAT'
32-
DOORBELL_TYPE = 'sdm.devices.types.DOORBELL'
33-
3430
_LOGGER = logging.getLogger(__name__)
3531

32+
3633
class APIError(Exception):
3734
def __init__(self, response, msg=None):
3835
if response is None:
@@ -87,103 +84,73 @@ def __init__(self, response, msg=None):
8784

8885
self.response = response
8986

90-
class NestBase(object):
91-
def __init__(self, name, nest_api):
87+
88+
class Device():
89+
90+
def __init__(self, nest_api=None, name=None, device_data=None):
9291
self._name = name
9392
self._nest_api = nest_api
93+
self._device_data = device_data
9494

9595
def __str__(self):
96-
return '<%s: %s>' % (self.__class__.__name__, self._repr_name)
97-
98-
def _set(self, data):
99-
path = f'/{self.name}:executeCommand'
100-
101-
response = self._nest_api._put(path=path, data=data)
102-
103-
return response
96+
trait_str = ','.join([f'<{k}: {v}>' for k, v in self.traits.items()])
97+
return f'name: {self.name} where:{self.where} - {self.type}({trait_str})'
10498

10599
@property
106100
def name(self):
107-
return self._name
108-
109-
@property
110-
def _repr_name(self):
111-
return self.name
112-
101+
if self._device_data is not None:
102+
return self._device_data['name']
103+
else:
104+
return self._name.split('/')[-1]
113105

114-
class Device(NestBase):
115106
@property
116107
def _device(self):
117-
return next(device for device in self._devices if device['name'] == self.name)
108+
if self._device_data is not None:
109+
return self._device_data
110+
else:
111+
return next(device for device in self._devices if self.name in device['name'])
118112

119113
@property
120114
def _devices(self):
115+
if self._device_data is not None:
116+
raise RuntimeError("Invalid use of singular device")
121117
return self._nest_api._devices
122118

123-
@property
124-
def _repr_name(self):
125-
if self.name:
126-
return self.name
127-
128-
return self.where
129-
130-
def __repr__(self):
131-
return str(self._device)
132-
133119
@property
134120
def where(self):
135121
return self._device['parentRelations'][0]['displayName']
136122

137-
138-
class Thermostat(Device):
139-
#TODO: Fill in rest from https://developers.google.com/nest/device-access/traits/device/thermostat-eco
140-
141-
@property
142-
def humidity(self):
143-
return self._device['traits']['sdm.devices.traits.Humidity']['ambientHumidityPercent']
144-
145-
@property
146-
def mode(self):
147-
return self._device['traits']['sdm.devices.traits.ThermostatMode']['mode']
148-
149123
@property
150-
def temperature_scale(self):
151-
return self._device['traits']['sdm.devices.traits.Settings']['temperatureScale']
124+
def type(self):
125+
return self._device['type'].split('.')[-1]
152126

153127
@property
154-
def temperature(self):
155-
return self._device['traits']['sdm.devices.traits.Temperature']['ambientTemperatureCelsius']
128+
def traits(self):
129+
return {k.split('.')[-1]: v for k, v in self._device['traits'].items()}
156130

157131
@property
158-
def hvac_state(self):
159-
return self._device['traits']['sdm.devices.traits.ThermostatHvac']['status']
160-
161-
@property
162-
def heat_setpoint(self):
163-
return self._device['traits']['sdm.devices.traits.ThermostatTemperatureSetpoint']['heatCelsius']
164-
165-
@heat_setpoint.setter
166-
def heat_setpoint(self, value):
167-
self._set({
168-
"command" : "sdm.devices.commands.ThermostatMode.SetHeat",
169-
"params" : {
170-
"heatCelsius" : value
171-
}
172-
})
173-
174-
def __str__(self):
175-
fields = ['name', 'where', 'temperature', 'humidity', 'heat_setpoint', 'hvac_state']
176-
properties = ' '.join([f'{field}={getattr(self, field)}' for field in fields])
177-
return f'Thermostat({properties})'
132+
def traits(self):
133+
return {k.split('.')[-1]: v for k, v in self._device['traits'].items()}
178134

135+
def send_cmd(self, cmd, params):
136+
cmd = '.'.join(cmd.split('.')[-2:])
137+
path = f'/{self.name}:executeCommand'
138+
data = {
139+
"command": "sdm.devices.commands." + cmd,
140+
'params': params
141+
}
142+
response = self._nest_api._put(path=path, data=data)
143+
return response
179144

180-
class Doorbell(Device):
181-
#TODO: Fill in rest from https://developers.google.com/nest/device-access/traits/device/camera-event-image
145+
@staticmethod
146+
def filter_for_trait(devices, trait):
147+
trait = trait.split('.')[-1]
148+
return [device for device in devices if trait in device.traits]
182149

183-
def __str__(self):
184-
fields = ['name', 'where']
185-
properties = ' '.join([f'{field}={getattr(self, field)}' for field in fields])
186-
return f'Doorbell({properties})'
150+
@staticmethod
151+
def filter_for_cmd(devices, cmd):
152+
trait = cmd.split('.')[-2]
153+
return Device.filter_for_trait(devices, trait)
187154

188155

189156
class Nest(object):
@@ -204,27 +171,28 @@ def __init__(self,
204171
self._devices_value = {}
205172

206173
if not access_token:
207-
try:
208-
with open(self._access_token_cache_file, 'r') as fd:
209-
access_token = json.load(fd)
210-
_LOGGER.debug("Loaded access token from %s",
211-
self._access_token_cache_file)
212-
except:
213-
_LOGGER.warn("Token load failed from %s",
214-
self._access_token_cache_file)
174+
try:
175+
with open(self._access_token_cache_file, 'r') as fd:
176+
access_token = json.load(fd)
177+
_LOGGER.debug("Loaded access token from %s",
178+
self._access_token_cache_file)
179+
except:
180+
_LOGGER.warn("Token load failed from %s",
181+
self._access_token_cache_file)
215182
if access_token:
216183
self._client = OAuth2Session(self._client_id, token=access_token)
217184

218185
def __save_token(self, token):
219186
with open(self._access_token_cache_file, 'w') as fd:
220187
json.dump(token, fd)
221188
_LOGGER.debug("Save access token to %s",
222-
self._access_token_cache_file)
189+
self._access_token_cache_file)
223190

224191
def __reauthorize(self):
225192
if self._reautherize_callback is None:
226193
raise AuthorizationError(None, 'No callback to handle OAuth URL')
227-
self._client = OAuth2Session(self._client_id, redirect_uri=REDIRECT_URI, scope=SCOPE)
194+
self._client = OAuth2Session(
195+
self._client_id, redirect_uri=REDIRECT_URI, scope=SCOPE)
228196

229197
authorization_url, state = self._client.authorization_url(
230198
AUTHORIZE_URL.format(project_id=self._project_id),
@@ -252,8 +220,8 @@ def _request(self, verb, path, data=None):
252220
try:
253221
_LOGGER.debug(">> %s %s", verb, url)
254222
r = self._client.request(verb, url,
255-
allow_redirects=False,
256-
data=data)
223+
allow_redirects=False,
224+
data=data)
257225
_LOGGER.debug(f"<< {r.status_code}")
258226
if r.status_code == 200:
259227
return r.json()
@@ -267,13 +235,15 @@ def _request(self, verb, path, data=None):
267235
'client_secret': self._client_secret,
268236
}
269237
_LOGGER.debug(">> refreshing token")
270-
token = self._client.refresh_token(ACCESS_TOKEN_URL, **extra)
238+
token = self._client.refresh_token(
239+
ACCESS_TOKEN_URL, **extra)
271240
self.__save_token(token)
272241
if attempt > 1:
273-
raise AuthorizationError(None, 'Repeated TokenExpiredError')
242+
raise AuthorizationError(
243+
None, 'Repeated TokenExpiredError')
274244
continue
275245
self.__reauthorize()
276-
246+
277247
def _put(self, path, data=None):
278248
pieces = path.split('/')
279249
path = '/' + pieces[-1]
@@ -295,26 +265,15 @@ def _devices(self):
295265
" %s", error)
296266
return self._devices_value
297267

298-
def thermostats(self, where=None):
299-
names = [ device['name'] for device in
300-
self._devices if device['type'] == THERMOSTAT_TYPE and (where is None or device['parentRelations'][0]['displayName'] in where)
301-
]
302-
return [Thermostat(name, self) for name in names]
303-
304-
def doorbells(self, where=None):
305-
names = [ device['name'] for device in
306-
self._devices if device['type'] == DOORBELL_TYPE and (where is None or device['parentRelations'][0]['displayName'] in where)
307-
]
308-
return [Doorbell(name, self) for name in names]
309-
310-
def get_devices(self, names=None, where=None):
268+
def get_devices(self, names=None, wheres=None, types=None):
311269
ret = []
312270
for device in self._devices:
313-
if (names is None or device['name'] in names) and (where is None or device['parentRelations'][0]['displayName'] in where):
314-
ret.append({
315-
THERMOSTAT_TYPE: Thermostat,
316-
DOORBELL_TYPE: Doorbell
317-
}[device['type']](device['name'], self))
271+
obj = Device(device_data=device)
272+
name_match = (names is None or obj.name in names)
273+
where_match = (wheres is None or obj.where in wheres)
274+
type_match = (types is None or obj.type in types)
275+
if name_match and where_match and type_match:
276+
ret.append(Device(nest_api=self, name=obj.name))
318277
return ret
319278

320279
def __enter__(self):

0 commit comments

Comments
 (0)