#!/usr/bin/python
"""
Class to interface with BBQ Guru's CyberQ Temperature Control System
**ChangeLog:**
=== ========== ========== ======================================================
Ver Date Editor Notes
=== ========== ========== ======================================================
0.1 09/29/2012 Bryan Kemp Initial Commit
0.9 11/03/2012 Bryan Kemp Added distools functionality
1.0 03/29/2013 Bryan Kemp First release
=== ========== ========== ======================================================
"""
__author__ = "Bryan Kemp <bryan@thebrilliantidea.com>"
__version__ = "1.0"
__date__ = '03/29/2013'
__license__ = "BSD New"
__license_text__ = """
Copyright (c) 2013, The Brilliant Idea
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the The Brilliant Idea, LLC. nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import requests
from lxml import objectify
from cyberqinterface_exceptions import *
[docs]class CyberQInterface:
"""
Web Interface to BBQ Guru's CyberQ Temperature Controller System.
"""
def __init__(self, host=None, headers=None):
"""
**Description:**
Initialiazer
** Keyword arguments:**
* **<String>** The hostname or IP of the CyberQ
* (optional) **<Dictionary>** Header Name: Header Value
Returns:
<object> CyberQInterface
**Example Usage:**
.. code-block:: python
cqi = CyberQInterface("10.0.1.5", {
"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain"} )
"""
self.validParameters = ["COOK_NAME", "COOK_SET", "FOOD1_NAME",
"FOOD1_SET", "FOOD2_NAME", "FOOD2_SET",
"FOOD3_NAME", "FOOD3_SET", "_COOK_TIMER",
"COOK_TIMER", "COOKHOLD", "TIMEOUT_ACTION",
"ALARMDEV", "COOK_RAMP", "OPENDETECT",
"CYCTIME", "PROPBAND", "MENU_SCROLLING",
"LCD_BACKLIGHT", "LCD_CONTRAST", "DEG_UNITS",
"ALARM_BEEPS", "KEY_BEEPS"]
if headers == None:
self.headers = {"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain"}
else:
self.headers = headers
self.host = host
self.url = "http://"+host+"/"
[docs] def sendUpdate(self, parameters):
"""
**Description:**
sendUpdate validates new parameters and sends update to CyberQ
**Possible parameters:**
============== ========================================================
Parameter Name Definition
============== ========================================================
COOK_NAME Pit Sensor name in plain text
COOK_SET Pit probe target temp in current units
FOOD1_NAME Food 1 name in plain text
FOOD1_SET Food probe 1 target temp in current units
FOOD2_NAME Food 2 name in plain text
FOOD2_SET Food probe 2 target temp in current units
FOOD3_NAME Food 3 name in plain text
FOOD3_SET Food probe 3 target temp in current units
_COOK_TIMER Set the countdown timer HH:MM:SS (must use urlencoded
colons - \%3A
COOK_TIMER Same as above - looks like you need to set both to keep
changes across refresh?
COOKHOLD Cook and hold target temp in current units if timer is
set to HOLD
TIMEOUT_ACTION What to do when timer hits 00:00:00 (0: No Action, 1:
HOLD, 2: Alarm, 3:Shutdown) See 8.3.2 in manual
ALARMDEV Alarm deviation setpoint in current units (see 8.3.3 in
Manual)
COOK_RAMP Which probe to use for Ramp mode (0: Off, 1: Food 1, 2:
Food 2, 3:Food 3)
OPENDETECT Enable/Disable Lid Open detection (0: Off, 1:On)
CYCTIME Fan Cycle time in s (between 4 and 10 seconds)
PROPBAND Proportional band size (between 5-100 degF)
MENU_SCROLLING Enable/Disable LCD scrolling (0: Off, 1:On)
LCD_BACKLIGHT Enable/Disable LCD backlight (0: Off, 1:On)
LCD_CONTRAST Contrast percent?
DEG_UNITS Master Switch for degC/degF (0:degC, 1:degF)
ALARM_BEEPS Alarm beeps (0-5)
KEY_BEEPS Enable/Disable key beeps (0: Off, 1:On)
============== ========================================================
**Keyword arguments:**
*<dictionary>* Dictionary of values to be updated. Note: will be validated against list of known values
**Returns:**
*<Boolean>* True if successful / False if not successful
**Example Usage:**
.. code-block:: python
cqi.sendUpdate({'FOOD1_NAME' : "Tri-Tip Roast",
'FOOD1_SET': '140',
'COOK_SET' : '300'})
"""
results = self._validateParameters(parameters)
if results != {}:
raise ParameterValidationException("Bad parameters passed", results)
response = requests.post(self.url, data=parameters,
headers=self.headers)
if response.status_code == 200:
return True
else:
raise ResponseHTTPException("%s Error: %s %s" %
(response.status_code, response.url,
response.reason),response)
def _validateParameters(self, parameters):
"""
Test all parameters against known set of keys
#TODO: Provide better tests for values
Keyword arguments:
<dictionary> parameters - Key/Value pairs for CyberQ settings
Returns:
<string> {} - returns dictionary of all invalid parameters with reason
Example Usage:
self._validateParameters({'FOOD1_NAME' : "Tri-Tip Roast", 'FOOD1_SET': '140',
'COOK_SET' : '300'})
"""
badParameters = {}
for key in parameters.keys():
if key not in self.validParameters:
badParameters[key] = "Not a valid parameter"
return badParameters
def _getResponseObject(self, xml):
"""
get data from CyberQ and return as an object
Keyword arguments:
<string> objectType
Returns:
Object of specifiedtype
Example Usage:
private
"""
try:
return objectify.fromstring(xml)
except(Exception):
raise ResponseValidationException("Invalid XML from CyberQ",
xml)
def _getResponseXML(self, objectURI):
"""
get data from CyberQ and return an XML
Keyword arguments:
<string> objectType
Returns:
XML
Example Usage:
private
"""
response = requests.get(self.url+objectURI)
if response.status_code == 200:
return response.text
else:
raise ResponseHTTPException("%s Error: %s %s" %
(response.status_code, response.url,
response.reason),response)
[docs] def getConfig(self):
"""
Get Configuration from CyberQ
Keyword arguments:
None
Returns:
Config Object
Example Usage:
print cqi.getConfig().FOOD1_TEMP
"""
return self._getResponseObject(self.getConfigXML())
[docs] def getStatus(self):
"""
Get Status from CyberQ
Keyword arguments:
None
Returns:
Status Object
Example Usage:
print cqi.getStatus().FOOD1_TEMP
"""
return self._getResponseObject(self.getStatusXML())
[docs] def getAll(self):
"""
Get All parameters from CyberQ
Keyword arguments:
None
Returns:
All Object
Example Usage:
cqi.getAll()
"""
return self._getResponseObject(self.getAllXML())
[docs] def getConfigXML(self):
"""
Get ConfigXML from CyberQ
Keyword arguments:
None
Returns:
Config Object in XML
Example Usage:
cqi.getStatus()
"""
return self._getResponseXML("config.xml")
[docs] def getStatusXML(self):
"""
Get StatusXML from CyberQ
Keyword arguments:
None
Returns:
Status Object
Example Usage:
cqi.getStatusXML()
"""
return self._getResponseXML("status.xml")
[docs] def getAllXML(self):
"""
Get AllXML from CyberQ
Keyword arguments:
None
Returns:
All Xml
Example Usage:
cqi.getAllXML()
"""
return self._getResponseXML("all.xml")
def _lookup(self, table, code):
"""
Lookup internal values by code. There are several values in the API
returned as Integers. This provides a means to look up a more useful
text reprentation
Keyword arguments:
<String> table - name of the table to find the code
<int> code - number of the code returned by the API.
Returns:
String
Raises: LookupException
Example Usage:
private
"""
codes = {
"status" : ["OK", "HIGH", "LOW", "DONE", "ERROR", "HOLD", "ALARM",
"SHUTDOWN"],
"temperature" : ["CELSIUS", "FAHRENHEIT"],
"ramp" : ["OFF", "FOOD1", "FOOD2", "FOOD3"]
}
if isinstance(code, objectify.IntElement):
code = int(code)
if not codes.has_key(table):
raise LookupException("No lookup table for: %s", table)
try:
return codes[table][code]
except IndexError as e:
raise LookupException("No value for code %s in lookup table %s" %
(code, table), e)
[docs] def statusLookup(self, code):
"""
Provides a text meaning for a given status code
"status" : ["OK", "HIGH", "LOW", "DONE", "ERROR", "HOLD", "ALARM",
"SHUTDOWN"],
============== ============
Value from API String Value
============== ============
0 OK
1 HIGH
2 LOW
3 DONE
4 ERROR
5 HOLD
6 ALARM
7 SHUTDOWN
============== ============
Keyword arguments:
<int> code - the code returned in the object or the XML.
Returns:
<String> Meaning behind the code
Example Usage:
cqi.statusLookup(cqi.getConfig().FOOD1_STATUS)
"""
return self._lookup("status", code)
[docs] def temperatureLookup(self, code):
"""
Provides a text meaning for the temperature scale in use
temperature" : ["CELSIUS", "FAHRENHEIT"]
============== ============
Value from API String Value
============== ============
0 CELSIUS
1 FAHRENHEIT
============== ============
Keyword arguments:
<int> code - the code returned in the object or XML.
Returns:
<String> 'Celsius' or 'Fahrenheit'
Example Usage:
cqi.temperatureLookup(cqi.getStatus().DEG_UNITS)
"""
return self._lookup("temperature", code)
[docs] def rampLookup(self, code):
"""
Provides a text representation of the food that is monitored for ramping
============== ============
Value from API String Value
============== ============
0 OFF
1 FOOD1
2 FOOD2
3 FOOD3
============== ============
Keyword arguments:
<int> code - the code from the object or xml
Returns:
<String> Which food, if any, is being monitored for ramping
Example Usage:
cqi.rampLookup(cqi.getStatus().COOK_RAMP)
"""
return self._lookup("ramp", code)
if __name__ == "__main__": # pragma: no cover
import argparse
parser = argparse.ArgumentParser(
description="""CyberQ Interface class. No real executable code""",
epilog=
"""
Lets go grillin'
""")
parser.add_argument(
'-d', "--docs",
action='store_true',
help="""display the pydoc documentation""")
parser.add_argument(
'-v', "--version",
action='store_true',
help="""get version information for this file""")
parser.add_argument(
'-l', "--license",
action='store_true',
help="""display license information for this file""")
args = parser.parse_args()
if args.docs == True:
help(CyberQInterface)
if args.version == True:
print "Version: %s" % __version__
print "Last Modified: %s" % __date__
if args.license == True:
print "License: %s" % __license__
print __license_text__