########################################################################### # Copyright (c) 2018- Franco (nextime) Lanza # # Penguidom System client Daemon "penguidomd" [https://git.nexlab.net/domotika/Penguidom] # # This file is part of penguidom. # # penguidom is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # ############################################################################## from penguidom import imodules from zope.interface import implements from twisted.plugin import IPlugin from twisted.internet.serialport import SerialPort from twisted.internet.protocol import BaseProtocol, Protocol, Factory from twisted.internet import reactor, tcp from nexlibs.utils import genutils from importlib import import_module import serial import time import paradox37b as p37b from mapping import EVENTMAP, REGISTERS SERIAL_PORT="/dev/ttyS0" BAUDRATE=9600 BOARDTYPE="MG5050" # Just a default PKTTIMEOUT=2 # Seconds before discard incorrect lenght packets REPLYTIMEOUT=1 # Seconds before to start reply checks SEND_INTERVAL=.5 # Seconds between messages sent REPLY_INTERVAL=.3 # Seconds between reply checks class ParadoxProtocol(BaseProtocol): packet=[] queue=[] sendqueue=[] replyqueue=[] packettimeout = 0 proxy = False zonemap = False boardname = '' def __init__(self, logger, core, cfg): self.log = logger self.core = core self.cfg = cfg self.log.debug(str(self.cfg)) self.flags = p37b.Flags() try: self.proxyonly = genutils.isTrue(self.cfg.get('connection', 'proxyonly')) self.mapnames = genutils.isTrue(self.cfg.get('panel', 'mapNames')) self.autodetect = genutils.isTrue(self.cfg.get('panel', 'autodetect')) except: self.proxyonly = False self.mapnames = False self.autodetect = False reactor.addSystemEventTrigger('before', 'shutdown', self._terminating) def _queueData(self): if len(self.packet) >= 37: packet="".join(self.packet[:37]) if p37b.checkSumTest(packet): self.log.debug("<< MESSAGE QUEUED: "+''.join( [ "\\x%02X" % ord( x ) for x in packet ] ).strip()) #print packet self.queue+=[packet] self.packet=self.packet[37:] else: self.log.debug("Serial message incorrect: shift queue and discard first byte ("+"\\x%02X" % ord( self.packet[0] )+")") self.packet=self.packet[1:] reactor.callLater(0, self._queueData) reactor.callLater(0, self._processQueue) elif len(self.packet) > 0: self.packettimeout = time.time() def _queueSendData(self, data, callback=False, expected_reply=False, callback_args=False): self.sendqueue+=[{'msg': data, 'cb': callback, 'try': 3, 'expected_reply': expected_reply, 'callback_args': callback_args}] reactor.callLater(0, self._processSendQueue) def _processQueue(self): def _expected_reply(replystart, expected): if expected: if isinstance(expected, (list, tuple)): for i in expected: if p37b.checkCmd(replystart, i, nibble=True): return True else: if p37b.checkCmd(replystart, expected, nibble=True): return True else: return True return False if len(self.queue) > 0: if p37b.checkCmd(ord(self.queue[0][0]), p37b.CMD_EVENT, nibble=True): reactor.callLater(0, self._processEvent, self.queue[0]) else: if len(self.replyqueue) > 0: if _expected_reply(ord(self.queue[0][0]), self.replyqueue[0]['expected_reply']): # XXX Can we check in some way if is the *RIGHT* reply? # in theory we don't need it as we will not send anything # until the reply is received, but it would be more error # prone that way... if self.replyqueue[0]['cb'] and callable(self.replyqueue[0]['cb']): reactor.callLater(0, self.replyqueue[0]['cb'], self.queue[0], self.replyqueue[0]['callback_args']) del self.replyqueue[0] else: if isinstance(self.replyqueue[0]['expected_reply'], (list, tuple)): expected = "["+",".join(["\\x%02X" % (x) for x in self.replyqueue[0]['expected_reply']])+"]" else: print self.replyqueue[0]['expected_reply'] << 4 expected = "\\x%02X" % (self.replyqueue[0]['expected_reply']) self.log.error("Wrong Expected Reply! (have "+"\\x%02X" % (ord(self.queue[0][0]) >> 4)+" expect "+expected+")") del self.queue[0] def _processSendQueue(self, addToReplyQueue=True): if len(self.sendqueue) > 0: if not len(self.replyqueue) or (not addToReplyQueue and len(self.replyqueue) > 0): packet = self.sendqueue[0]['msg'] if addToReplyQueue: self.replyqueue+=[self.sendqueue[0]] del self.sendqueue[0] self.log.debug(">> SEND MESSAGE: "+''.join( [ "\\x%02X" % ord( x ) for x in packet ] ).strip()) self.transport.write(packet) reactor.callLater(REPLYTIMEOUT, self._checkReplies) else: reactor.callLater(SEND_INTERVAL, self._processSendQueue) def _checkReplies(self): if len(self.replyqueue) > 0: # Is there a reply waiting? why? if self.replyqueue[0]['try'] > 0: self.replyqueue[0]['try']-=1 self.sendqueue+=[self.replyqueue[0]] reactor.callLater(.3, self._processSendQueue, False) else: self.log.error("FAILED TO SEND MESSAGE: "+''.join( [ "\\x%02X" % ord( x ) for x in self.replyqueue[0]['msg'] ] ).strip()) del self.replyqueue[0] def dataReceived(self, data): if self.proxy: self.proxy.serialInData(data) if len(self.packet) > 0 and (time.time()-self.packettimeout) > PKTTIMEOUT: self.log.debug("Serial Timeout: discard packet "+''.join( [ "\\x%02X" % ord( x ) for x in self.packet ] ).strip()) self.packet = [] self.packet+=list(data) self._queueData() def connectionMade(self): self.log.info("Serial port correctly connected") self.connect() def _terminating(self, *a, **kw): self.log.debug("Send Disconnect message") self.write(p37b.MSG_DISCONNECT) self.transport.loseConnection() def connectionLost(self, reason): self.log.debug("Connection lost for "+str(reason)) def connect(self): if not self.proxyonly: self.write(p37b.MSG_CONNECT, self._detectBoard, expected_reply=p37b.REPLY_CONNECT) self.write(p37b.MSG_GETSTATUS, expected_reply=p37b.REPLY_GETSTATUS) self.write(p37b.MSG_SYNC, self._replySync, expected_reply=p37b.REPLY_SYNC) def write(self, message, reply_callback=False, expected_reply=False, callback_args=False): self._queueSendData(p37b.format37ByteMessage(message), reply_callback, expected_reply, callback_args) def _replySync(self, reply, cbargs): self.log.debug("REPLY SYNC RECEIVED") self.write(reply, self._endHandshacke) def _endHandshacke(self, reply=None, cbargs=None): # XXX Here things starts to get weird. # I have no idea of what exactly those two messages do, # but they seems to be needed for the initial handshake # when connecting. So, we just send both, hope # sometime i will fully understand what they do exactly. # # After the end of the handshake we proceed to map zone names, # so, callback for last message drive the runflow in that direction. self.write(p37b.MSG_UNKWNOWN_HS1, expected_reply=p37b.REPLY_QUERY) self.write(p37b.MSG_UNKWNOWN_HS2, self.mapNames, expected_reply=p37b.REPLY_QUERY) def _detectBoard(self, reply, cbargs=None): board = p37b.getPanelName(reply) self.boardname = board self.log.info('Detected Panel Board as '+board) if self.autodetect: try: self.log.info("Autoload panel board mapping for "+str(board)) maps = import_module("penguidom.plugins.paradox.panels."+str(board)) EVENTMAP.loadEvents(maps.EventMap()) REGISTERS.loadRegisters(maps.Registers()) self.log.info("Panel board mapping for "+str(board)+" loaded successfully") except: self.log.error("Cannot autoload board mapping for "+str(board)) def _processEvent(self, event): self.flags.asByte = ord(event[0]) alarm = self.flags.alarm year = (ord(event[1])*100)+ord(event[2]) month = ord(event[3]) day = ord(event[4]) hour = ord(event[5]) minute = ord(event[6]) date = "-".join([str(year), str(month), str(day)])+" "+":".join([str(hour), str(minute)]) event, subevent = EVENTMAP.getEventDescription(ord(event[7]), ord(event[8])) event = event.strip() subevent = subevent.strip() self.log.debug("Event: "+date+" - "+event+": "+subevent+" - In alarm:"+str(alarm)) def _setNames(self, reply, item): if item in REGISTERS.getsupportedItems(): reglist = getattr(REGISTERS, 'get'+item+'Register')() setfunc = getattr(EVENTMAP, 'set'+item) reg = reply[1:4] if reg in reglist.keys(): nums = reglist[reg] for num in range(0,len(nums)): if nums[num] is not None: label = reply[4+(16*num):20+(16*num)].strip() self.log.info('Set label for '+item+' '+str(nums[num])+' as \"'+label+'\"') setfunc(nums[num], label) def _mapItemNames(self, item): try: items = getattr(REGISTERS, 'get'+item+'Register')() for i in items.keys(): self.write(p37b.SEND_QUERY+i, self._setNames, expected_reply=p37b.REPLY_QUERY, callback_args=item ) except: self.log.error("Cannot find item named "+item+" in registers") def mapNames(self, reply=None, cbargs=None): if self.mapnames: self.log.info("Start Mapping names...") self.log.info("Supported Items:") for i in REGISTERS.getsupportedItems(): self.log.info(" "+i) reactor.callLater(0, self._mapItemNames, i) class ParadoxTCPProxy(Protocol): cid = 0 def __init__(self, clients, factory): self.clients = clients self.factory = factory def connectionMade(self): cid = len(self.clients) self.clients[cid] = self self.cid = cid def connectionLost(self, reason): if self.cid in self.clients.keys(): del self.clients[self.cid] def dataReceived(self, data): self.factory.log.debug("Data FROM TCP Client "+str(self.cid)+": "+''.join( [ "\\x%02X" % ord( x ) for x in data ] ).strip()) self.factory.serproto.transport.write(data) class ParadoxTCPFactory(Factory): def __init__(self, core, serproto, logger): self.clients = {} self.serproto = serproto self.serproto.proxy = self self.log = logger def serialInData(self, data): for cid in self.clients.keys(): self.clients[cid].transport.write(data) def buildProtocol(self, addr): return ParadoxTCPProxy(self.clients, self) class Paradox(object): implements(IPlugin, imodules.IModules) serproto = False def _openSerial(self, port=SERIAL_PORT, retry=3): try: dev = self.cfg.get('connection', 'port') except: dev = port try: brate = int(self.cfg.get('connection', 'baudrate')) except: brate = BAUDRATE if retry > 0: try: self.serproto = ParadoxProtocol(self.log, self.core, self.cfg) self.port = SerialPort(self.serproto, dev, reactor, baudrate=brate) except serial.SerialException as err: self.log.info("Serial Port ERROR: "+str(err)) reactor.callLater(1, self._openSerial, port, retry-1) else: self.log.info("Unable to open Serial Port: retry in 1 minute") reactor.callLater(60, self._openSerial, port) def initialize(self, callback, logger): self.log = logger self.core = callback self.cfg = self.core.readPluginConfig('paradox') logger.info("Initialize Serial Connection...") self._openSerial() if genutils.isTrue(self.cfg.get("connection", "enableTCPProxy")) and self.serproto: self.proxy = ParadoxTCPFactory(self.core, self.serproto, self.log) tcpport = self.cfg.get("connection", "tcpport") self.log.debug("TCP Port "+str(tcpport)+" OPEN") port = tcp.Port(int(tcpport), self.proxy, 50, self.cfg.get("connection", "tcpaddr"), reactor) port.startListening() try: defboard = self.cfg.get("panel", "paneltype") except: defboard = BOARDTYPE try: maps = import_module("penguidom.plugins.paradox.panels."+str(defboard)) EVENTMAP.loadEvents(maps.EventMap()) REGISTERS.loadRegisters(maps.Registers()) self.log.info("Loaded registers and event mapping for default panel type "+str(defboard)) except: self.log.error("Cannot import panel mapping for panel type "+str(defboard)) logger.info("Plugin initialized") def getWebHome(self): webparts = self.core.getWebParts() labelreg = REGISTERS.getsiteNameLabelRegister() try: if labelreg.items()[0][1][0] is not None: colname = str(labelreg.items()[0][1][0]) else: colname = " ".join(['Paradox', self.serproto.boardname]) except: colname = 'paradox' partitions = EVENTMAP.getAllpartitionLabel() #self.log.info(EVENTMAP.getsiteNameLabel()) colcont = '' for partition in partitions.items(): partname = partition[1] partidx = partition[0] colcont+=webparts.getButton(title=str(partname), act=webparts.getOnOffSwitch("".join(['paradox_partition_', str(partidx)]), on='ARMED', off='DISARMED')) return webparts.getColumn(title=colname, content=colcont) def getWebPage(self): return REGISTERS.getsiteNameLabelRegister()