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 = """ + +
+ + + + {}