import json from . import validate class Field(object): """ Instances of this class act as descriptors on the Thermostat class. They define a piece of data from the API that can be accessed with GET and optionally with POST. """ def __init__(self, url, name, human_value_map=None, post_url=None, post_name=None, validate_response=validate.validate_response): """ :param url: relative URL to use for GET and POST, except when post_url is defined. :param name: key value for the data point you want. Most responses come as a dictionary, for example {'temp' : 73}, so you would pass 'temp' for this argument. :param human_value_map: An optional dictionary where keys are actual values the server might return, and values are a human-readable form. This is useful for example when a return value of "0" actually means "Off". If you pass this argument, make sure it includes all possible values. :param post_url: A few items require you to POST to a different URL than where you GET. Use this argument to specify a POST URL, and the 'url' argument will continue to be used for GETs. :param post_name: A few items that can be set are not available for reading, such as it_heat. Use this argument to specify a POST variable which is different from the GET variable. :param validate_response: The default validate_response function. Use this argument to override it. """ self.url = url self.name = name self.human_value_map = human_value_map self.post_url = post_url self.post_name = post_name self.validate_response = validate_response def __get__(self, instance, owner): response = instance.get(self.url) envelope = json.loads(response.read().decode('utf-8')) self.validate_response(response, envelope) return self._build_get_return(envelope) def _build_get_return(self, envelope): """ :param envelope: raw value returned from the thermostat, which usually is a dict. There are some attributes that don't come with an envelope, and are just the raw value by itself. In that case, set name=None. :returns: dict with key 'raw' whose value is the value this Field is setup to return. For example, it might be the current temp. If there is a human_value_map, this dict will have an additional 'human' key whose value is a human-readable version of the raw value. """ # Some URLs, like /tstat, don't have an envelope ret = {'raw' : envelope.get(self.name) if self.name else envelope} if self.human_value_map: ret['human'] = self._convert_to_human(ret['raw']) return ret def __set__(self, instance, value): data = json.dumps({self.post_name or self.name: value}).encode('utf-8') response = instance.post(self.post_url or self.url, data) self.validate_response(response) def _convert_to_human(self, value): """ :param value: raw value retrieved from the thermostat and removed from its envelope :returns: human-readable version of the value, as retrieved from self.human_value_map """ try: return self.human_value_map[value] except KeyError: raise AttributeError('Human readable value not known for raw value %s' % (str(value))) class ReadOnlyField(Field): """For read-only values like the current temperature""" def __set__(self, instance, value): raise TypeError('This attribute does not support writes.') class WriteOnlyField(Field): """For write-only values like the remote temperature""" def __get__(self, instance, owner): raise TypeError('This attribute does not support reads.')