Created
September 13, 2013 13:53
Revisions
-
ptman created this gist
Sep 13, 2013 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,315 @@ #!/usr/bin/env python # coding: utf-8 # vim: set ts=4 sts=4 sw=4 si ai et ft=python: # author: Paul Tötterman <[email protected]> # # Copyright (c) 2013, ZenRobotics Ltd. # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """Planet IP Power Manager Command Line Interface. Supports model IPM-1200x. Usage: ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] serial ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] status ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] on OUTLETS ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] off OUTLETS ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] cycle OUTLETS ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] name OUTLET NAME ipm [-dPqv] [-H HOST] [-u USER] [-p PASS] location OUTLET LOC ipm -h ipm -V Options: -h --help Show this usage message. -V --version Show version. -d --debug Debug output. -v --verbose Verbose output. -q --quiet Less output. -H --host HOST Specify hostname [default: ipm]. -u --user USER Specify username. -p --pass PASS Specify password. -P --prompt-password Prompt for password. Commands: serial Get serial number of IPM. status Get status of outlets from IPM. on OUTLETS Turn on outlets. off OUTLETS Turn off outlets. cycle OUTLETS Power cycle outlets. name OUTLET NAME Change the name of an outlet. location OUTLET LOC Change the location of an outlet. Note: Outlets are specific by lower-case letters. Several can be specified by separating with commas (without whitespace) or the keyword 'all'. """ import BeautifulSoup import collections import docopt import getpass import logging import pprint import re import requests import sys class IPM12(object): """Remotely manage a Planet IPM-1200x device.""" ACTION_NONE = '1' ACTION_ON = '2' ACTION_OFF = '3' ACTION_CYCLE = '4' ALLON = {'XAAAAAAABADMM': '0'} ALLOFF = {'XAAAAAAABADMM': '4095'} OUTLET_ACTIONS = {'a': 'XAAAAAAABOEBA', 'b': 'XAAAAAAABOEBB', 'c': 'XAAAAAAABOEBC', 'd': 'XAAAAAAABOEBD', 'e': 'XAAAAAAABOEBE', 'f': 'XAAAAAAABOEBF', 'g': 'XAAAAAAABOEBG', 'h': 'XAAAAAAABOEBH', 'i': 'XAAAAAAABOEBI', 'j': 'XAAAAAAABOEBJ', 'k': 'XAAAAAAABOEBK', 'l': 'XAAAAAAABOEBL'} OUTLET_NAMES = {'a': 'XAAAAAAABADJB', 'b': 'XAAAAAAABADJC', 'c': 'XAAAAAAABADJD', 'd': 'XAAAAAAABADJE', 'e': 'XAAAAAAABADJF', 'f': 'XAAAAAAABADJG', 'g': 'XAAAAAAABADJH', 'h': 'XAAAAAAABADJI', 'i': 'XAAAAAAABADJJ', 'j': 'XAAAAAAABADJK', 'k': 'XAAAAAAABADJL', 'l': 'XAAAAAAABADJM'} OUTLET_LOCS = {'a': 'XAAAAAAABADKB', 'b': 'XAAAAAAABADKC', 'c': 'XAAAAAAABADKD', 'd': 'XAAAAAAABADKE', 'e': 'XAAAAAAABADKF', 'f': 'XAAAAAAABADKG', 'g': 'XAAAAAAABADKH', 'h': 'XAAAAAAABADKI', 'i': 'XAAAAAAABADKJ', 'j': 'XAAAAAAABADKK', 'k': 'XAAAAAAABADKL', 'l': 'XAAAAAAABADKM'} STATUS_RE = re.compile(r'var\s+Power_status\s+= new Array\((.*?)\);') URL_CONTROL = 'PageControl.htm' URL_LOGO = 'logo.js' URL_OUTLET = 'Outlet.js' VERSION_RE = re.compile(r'IPM System v1.00 \(SN (\w+)\)') def __init__(self, hostname='ipm', username='zrripm', password='zrripm'): self.hostname = hostname self.username = username self.password = password self.baseurl = 'http://%s' % hostname self.auth = (username, password) self.supported = None self.serial = None def _get(self, url): """GET request with a few assumtions.""" # pylint: disable-msg=E1103 url = '%s/%s' % (self.baseurl, url) try: resp = requests.get(url, auth=self.auth) except requests.exceptions.ConnectionError: logging.error('Could not connect to IPM') return False if resp.status_code == 200: logging.debug('Successful GET') return resp.content elif resp.status_code == 401: logging.error('Invalid credentials') return False def _post(self, url, data=None): """POST request with a few assumtions.""" # pylint: disable-msg=E1103 url = '%s/%s' % (self.baseurl, url) try: resp = requests.post(url, auth=self.auth, data=data) except requests.exceptions.ConnectionError: logging.error('Could not connect to IPM') return False if resp.status_code == 200: return True elif resp.status_code == 401: logging.error('Invalid credentials') return False def check_supported(self): """Check if the IPM is of the expected version.""" content = self._get(IPM12.URL_LOGO) if content is False: self.supported = False return self.supported match = IPM12.VERSION_RE.search(content) if not match: self.supported = False return self.supported self.serial = match.group(1) self.supported = True return self.supported def power_on_all(self): """Power on all outlets.""" return self._post(IPM12.URL_CONTROL, data=IPM12.ALLON) def power_off_all(self): """Power off all outlets.""" return self._post(IPM12.URL_CONTROL, data=IPM12.ALLOFF) def power_on(self, outlets): """Power on a set of outlets or 'all'.""" if outlets == 'all': return self.power_on_all() data = {} for outlet in outlets: data[IPM12.OUTLET_ACTIONS[outlet]] = IPM12.ACTION_ON return self._post(IPM12.URL_CONTROL, data=data) def power_off(self, outlets): """Power off a set of outlets or 'all'.""" if outlets == 'all': return self.power_off_all() data = {} for outlet in outlets: data[IPM12.OUTLET_ACTIONS[outlet]] = IPM12.ACTION_OFF return self._post(IPM12.URL_CONTROL, data=data) def power_cycle(self, outlets): """Power cycle a set of outlets or 'all'.""" if outlets == 'all': outlets = normalize_outlets('a,b,c,d,e,f,g,h,i,j,k,l') data = {} for outlet in outlets: data[IPM12.OUTLET_ACTIONS[outlet]] = IPM12.ACTION_CYCLE return self._post(IPM12.URL_CONTROL, data=data) def set_name(self, outlet, name): """Set the name of an outlet.""" data = {IPM12.OUTLET_NAMES[outlet]: name} return self._post(IPM12.URL_CONTROL, data=data) def set_location(self, outlet, location): """Set the location -field of an outlet.""" data = {IPM12.OUTLET_LOCS[outlet]: location} return self._post(IPM12.URL_CONTROL, data=data) def status(self): """Return status of IPM as dict.""" soup = BeautifulSoup.BeautifulSoup(self._get(IPM12.URL_CONTROL)) outlets = collections.defaultdict(dict) for outlet, oid in IPM12.OUTLET_NAMES.iteritems(): tag = soup.find('input', attrs={'name': oid}) if tag is not None: outlets[outlet]['name'] = tag.get('value') for outlet, oid in IPM12.OUTLET_LOCS.iteritems(): tag = soup.find('input', attrs={'name': oid}) if tag is not None: outlets[outlet]['location'] = tag.get('value') power = IPM12.STATUS_RE.search(self._get(IPM12.URL_OUTLET)).group(1) power = [x.strip('"') for x in power.split(',')] for i, k in enumerate(sorted(IPM12.OUTLET_NAMES.keys())): outlets[k]['status'] = power[i] return dict(outlets) def normalize_outlets(outlets): """Return list of outlets, lower-cased, or 'all'.""" if outlets.lower() == 'all': return 'all' if ',' in outlets: return [x.lower() for x in outlets.split(',')] return [outlets.lower()] def main(): """Main function.""" opts = docopt.docopt(__doc__, version='0.1.0') if opts['--debug']: logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) elif opts['--verbose']: logging.basicConfig(stream=sys.stderr, level=logging.INFO) elif opts['--quiet']: logging.basicConfig(stream=sys.stderr, level=logging.ERROR) else: logging.basicConfig(stream=sys.stderr, level=logging.WARNING) if opts['--prompt-password']: opts['--pass'] = getpass.getpass() ipm = IPM12(hostname=opts['--host'], username=opts['--user'], password=opts['--pass']) if not ipm.check_supported(): logging.error('Unsupported device') sys.exit(2) if opts['serial']: print ipm.serial elif opts['status']: pprint.pprint(ipm.status()) elif opts['on']: if not ipm.power_on(normalize_outlets(opts['OUTLETS'])): logging.error('Problem powering on') sys.exit(4) elif opts['off']: if not ipm.power_off(normalize_outlets(opts['OUTLETS'])): logging.error('Problem powering off') sys.exit(4) elif opts['cycle']: if not ipm.power_cycle(normalize_outlets(opts['OUTLETS'])): logging.error('Problem cycling power') sys.exit(4) elif opts['name']: if not ipm.set_name(opts['OUTLET'].lower(), opts['NAME']): logging.error('Problem naming outlet') sys.exit(4) elif opts['location']: if not ipm.set_location(opts['OUTLET'].lower(), opts['LOC']): logging.error('Problem changing location for outlet') sys.exit(4) else: logging.critical('Error, should never be reachable') sys.exit(5) if __name__ == '__main__': main()