diff --git a/bin/smarthome.py b/bin/smarthome.py index 7e02a530..4a115293 100755 --- a/bin/smarthome.py +++ b/bin/smarthome.py @@ -573,6 +573,7 @@ def reload_logics(): arggroup.add_argument('-q', '--quiet', help='reduce logging to the logfile', action='store_true') arggroup.add_argument('-V', '--version', help='show SmartHome.py version', action='store_true') arggroup.add_argument('--start', help='start SmartHome.py and detach from console (default)', default=True, action='store_true') + arggroup.add_argument('-f', '--foreground', help='start SmartHome.py and stay in foreground', action='store_true') args = argparser.parse_args() if args.interactive: @@ -612,6 +613,8 @@ def reload_logics(): LOGLEVEL = logging.WARNING elif args.verbose: LOGLEVEL = logging.DEBUG + elif args.foreground: + MODE = 'foreground' # check for pid file pid = lib.daemon.get_pid(__file__) diff --git a/plugins/enocean/__init__.py b/plugins/enocean/__init__.py index 79943a9e..a8f54168 100644 --- a/plugins/enocean/__init__.py +++ b/plugins/enocean/__init__.py @@ -156,6 +156,15 @@ def _rocker_sequence(self, item, sender_id, sequence): except Exception as e: logger.error("enocean: error handling enocean_rocker_sequence \"{}\" - {}".format(sequence, e)) + def _multivalue_txdelay(self, item, id_offset, delay): + try: + time.sleep(0.3) + if ('RED' in item._enocean_txdl_data) and ('GREEN' in item._enocean_txdl_data) and ('BLUE' in item._enocean_txdl_data): + self.send_4bs([7 << 4, item._enocean_txdl_data['BLUE'], item._enocean_txdl_data['GREEN'], item._enocean_txdl_data['RED']], id_offset) + logger.debug("enocean: tx delay for item {} with {}".format(item, item._enocean_txdl_data)) + except Exception as e: + logger.error("enocean: error handling tx delay for item {} - {}".format(item, e)) + def _process_packet_type_radio(self, data, optional): #logger.warning("enocean: processing radio message with data = [{}] / optional = [{}]".format(', '.join(['0x%02x' % b for b in data]), ', '.join(['0x%02x' % b for b in optional]))) @@ -367,57 +376,108 @@ def parse_item(self, item): self._rx_items[rx_id][rx_eep].append(item) logger.info("enocean: item {} listens to id {:08X} with eep {} key {}".format(item, rx_id, rx_eep, rx_key)) - #logger.info("enocean: self._rx_items = {}".format(self._rx_items)) + + if 'enocean_tx_key' in item.conf: + # look for info from the most specific info to the broadest (key->eep->id) - one id might use multiple eep might define multiple keys + eep_item = item + while (not 'enocean_tx_eep' in eep_item.conf): + eep_item = eep_item.return_parent() + if (eep_item is self._sh): + logger.error("enocean: could not find enocean_tx_eep for item {}".format(item)) + return None + id_item = eep_item + while (not 'enocean_tx_id' in id_item.conf): + id_item = id_item.return_parent() + if (id_item is self._sh): + logger.error("enocean: could not find enocean_tx_id for item {}".format(item)) + return None return self.update_item + return None def update_item(self, item, caller=None, source=None, dest=None): - if caller != 'EnOcean': - logger.debug('enocean: item updated externally') - if self._block_ext_out_msg: - logger.debug('enocean: sending manually blocked by user. Aborting') + if caller == 'EnOcean': + return + logger.debug('enocean: item updated externally') + if self._block_ext_out_msg: + logger.debug('enocean: sending manually blocked by user. Aborting') + return + tx_key = item.conf['enocean_tx_key'] + eep_item = item + while (not 'enocean_tx_eep' in eep_item.conf): + eep_item = eep_item.return_parent() + if (eep_item is self._sh): + logger.error("enocean: could not find enocean_tx_eep for item {}".format(item)) return - if 'enocean_tx_eep' in item.conf: - if isinstance(item.conf['enocean_tx_eep'], str): - tx_eep = item.conf['enocean_tx_eep'] - logger.debug('enocean: item has tx_eep') - id_offset = 0 - if 'enocean_tx_id_offset' in item.conf and (isinstance(item.conf['enocean_tx_id_offset'], str)): - logger.debug('enocean: item has valid enocean_tx_id_offset') - id_offset = int(item.conf['enocean_tx_id_offset']) - #if (isinstance(item(), bool)): - #if item.conf['type'] == bool: - #Identify send command based on tx_eep coding: - if(tx_eep == 'A5_38_08_02'): - #if isinstance(item, bool): - logger.debug('enocean: item is A5_38_08_02 type') - if not item(): - self.send_dim(id_offset, 0, 0) - logger.debug('enocean: sent off command') - else: - if 'ref_level' in item.level.conf: - dim_value = int(item.level.conf['ref_level']) - logger.debug('enocean: ref_level found') - else: - dim_value = 100 - logger.debug('enocean: no ref_level found. Setting to default 100') - self.send_dim(id_offset, dim_value, 0) - logger.debug('enocean: sent dim on command') - elif(tx_eep == 'A5_38_08_03'): - logger.debug('enocean: item is A5_38_08_03 type') - self.send_dim(id_offset, item(), 0) - logger.debug('enocean: sent dim command') - elif(tx_eep == 'A5_38_08_01'): - logger.debug('enocean: item is A5_38_08_01 type') - self.send_switch(id_offset, item(), 0) - logger.debug('enocean: sent switch command') - else: - logger.error('enocean: error: Unknown tx eep command') + tx_eep = eep_item.conf['enocean_tx_eep'] + id_item = eep_item + while (not 'enocean_tx_id' in id_item.conf): + id_item = id_item.return_parent() + if (id_item is self._sh): + logger.error("enocean: could not find enocean_tx_id for item {}".format(item)) + return + tx_id = id_item.conf['enocean_tx_id'] + id_offset = 0 + if 'enocean_tx_id_offset' in item.conf and (isinstance(item.conf['enocean_tx_id_offset'], str)): + logger.debug('enocean: item has valid enocean_tx_id_offset') + id_offset = int(item.conf['enocean_tx_id_offset']) + #if (isinstance(item(), bool)): + #if item.conf['type'] == bool: + #Identify send command based on tx_eep coding: + if (tx_eep == 'A5_38_09'): + #logger.debug('enocean: item is A5_38_09 type') + if (tx_key.upper()) in ['RED', 'GREEN', 'BLUE']: + #logger.debug('enocean: rgb-type') + if ('enocean_tx_delay' in item.conf) and (float(item.conf['enocean_tx_delay']) > 0.0): + if not hasattr(eep_item, '_enocean_txdl_thread') or not eep_item._enocean_txdl_thread.isAlive(): + eep_item._enocean_txdl_data = {'RED': 0, 'GREEN': 0, 'BLUE': 0} + eep_item._enocean_txdl_thread = threading.Thread(target=self._multivalue_txdelay, name="enocean-txdl", args=(eep_item, id_offset, float(item.conf['enocean_tx_delay']), )) + logger.info("starting enocean tx delay thread for eep_item {}".format(eep_item)) + eep_item._enocean_txdl_thread.daemon = True + eep_item._enocean_txdl_thread.start() + eep_item._enocean_txdl_data[tx_key.upper()] = item() else: - logger.error('enocean: tx_eep is not a string value') + data = [0, 0, 0, 0] + data[0] = 7 << 4 + for rgb_item in item.return_parent().return_children(): + if (not 'enocean_tx_key' in rgb_item.conf): + continue + if (rgb_item.conf['enocean_tx_key'].upper() == 'RED'): + data[3] = rgb_item() + logger.debug('enocean: found red-value') + elif (rgb_item.conf['enocean_tx_key'].upper() == 'GREEN'): + data[2] = rgb_item() + logger.debug('enocean: found green-value') + elif (rgb_item.conf['enocean_tx_key'].upper() == 'BLUE'): + data[1] = rgb_item() + logger.debug('enocean: found blue-value') + self.send_4bs(data, id_offset) + elif (tx_eep == 'A5_38_08_02'): + #if isinstance(item, bool): + logger.debug('enocean: item is A5_38_08_02 type') + if not item(): + self.send_dim(id_offset, 0, 0) + logger.debug('enocean: sent off command') else: - logger.debug('enocean: item has no tx_eep value') + if 'ref_level' in item.level.conf: + dim_value = int(item.level.conf['ref_level']) + logger.debug('enocean: ref_level found') + else: + dim_value = 100 + logger.debug('enocean: no ref_level found. Setting to default 100') + self.send_dim(id_offset, dim_value, 0) + logger.debug('enocean: sent dim on command') + elif (tx_eep == 'A5_38_08_03'): + logger.debug('enocean: item is A5_38_08_03 type') + self.send_dim(id_offset, item(), 0) + logger.debug('enocean: sent dim command') + elif (tx_eep == 'A5_38_08_01'): + logger.debug('enocean: item is A5_38_08_01 type') + self.send_switch(id_offset, item(), 0) + logger.debug('enocean: sent switch command') + else: + logger.error('enocean: error: Unknown tx eep command') - def read_num_securedivices(self): + def read_num_securedevices(self): self._send_common_command(CO_RD_NUMSECUREDEVICES) logger.info("enocean: Read number of secured devices") @@ -479,56 +539,67 @@ def _send_common_command(self, _code, data=[], optional=[]): self._response_lock.release() self._cmd_lock.release() - def _send_radio_packet(self, id_offset, _code, data=[], optional=[]): + def _send_radio_packet(self, _code, data, optional=[], id_offset = 0): if (id_offset < 0) or (id_offset > 127): logger.error("enocean: invalid base ID offset range. (Is {}, must be [0 127])".format(id_offset)) return self._cmd_lock.acquire() self._last_cmd_code = SENT_RADIO_PACKET - self._send_packet(PACKET_TYPE_RADIO, [_code] + data + list((self.tx_id + id_offset).to_bytes(4, byteorder='big')) + [0x00], optional) + self._send_packet(PACKET_TYPE_RADIO, [_code] + list(reversed(data)) + list((self.tx_id + id_offset).to_bytes(4, byteorder='big')) + [0x00], optional) self._response_lock.acquire() # wait 5sec for response self._response_lock.wait(5) self._response_lock.release() self._cmd_lock.release() - def send_dim(self,id_offset=0, dim=0, dimspeed=0): + def send_4bs(self, data, id_offset = 0): + if (len(data) != 4): + logger.error("enocean: supply all 4 data bytes!") + return + self._send_radio_packet(0xA5, data, id_offset = id_offset) + + def send_learn_4bs(self, _func, _type, _manufacturer_id = 0x7ff, id_offset = 0): + if (_func > 0x3f) or (_type > 0x7f) or (_manufacturer_id > 0x7ff): + logger.error("enocean: learn - invalid params!") + return + data = [0, 0, 0, 0] + data[0] = 0x80 + data[1] = _manufacturer_id & 0xff + data[2] = (_manufacturer_id >> 8) + ((_type & 0x1f) << 3) + data[3] = (_type >> 5) + (_func << 2) + self.send_4bs(data, id_offset) + + def send_dim(self, dim = 0, dimspeed = 0, id_offset = 0): if (dimspeed < 0) or (dimspeed > 255): logger.error("enocean: sending dim command A5_38_08: invalid range of dimspeed") return logger.debug("enocean: sending dim command A5_38_08") if (dim == 0): - self._send_radio_packet(id_offset, 0xA5, [0x02, 0x00, dimspeed, 0x08]) + self.send_4bs([0x08, dimspeed, 0x00, 0x02], id_offset) elif (dim > 0) and (dim <= 100): - self._send_radio_packet(id_offset, 0xA5, [0x02, dim, dimspeed, 0x09]) + self.send_4bs([0x09, dimspeed, dim, 0x02], id_offset) else: logger.error("enocean: sending command A5_38_08: invalid dim value") - def send_switch(self,id_offset=0, on=0, block=0): + def send_switch(self, on = 0, block = 0, id_offset = 0): if (block < 0) and (block > 1): logger.error("enocean: sending switch command A5_38_08: invalid range of block (0,1)") return logger.debug("enocean: sending switch command A5_38_08") if (on == 0): - self._send_radio_packet(id_offset, 0xA5, [0x01, 0x00, 0x00, 0x08]) + self.send_4bs([0x08, 0x00, 0x00, 0x01], id_offset) elif (on == 1) and (block == 0): - self._send_radio_packet(id_offset, 0xA5, [0x01, 0x00, 0x00, 0x09]) + self.send_4bs([0x09, 0x00, 0x00, 0x01], id_offset) else: logger.error("enocean: sending command A5_38_08: error") def send_learn_dim(self, id_offset=0): - if (id_offset < 0) or (id_offset > 127): - logger.error("enocean: ID offset out of range (0-127). Aborting.") - return logger.info("enocean: sending learn telegram for dim command") - self._send_radio_packet(id_offset, 0xA5, [0x02, 0x00, 0x00, 0x00]) + self.send_4bs([0x00, 0x00, 0x00, 0x02], id_offset) def send_learn_switch(self, id_offset=0): - if (id_offset < 0) or (id_offset > 127): - logger.error("enocean: ID offset out of range (0-127). Aborting.") - return logger.info("enocean: sending learn telegram for switch command") - self._send_radio_packet(id_offset, 0xA5, [0x01, 0x00, 0x00, 0x00]) + self.send_4bs([0x00, 0x00, 0x00, 0x01], id_offset) def _calc_crc8(self, msg, crc=0): for i in msg: diff --git a/plugins/iaqstick/__init__.py b/plugins/iaqstick/__init__.py index 7250d564..4677b716 100644 --- a/plugins/iaqstick/__init__.py +++ b/plugins/iaqstick/__init__.py @@ -89,8 +89,8 @@ def _init_dev(self, dev): def run(self): devs = usb.core.find(idVendor=0x03eb, idProduct=0x2013, find_all=True) - if devs is None: - logger.error('iaqstick: iAQ Stick not found') + if not devs: + logger.error('iaqstick: no iAQ Stick found') return logger.debug('iaqstick: {} iAQ Stick connected'.format(len(devs))) self._intf = 0 diff --git a/plugins/mail/__init__.py b/plugins/mail/__init__.py index cd142f66..3fdcb3c1 100755 --- a/plugins/mail/__init__.py +++ b/plugins/mail/__init__.py @@ -23,8 +23,16 @@ import imaplib import smtplib import email -from email.mime.text import MIMEText + +import urllib + from email.header import Header +from email import encoders + +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.mime.image import MIMEImage logger = logging.getLogger('') @@ -170,6 +178,81 @@ def __call__(self, to, sub, msg): except: pass + def extended(self, to, sub, msg, sender_name: str, img_list: list=[], attachments: list=[]): + try: + smtp = self._connect() + except Exception as e: + logger.warning("Could not connect to {0}: {1}".format(self._host, e)) + return + try: + sender_name = Header(sender_name, 'utf-8').encode() + msg_root = MIMEMultipart('mixed') + msg_root['Subject'] = Header(sub, 'utf-8') + msg_root['From'] = email.utils.formataddr((sender_name, self._from)) + msg_root['Date'] = email.utils.formatdate(localtime=1) + if not isinstance(to, list): + to = [to] + msg_root['To'] = email.utils.COMMASPACE.join(to) + + msg_root.preamble = 'This is a multi-part message in MIME format.' + + msg_related = MIMEMultipart('related') + msg_root.attach(msg_related) + + msg_alternative = MIMEMultipart('alternative') + msg_related.attach(msg_alternative) + + msg_text = MIMEText(msg.encode('utf-8'), 'plain', 'utf-8') + msg_alternative.attach(msg_text) + + html = """ + + + + + + {}
+ + + + """.format(msg) # template + + msg_html = MIMEText(html.encode('utf-8'), 'html', 'utf-8') + msg_alternative.attach(msg_html) + + for i, img in enumerate(img_list): + if img.startswith('http://'): + fp = urllib.request.urlopen(img) + else: + fp = open(img, 'rb') + msg_image = MIMEImage(fp.read()) + msg_image.add_header('Content-ID', ''.format(i)) + msg_related.attach(msg_image) + + for attachment in attachments: + fname = os.path.basename(attachment) + + if attachment.startswith('http://'): + f = urllib.request.urlopen(attachment) + else: + f = open(attachment, 'rb') + msg_attach = MIMEBase('application', 'octet-stream') + msg_attach.set_payload(f.read()) + encoders.encode_base64(msg_attach) + msg_attach.add_header('Content-Disposition', 'attachment', + filename=(Header(fname, 'utf-8').encode())) + msg_root.attach(msg_attach) + + smtp.send_message(msg_root) + except Exception as e: + logger.warning("Could not send message {} to {}: {}".format(sub, to, e)) + finally: + try: + smtp.quit() + del(smtp) + except: + pass + def _connect(self): smtp = smtplib.SMTP(self._host, self._port) if self._ssl: @@ -191,4 +274,4 @@ def parse_logic(self, logic): pass def update_item(self, item, caller=None, source=None, dest=None): - pass + pass \ No newline at end of file diff --git a/plugins/vr100/README.md b/plugins/vr100/README.md index 059d5398..9e1c0285 100644 --- a/plugins/vr100/README.md +++ b/plugins/vr100/README.md @@ -1,6 +1,12 @@ # VR100 -# Requirements +## Supported Hardware + +A Vorwerk Kobold VR100 robotic vacuum cleaner with +* a retrofitted bluetooth module (e.g. HC-05) +* a retrofitted WLAN module (e.g. ESP8266-based) + +## Requirements for using Bluetooth bluez @@ -19,10 +25,6 @@ $ bluez-test-device trusted yes $ bluez-test-device list -## Supported Hardware - -A Vorwerk Kobold VR100 robotic vacuum cleaner with a retrofitted bluetooth module. - # Configuration ## plugin.conf @@ -37,7 +39,9 @@ A Vorwerk Kobold VR100 robotic vacuum cleaner with a retrofitted bluetooth modul Description of the attributes: -* __bt_addr__: MAC-address of the robot (find out with 'hcitool scan') +* __bt_addr__: MAC-address of the robot if using Bluetooth (find out with 'hcitool scan') +* __ip_addr__: IP-address/DNS name of the robot if using TCP/IP +* __tcp_port__: TCP-port of the robot if using TCP/IP * __update_cycle__: interval in seconds how often the data is read from the robot (default 60) ## items.conf diff --git a/plugins/vr100/__init__.py b/plugins/vr100/__init__.py index 537824db..30df6d06 100755 --- a/plugins/vr100/__init__.py +++ b/plugins/vr100/__init__.py @@ -28,12 +28,15 @@ class VR100(): - def __init__(self, smarthome, bt_addr, update_cycle="300"): + def __init__(self, smarthome, bt_addr = "", ip_addr = "", tcp_port = "0", update_cycle="300"): self._sh = smarthome self._update_cycle = int(update_cycle) self._query_items = {} - self._bt_addr = bt_addr + self._bt_addr = str(bt_addr) + self._ip_addr = str(ip_addr) + self._tcp_port = int(tcp_port) self._terminator = bytes('\r\n\x1a\r\n\x1a', 'utf-8') + self._connected = False def _update_values(self): #logger.debug("vr100: update") @@ -48,21 +51,33 @@ def _update_values(self): for item in self._query_items[query_cmd][field]['items']: item(value, 'VR100', "field \'{}\'".format(field)) + def connect(self): + logger.debug("vr100: connecting...") + try: + if self._bt_addr != "": + self._socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) + self._socket.settimeout(5.0) + self._socket.connect((self._bt_addr, 1)) + logger.info("vr100: via bluetooth connected to {}".format(self._bt_addr)) + elif (self._ip_addr != "") and (self._tcp_port != 0): + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(5.0) + self._socket.connect((self._ip_addr, self._tcp_port)) + logger.info("vr100: via tcp connected to {}:{}".format(self._ip_addr, self._tcp_port)) + else: + logger.error("vr100: missing configuration - either bt_addr = or tcp_addr = and tcp_port = must be set") + self._connected = True + except: + logger.error("vr100: establishing connection to robot failed - {}".format(sys.exc_info())) + def run(self): self.alive = True - if True: - try: - self._btsocket = socket.socket( - socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM) - self._btsocket.connect((self._bt_addr, 1)) - logger.info( - "vr100: via bluetooth connected to {}".format(self._bt_addr)) - except: - logger.error( - "vr100: establishing connection to robot failed - {}".format(sys.exc_info())) - return - self._sh.scheduler.add('VR100', self._update_values, - prio=5, cycle=self._update_cycle) + try: + self.connect() + if self._update_cycle: + self._sh.scheduler.add('VR100', self._update_values, prio=5, cycle=self._update_cycle) + except: + logger.error("vr100: initialisation failed".format(sys.exc_info())) def stop(self): self.alive = False @@ -72,7 +87,7 @@ def stop(self): logger.error( "vr100: removing VR100 from scheduler failed - {}".format(sys.exc_info())) try: - self._btsocket.close() + self._socket.close() except: logger.error( "vr100: closing connection to robot failed - {}".format(sys.exc_info())) @@ -110,14 +125,13 @@ def update_item(self, item, caller=None, source=None, dest=None): except: pass - def _recv(self, timeout=1.0): + def _recv(self): try: msg = bytearray() - self._btsocket.settimeout(timeout) while ((len(msg) < len(self._terminator)) or (msg[-len(self._terminator):] != self._terminator)): - msg += self._btsocket.recv(1000) + msg += self._socket.recv(1000) except socket.timeout: - logger.warning("vr100: rx: timeout after {}s".format(timeout)) + logger.warning("vr100: rx: timeout") return '' except: logger.warning("vr100: rx: exception - {}".format(sys.exc_info())) @@ -131,10 +145,11 @@ def _recv(self, timeout=1.0): def _send(self, msg): #logger.debug("vr100: tx: len={} / str={}".format(len(msg), msg)) + if not self._connected: + self.connect() try: - self._btsocket.send(bytes(msg + '\r\n', 'utf-8')) + self._socket.send(bytes(msg + '\r\n', 'utf-8')) except OSError as e: - if e.errno == 107: # Der Socket ist nicht verbunden - self.run() + self._connected = False except: logger.warning("vr100: rx: exception - {}".format(sys.exc_info()))