Skip to content

Commit

Permalink
Merge pull request maximvelichko#113 from brandond/guard-no-PK_Device
Browse files Browse the repository at this point in the history
Fix flake8 tests; guard against missing fields in device and alert responses
  • Loading branch information
pavoni committed Aug 1, 2019
2 parents 854164e + e1206bd commit 7462194
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 51 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
ignore = E129 W504
max-line-length = 160
58 changes: 30 additions & 28 deletions pyvera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import logging
import requests
import sys
import json
import os
import time
from datetime import datetime
Expand Down Expand Up @@ -149,9 +148,9 @@ def get_device_by_name(self, device_name):
found_device = None
for device in self.get_devices():
if device.name == device_name:
found_device = device
# found the first (and should be only) one so we will finish
break
found_device = device
# found the first (and should be only) one so we will finish
break

if found_device is None:
logger.debug('Did not find device with {}'.format(device_name))
Expand All @@ -168,9 +167,9 @@ def get_device_by_id(self, device_id):
found_device = None
for device in self.get_devices():
if device.device_id == device_id:
found_device = device
# found the first (and should be only) one so we will finish
break
found_device = device
# found the first (and should be only) one so we will finish
break

if found_device is None:
logger.debug('Did not find device with {}'.format(device_id))
Expand Down Expand Up @@ -201,8 +200,8 @@ def get_devices(self, category_filter=''):
device_category = item.get('deviceInfo').get('category')
if device_category == CATEGORY_DIMMER:
device = VeraDimmer(item, item_alerts, self)
elif ( device_category == CATEGORY_SWITCH or
device_category == CATEGORY_VERA_SIREN):
elif (device_category == CATEGORY_SWITCH or
device_category == CATEGORY_VERA_SIREN):
device = VeraSwitch(item, item_alerts, self)
elif device_category == CATEGORY_THERMOSTAT:
device = VeraThermostat(item, item_alerts, self)
Expand Down Expand Up @@ -302,7 +301,7 @@ def get_changed_devices(self, timestamp):

# double the timeout here so requests doesn't timeout before vera
logger.debug("get_changed_devices() requesting payload %s", str(payload))
r = self.data_request(payload, TIMEOUT*2)
r = self.data_request(payload, TIMEOUT * 2)
r.raise_for_status()

# If the Vera disconnects before writing a full response (as lu_sdata
Expand All @@ -321,8 +320,9 @@ def get_changed_devices(self, timestamp):
except ValueError as ex:
raise PyveraError("JSON decode error: " + str(ex))

if not ( type(result) is dict
and 'loadtime' in result and 'dataversion' in result ):
if not (type(result) is dict and
'loadtime' in result and
'dataversion' in result):
raise PyveraError("Unexpected/garbled response from Vera")

# At this point, all good. Update timestamp and return change data.
Expand All @@ -347,20 +347,20 @@ def get_alerts(self, timestamp):
r.raise_for_status()

if r.text == "":
raise PyVeraError("Empty response from Vera")
raise PyveraError("Empty response from Vera")

try:
result = r.json()
except ValueError as ex:
raise PyVeraError("JSON decode error: " + str(ex))
raise PyveraError("JSON decode error: " + str(ex))

if not ( type(result) is dict
and 'LoadTime' in result and 'DataVersion' in result ):
if not (type(result) is dict and
'LoadTime' in result and
'DataVersion' in result):
raise PyveraError("Unexpected/garbled response from Vera")

return result.get('alerts', [])


def start(self):
"""Start the subscription thread."""
self.subscription_registry.start()
Expand Down Expand Up @@ -496,8 +496,8 @@ def set_service_value(self, service_id, set_name, parameter_name, value):
}
result = self.vera_request(**payload)
logger.debug("set_service_value: "
"result of vera_request with payload %s: %s",
payload, result.text)
"result of vera_request with payload %s: %s",
payload, result.text)

def call_service(self, service_id, action):
"""Call a Vera service.
Expand All @@ -507,8 +507,8 @@ def call_service(self, service_id, action):
result = self.vera_request(id='action', serviceId=service_id,
action=action)
logger.debug("call_service: "
"result of vera_request with id %s: %s", service_id,
result.text)
"result of vera_request with id %s: %s", service_id,
result.text)
return result

def set_cache_value(self, name, value):
Expand All @@ -521,7 +521,7 @@ def set_cache_value(self, name, value):
dev_info = self.json_state.get('deviceInfo')
if dev_info.get(name.lower()) is None:
logger.error("Could not set %s for %s (key does not exist).",
name, self.name)
name, self.name)
logger.error("- dictionary %s", dev_info)
return
dev_info[name.lower()] = str(value)
Expand Down Expand Up @@ -1014,8 +1014,10 @@ def is_locked(self, refresh=False):
# or the locking action took too long
# then reset the target and time
now = time.time()
if self.lock_target is not None and (self.lock_target[0] == self.get_value("locked") or now - self.lock_target[1] >= LOCK_TARGET_TIMEOUT_SEC):
logger.debug("Resetting lock target for {} ({}=={}, {} - {} >= {})".format(self.name, self.lock_target[0], self.get_value("locked"), now, self.lock_target[1], LOCK_TARGET_TIMEOUT_SEC))
if (self.lock_target is not None and
(self.lock_target[0] == self.get_value("locked") or now - self.lock_target[1] >= LOCK_TARGET_TIMEOUT_SEC)):
logger.debug("Resetting lock target for {} ({}=={}, {} - {} >= {})".format(
self.name, self.lock_target[0], self.get_value("locked"), now, self.lock_target[1], LOCK_TARGET_TIMEOUT_SEC))
self.lock_target = None

locked = self.get_value("locked") == '1'
Expand Down Expand Up @@ -1043,7 +1045,7 @@ def get_last_user(self, refresh=False):
logger.error('Got unsupported user string {}: {}'.format(val, ex))
return None

return ( userid, username )
return (userid, username)

def get_pin_failed(self, refresh=False):
"""True when a bad PIN code was entered"""
Expand Down Expand Up @@ -1102,7 +1104,7 @@ def get_pin_codes(self, refresh=False):
codes.append((slot, name, pin))
except Exception as ex:
logger.error('Problem parsing pin code string {}: {}'.format(code, ex))

return codes

@property
Expand Down Expand Up @@ -1304,8 +1306,8 @@ def activate(self):
}
result = self.vera_request(**payload)
logger.debug("activate: "
"result of vera_request with payload %s: %s",
payload, result.text)
"result of vera_request with payload %s: %s",
payload, result.text)

self._active = True

Expand Down
53 changes: 31 additions & 22 deletions pyvera/subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
# Get the logger for use in this module
logger = logging.getLogger(__name__)


class PyveraError(Exception):
pass


class SubscriptionRegistry(object):
"""Class for subscribing to wemo events."""

Expand All @@ -52,7 +54,6 @@ def register(self, device, callback):
self._devices[device.vera_device_id].append(device)
self._callbacks[device].append(callback)


def unregister(self, device, callback):
"""Remove a registered change callback.
Expand All @@ -67,29 +68,43 @@ def unregister(self, device, callback):
self._callbacks[device].remove(callback)
self._devices[device.vera_device_id].remove(device)


def _event(self, device_data_list, device_alert_list):
# Guard against invalid data from Vera API
if not isinstance(device_data_list, list):
logger.debug('Got invalid device_data_list: {}'.format(device_data_list))
device_data_list = []

if not isinstance(device_alert_list, list):
logger.debug('Got invalid device_alert_list: {}'.format(device_alert_list))
device_alert_list = []

# Find unique device_ids that have data across both device_data and alert_data
device_ids = set()
[device_ids.add(int(device_data['id'])) for device_data in device_data_list]
[device_ids.add(int(alert_data['PK_Device'])) for alert_data in device_alert_list]

for device_id in device_ids:
device_list = self._devices.get(device_id, ())
device_datas = [data for data in device_data_list if int(data['id']) == device_id]
device_alerts = [alert for alert in device_alert_list if int(alert['PK_Device']) == device_id]
for device_data in device_data_list:
if 'id' in device_data:
device_ids.add(device_data['id'])
else:
logger.debug('Got invalid device_data: {}'.format(device_data))

for alert_data in device_alert_list:
if 'PK_Device' in alert_data:
device_ids.add(alert_data['PK_Device'])
else:
logger.debug('Got invalid alert_data: {}'.format(alert_data))

device_data = device_datas[0] if device_datas else {}
for device_id in device_ids:
try:
device_list = self._devices.get(device_id, ())
device_datas = [data for data in device_data_list if data.get('id') == device_id]
device_alerts = [alert for alert in device_alert_list if alert.get('PK_Device') == device_id]

for device in device_list:
self._event_device(device, device_data, device_alerts)
device_data = device_datas[0] if device_datas else {}

for device in device_list:
self._event_device(device, device_data, device_alerts)
except Exception as e:
logger.exception('Error processing event for device_id {}: {}'.format(device_id, e))

def _event_device(self, device, device_data, device_alerts):
if device is None:
Expand All @@ -100,10 +115,7 @@ def _event_device(self, device, device_data, device_alerts):
comment = device_data.get('comment', '')
sending = comment.find('Sending') >= 0
logger.debug("Event: %s, state %s, alerts %s, %s",
device.name,
state,
len(device_alerts),
json.dumps(device_data))
device.name, state, len(device_alerts), json.dumps(device_data))
device.set_alerts(device_alerts)
if sending and state == STATE_NO_JOB:
state = STATE_JOB_WAITING_TO_START
Expand Down Expand Up @@ -134,15 +146,13 @@ def _event_device(self, device, device_data, device_alerts):
(state == STATE_JOB_ERROR and
comment.find('Setting user configuration'))):
logger.error("Device %s, state %s, %s",
device.name,
state,
comment)
device.name, state, comment)
return
device.update(device_data)
for callback in self._callbacks.get(device, ()):
try:
callback(device)
except:
except Exception:
# (Very) broad check to not let loosely-implemented callbacks
# kill our polling thread. They should be catching their own
# errors, so if it gets back to us, just log it and move on.
Expand Down Expand Up @@ -192,8 +202,7 @@ def _run_poll_server(self):
logger.debug("Non-fatal error in poll: %s", str(ex))
pass
except Exception as ex:
logger.exception("Vera poll thread general exception: %s",
str(ex))
logger.exception("Vera poll thread general exception: %s", str(ex))
raise
else:
logger.debug("Poll returned")
Expand All @@ -209,7 +218,7 @@ def _run_poll_server(self):
# After error, discard timestamp for fresh update. pyvera issue #89
timestamp = {'dataversion': 1, 'loadtime': 0}
logger.info("Could not poll Vera - will retry in %ss",
SUBSCRIPTION_RETRY)
SUBSCRIPTION_RETRY)
time.sleep(SUBSCRIPTION_RETRY)

logger.info("Shutdown Vera Poll Thread")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages

setup(name='pyvera',
version='0.3.2',
version='0.3.3',
description='Python API for talking to Vera Z-Wave controllers',
url='https://github.com/pavoni/pyvera',
author='James Cole, Greg Dowling',
Expand Down

0 comments on commit 7462194

Please sign in to comment.