diff --git a/houseagent/plugins/jeelabs.py b/houseagent/plugins/jeelabs.py new file mode 100644 index 0000000..e855f1b --- /dev/null +++ b/houseagent/plugins/jeelabs.py @@ -0,0 +1,312 @@ +import os, sys +import time +from twisted.internet.serialport import SerialPort +from twisted.protocols import basic +import ConfigParser +from houseagent.plugins import pluginapi +from twisted.internet import reactor, defer +from twisted.python import log + +# Platform specific imports +if os.name == "nt": + import win32service + import win32serviceutil + import win32event + import win32evtlogutil + +class JeelabsDevice(object): + ''' + Abstract class to represent a JeelabsDevice + ''' + def __init__(self, id, type, subtype, rssi): + self.id = id + self.type = type + self.subtype = subtype + self.rssi = rssi + + +class RoomNode(JeelabsDevice): + ''' + Anstract class to represent a RoomNode device. + ''' + def __init__(self, id, type, subtype, rssi): + JeelabsDevice.__init__(self, id, type, subtype, rssi) + self.counter = None + + def __repr__(self): + return '[RoomNode] id: %r, type: %r, subtype: %r, counter: %r, rssi: %r' % (self.id, self.type, self.subtype, self.counter, self.rssi) + +class MeterNode(JeelabsDevice): + ''' + Anstract class to represent a RoomNode device. + ''' + def __init__(self, id, type, subtype, rssi): + JeelabsDevice.__init__(self, id, type, subtype, rssi) + self.counter = None + + def __repr__(self): + return '[RoomNode] id: %r, type: %r, subtype: %r, counter: %r, rssi: %r' % (self.id, self.type, self.subtype, self.counter, self.rssi) + + +class JeelabsProtocol(basic.LineReceiver): + ''' + This class handles the JeeLabs protocol, i.e. the wire level stuff. + ''' + def __init__(self, wrapper): + self.wrapper = wrapper + self._devices = [] + + def lineReceived(self, line): + if line.startswith("OK"): + self._handle_data(line) + + def _handle_data(self, line): + ''' + This function handles incoming node data, current the following sketches/node types are supported: + - Roomnode sketch + - Outside node sketch + @param line: the raw line of data received. + ''' + data = line.split(" ") + +# log.msg (line) +# log.msg (data[2]) +# log.msg (len(data)) + + if len(data) > 6: + if int(data[2]) == 1: + + # Raw data packets (information from host.tcl (JeeLabs)) + a = int(data[4]) + b = int(data[5]) + c = int(data[6]) + d = int(data[7]) + node_id = str(int(data[1]) & 0x1f) + msg_seq = str(int(data[3])) + + type = 'JeeLabs' + subtype = 'RoomNode' + rssi = 88 + + device = self._device_exists(id, type) + + if not device: + device = RoomNode(node_id, type, subtype, rssi) + self._devices.append(device) + + light = a + motion = b & 1 + humidity = b >> 1 + temperature = str(((256 * (d&3) + c) ^ 512) - 512) + battery = (d >> 2) & 1 + temperature = temperature[0:2] + '.' + temperature[-1] + +# print(subtype) + + log.msg("Received data from rooms jeenode; channel: %s, sequence: %s LDR: %s, " \ + "humidity: %s, temperature: %s, motionsensor: %s, battery: %s" % (node_id, msg_seq, light, humidity, temperature, motion, battery)) + + values = {'Light': str(light), 'Humidity': str(humidity), + 'Temperature': str(temperature), 'Motion': str(motion), 'Battery': str(battery)} + + self.wrapper.pluginapi.value_update(node_id, values) + + # Handle outside node sketch + elif int(data[2]) == 2: + + node_id = str(int(data[1]) & 0x1f) + + type = 'JeeLabs' + subtype = 'OutsideNode' + + # temperature from pressure chip (16bit) + temp = str((int(data[4]) << 8) + int(data[3])) + temp = temp[0:2] + '.' + temp[-1] + + # Lux level (32bit) + lux = str((int(data[8]) << 24) + (int(data[7]) << 16) + (int(data[6]) << 8) + int(data[5])) + + # barometric pressure (32bit) + pressure = str((int(data[-1]) << 24) + (int(data[-2]) << 16) + (int(data[-3]) << 8) + int(data[-4])) + pressure = pressure[0:4] + "." + pressure[-2:] + + log.msg("Received data from outside sketch jeenode; channel: %s, lux: %s, " \ + "pressure: %s, temperature: %s" % (node_id, lux, pressure, temp)) + + values = {'Lux': str(lux), 'Pressure': str(pressure), 'Temperature': str(temp)} +# print('Received data') + self.wrapper.pluginapi.value_update(node_id, values) + + # handle moternode sketch + elif int(data[2]) == 3: + + # Raw data packets (information from host.tcl (JeeLabs)) + a = int(data[4]) + b = int(data[5]) + c = int(data[6]) + d = int(data[7]) + node_id = str(int(data[1]) & 0x1f) + msg_seq = str(int(data[3])) + + type = 'JeeLabs' + subtype = 'MeterNode' + rssi = 88 + + device = self._device_exists(id, type) + + if not device: + device = MeterNode(node_id, type, subtype, rssi) + self._devices.append(device) + + counter = d << 24 | c << 16 | b << 8 | a + cnt = str(int(counter / 100)) + '.' + str(counter % 100) +# print(subtype) + + log.msg("Received data from MeterNode; channel: %s, sequence: %s Counter: %s, " % (node_id, msg_seq, cnt)) + + values = {'Counter': cnt} + + self.wrapper.pluginapi.value_update(node_id, values) + + + + + def _device_exists(self, id, type): + ''' + Helper function to check whether a device exists in the device list. + @param id: the id of the device + @param type: the type of the device + ''' + for device in self._devices: + if device.id == id and device.type == type: + return device + + return False + + + +class JeelabsWrapper(): + + def __init__(self): + ''' + Load initial JeeLabs configuration from jeelabs.conf + ''' + from houseagent.utils.generic import get_configurationpath + config_path = "/etc" + + config = ConfigParser.RawConfigParser() + config.read(os.path.join(config_path, 'jeelabs.conf')) + self.port = config.get("serial", "port") + + # Get broker information (RabbitMQ) + self.broker_host = config.get("broker", "host") + self.broker_port = config.getint("broker", "port") + self.broker_user = config.get("broker", "username") + self.broker_pass = config.get("broker", "password") + self.broker_vhost = config.get("broker", "vhost") + + self.logging = config.getboolean('general', 'logging') + + self.log = pluginapi.Logging("Jeenode plugin") + + self.id = config.get('general', 'id') + + def start(self): + ''' + Function that starts the JeeLabs plug-in. It handles the creation + of the plugin connection and connects to the specified serial port. + ''' + callbacks = {'custom': self.cb_custom} + + self.pluginapi = pluginapi.PluginAPI(self.id, 'Jeelabs', broker_host=self.broker_host, broker_port=self.broker_port, **callbacks) + + self.protocol = JeelabsProtocol(self) + myserial = SerialPort (self.protocol, self.port, reactor, rtscts=0, xonxoff=1, baudrate=57600) + + log.startLogging(open('/var/log/houseagent/jeelabs.log','w')) + + self.log.debug("Started plugin") + + self.pluginapi.ready() + + reactor.run(installSignalHandlers=0) + return True + + def cb_custom(self, action, parameters): + ''' + This function is a callback handler for custom commands + received from the coordinator. + @param action: the custom action to handle + @param parameters: the parameters passed with the custom action + ''' + if action == 'get_devices': + devices = {} + for dev in self.protocol._devices: + devices[dev.id] = [dev.type, dev.subtype, dev.rssi] + d = defer.Deferred() + d.callback(devices) + return d + + +if os.name == "nt": + + class JeelabsService(win32serviceutil.ServiceFramework): + ''' + This class is a Windows Service handler, it's common to run + long running tasks in the background on a Windows system, as such we + use Windows services for HouseAgent. + ''' + _svc_name_ = "hajeelabs" + _svc_display_name_ = "HouseAgent - Jeelabs Service" + + def __init__(self,args): + win32serviceutil.ServiceFramework.__init__(self,args) + self.hWaitStop=win32event.CreateEvent(None, 0, 0, None) + self.isAlive=True + + def SvcStop(self): + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + reactor.stop() + win32event.SetEvent(self.hWaitStop) + self.isAlive=False + + def SvcDoRun(self): + import servicemanager + + win32evtlogutil.ReportEvent(self._svc_name_,servicemanager.PYS_SERVICE_STARTED,0, + servicemanager.EVENTLOG_INFORMATION_TYPE,(self._svc_name_, '')) + + self.timeout=1000 # In milliseconds (update every second) + jeelabs = JeelabsWrapper() + + if jeelabs.start(): + win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE) + + win32evtlogutil.ReportEvent(self._svc_name_,servicemanager.PYS_SERVICE_STOPPED,0, + servicemanager.EVENTLOG_INFORMATION_TYPE,(self._svc_name_, '')) + + self.ReportServiceStatus(win32service.SERVICE_STOPPED) + + return + +if __name__ == '__main__': + + if os.name == "nt": + + if len(sys.argv) == 1: + try: + + import servicemanager, winerror + evtsrc_dll = os.path.abspath(servicemanager.__file__) + servicemanager.PrepareToHostSingle(JeelabsService) + servicemanager.Initialize('JeelabsService', evtsrc_dll) + servicemanager.StartServiceCtrlDispatcher() + + except win32service.error, details: + if details[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT: + win32serviceutil.usage() + else: + win32serviceutil.HandleCommandLine(JeelabsService) + else: + jeelabs = JeelabsWrapper() + jeelabs.start() \ No newline at end of file diff --git a/houseagent/plugins/jeelabs/README.md b/houseagent/plugins/jeelabs/README.md new file mode 100644 index 0000000..c30e1a6 --- /dev/null +++ b/houseagent/plugins/jeelabs/README.md @@ -0,0 +1,4 @@ +HouseAgent JeeLabs plug-in +========================== +This is the JeeLabs plug-in for HouseAgent. For more information about JeeLabs visit: http://www.jeelabs.org +For more information about HouseAgent visit: http://www.houseagent.nl \ No newline at end of file diff --git a/houseagent/plugins/jeelabs/menu.xml b/houseagent/plugins/jeelabs/menu.xml new file mode 100644 index 0000000..5746a01 --- /dev/null +++ b/houseagent/plugins/jeelabs/menu.xml @@ -0,0 +1,9 @@ + + + + Discovered devices + /jeelabs_devices_view + Use this page to view discovered devices. This page can also be used to add the device to the HouseAgent database. + + + \ No newline at end of file diff --git a/houseagent/plugins/jeelabs/pages.py b/houseagent/plugins/jeelabs/pages.py new file mode 100644 index 0000000..226c43a --- /dev/null +++ b/houseagent/plugins/jeelabs/pages.py @@ -0,0 +1,110 @@ +from twisted.web.static import File +import os +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET +import json +from mako.lookup import TemplateLookup +from mako.template import Template +from twisted.web import http +from twisted.internet.defer import inlineCallbacks +import sys + +def init_pages(web, coordinator, db): + + if hasattr(sys, 'frozen'): + web.putChild("jeelabs_images", File(os.path.join(os.path.dirname(sys.executable), 'plugins/jeelabs/templates/images'))) + else: + web.putChild("jeelabs_images", File(os.path.join('houseagent/plugins/jeelabs/templates/images'))) + web.putChild('jeelabs_devices', JeelabsDevices(coordinator, db)) + web.putChild('jeelabs_devices_view', JeelabsDevicesView(db, coordinator)) + + +class JeelabsDevicesView(Resource): + def __init__(self, db, coordinator): + self.db = db + self.coordinator = coordinator + Resource.__init__(self) + + def finished(self, locations): + + if hasattr(sys, 'frozen'): + # Special case when we are frozen. + lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(sys.executable), 'templates/')]) + template = Template(filename=os.path.join(os.path.dirname(sys.executable), 'plugins/jeelabs/templates/devices.html'), lookup=lookup) + else: + lookup = TemplateLookup(directories=['houseagent/templates/']) + template = Template(filename='houseagent/plugins/jeelabs/templates/devices.html', lookup=lookup) + + self.request.write(str(template.render(locations=locations, pluginid=self.pluginid))) + self.request.finish() + + def render_GET(self, request): + self.request = request + self.db.query_locations().addCallback(self.finished) + + # Uglyness alert! + plugins = self.coordinator.get_plugins_by_type("Jeelabs") + if len(plugins) == 0: + self.request.write(str("No online Jeelabs plugins found...")) + self.request.finish() + elif len(plugins) == 1: + self.pluginguid = plugins[0].guid + self.pluginid = plugins[0].id + + return NOT_DONE_YET + +class JeelabsDevices(Resource): + def __init__(self, coordinator, db): + self.coordinator = coordinator + self.db = db + + @inlineCallbacks + def result(self, result): + db_devices = yield self.db.query_devices() + + output = [] + for id, data in result.iteritems(): + in_db = False + + for db_device in db_devices: + if db_device[2] == id: + in_db = True + + dev = {'id': id, + 'type': data[0], + 'subtype': data[1], + 'rssi': data[2], + 'database': in_db} + output.append(dev) + + self.request.write(json.dumps(output)) + self.request.finish() + + @inlineCallbacks + def _add(self, parameters): + address = parameters['address'][0] + name = parameters['name'][0] + plugin_id = parameters['plugin_id'][0] + location_id = parameters['location_id'][0] + + done = yield self.db.save_device(name, address, plugin_id, location_id) + self.request.finish(done) + + def render_PUT(self, request): + self.request = request + self._add(http.parse_qs(request.content.read(), 1)) # http://twistedmatrix.com/pipermail/twisted-web/2007-March/003338.html + return NOT_DONE_YET + + def render_GET(self, request): + self.request = request + plugins = self.coordinator.get_plugins_by_type("Jeelabs") + + if len(plugins) == 0: + self.request.write(str("No online Jeelabs plugins found...")) + self.request.finish() + elif len(plugins) == 1: + self.pluginguid = plugins[0].guid + self.pluginid = plugins[0].id + self.coordinator.send_custom(plugins[0].guid, "get_devices", '').addCallback(self.result) + + return NOT_DONE_YET \ No newline at end of file diff --git a/houseagent/plugins/jeelabs/pages.pyc b/houseagent/plugins/jeelabs/pages.pyc new file mode 100644 index 0000000..d1beeeb Binary files /dev/null and b/houseagent/plugins/jeelabs/pages.pyc differ diff --git a/houseagent/plugins/jeelabs/templates/devices.html b/houseagent/plugins/jeelabs/templates/devices.html new file mode 100644 index 0000000..909591a --- /dev/null +++ b/houseagent/plugins/jeelabs/templates/devices.html @@ -0,0 +1,112 @@ +<%inherit file="/master.html"/> + +<%def name="head()"> + + + + + + + +<%def name="content()"> +
Jeelabs discovered devices
+

This page allows you to view devices that were discovered/received by the Jeelabs interface. This page also allows you to add the device to the HouseAgent database in order to track the values of the device.

+
+
+ \ No newline at end of file diff --git a/houseagent/plugins/jeelabs/templates/images/icon.png b/houseagent/plugins/jeelabs/templates/images/icon.png new file mode 100644 index 0000000..07777f1 Binary files /dev/null and b/houseagent/plugins/jeelabs/templates/images/icon.png differ