| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043 | #!/usr/bin/python3 -u## pip3 install pyyaml paho-mqtt pyserialimport yamlfrom yaml.constructor import ConstructorErrorimport serialimport sysimport timefrom time import sleepfrom time import localtime, strftimeimport datetimeimport paho.mqtt.client as mqttimport jsonimport osimport reimport configparserversion = 0.4# Change working dir to the same dir as this scriptos.chdir(sys.path[0])config = configparser.ConfigParser()config.read('cul2mqtt.ini')# global variablesverbose = Falsedebug = Falsequiet = TrueserialCULAvailable = Falsedevdata = {}RXcodesToDevFunction_IT = {}RXcodesToDevFunction_RAW = {}InTopicsToDevIds = {}lastDataReceiveTime = time.time()# config varsdeviceConfigFile = config['main'].get('devices_config_yml')log_enable = config['main'].getboolean('log_enable')logSuccessfulCommands = config['main'].getboolean('logSuccessfulCommands')log_path = config['main'].get('log_path')if not os.path.exists(log_path):    os.makedirs(log_path)mqtt_server = config['mqtt'].get('server')mqtt_port = config['mqtt'].getint('port')mqtt_user = config['mqtt'].get('user')mqtt_password = config['mqtt'].get('password')TX_interface_prefer = config['cul'].get('TX_interface_prefer') # UART or MQTTif TX_interface_prefer is None: # compatibility with old ini file structure    TX_interface_prefer = config['main'].get('TX_interface_prefer') # UART or MQTTrepeat_received_commands = False # not yet implementeddataReceiveTimeout = config['main'].getint('dataReceiveTimeout')lastReceivedStatusFile = config['main'].get('lastReceivedStatusFile')lastReceivedStatusFileUpdateTime = 0if len(sys.argv) >= 2:    if sys.argv[1] == "-q":        verbose = False        debug = False        quiet = True    elif sys.argv[1] == "-v":        verbose = True        debug = False        quiet = False    elif sys.argv[1] == "-d":        verbose = True        debug = True        quiet = False# serial (USB) CUL devicereceive_from_serial_cul = config['cul'].getboolean('receive_from_serial_cul')send_on_serial_cul = config['cul'].getboolean('send_on_serial_cul')serialPort = config['cul'].get('serialPort')serialBaudrate = config['cul'].getint('serialBaudrate')serialTimeout = config['cul'].getint('serialTimeout')# CUL init command for normal operation, i.E. X21, X05 culInitCmd = config['cul'].get('culInitCmd') + '\r\n'culSendsRSSI = config['cul'].getboolean('culSendsRSSI') # set depending on culInitCmd chosenserialCulInitTimeout = config['cul'].getint('serialCulInitTimeout')forceSerialCULConnected = config['cul'].getboolean('forceSerialCULConnected')# MQTT CULreceive_from_mqtt_cul = config['cul'].getboolean('receive_from_mqtt_cul')send_on_mqtt_cul = config['cul'].getboolean('send_on_mqtt_cul')mqtt_cul_topic_received = config['cul'].get('mqtt_cul_topic_received')mqtt_cul_topic_send = config['cul'].get('mqtt_cul_topic_send')filterSelfSentIncomingTimeout = config['main'].get('filterSelfSentIncomingTimeout')try:    from yaml import CLoader as Loaderexcept ImportError:    from yaml import Loader    def no_duplicates_constructor(loader, node, deep=False):    """Check for duplicate keys."""    mapping = {}    for key_node, value_node in node.value:        key = loader.construct_object(key_node, deep=deep)        value = loader.construct_object(value_node, deep=deep)        if key in mapping:            raise ConstructorError("while constructing a mapping", node.start_mark, "found duplicate key (%s)" % key, key_node.start_mark)        mapping[key] = value    return loader.construct_mapping(node, deep)yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, no_duplicates_constructor)log_last_date = Nonelogfilehandle = Falsedef log_start():    global logfilehandle, log_last_date    if log_enable:        if not os.path.exists(log_path):            os.makedirs(log_path)                    try:            _log_current_date = strftime("%Y%m%d")            _logfilename = _log_current_date + "_debug.log"            logfilehandle = open(log_path + '/' + _logfilename, 'a')            log_last_date = _log_current_date        except:            passdef log_rotate():    global logfilehandle, log_last_date    if log_enable:        _log_current_date = strftime("%Y%m%d")                if log_last_date != _log_current_date:            try:                logfilehandle.close()                _logfilename = _log_current_date + "_debug.log"                logfilehandle = open(log_path + '/' + _logfilename, 'a')                log_last_date = _log_current_date            except:                pass            def log_write(_msg):    global logfilehandle    if not quiet: print(_msg)    log_rotate()    if log_enable:        try:            logfilehandle.write("[" + str(datetime.datetime.now()) + "] " + _msg + "\n")            logfilehandle.flush()        except:            # guat dann hoit ned...            pass                        # Log successfully received (known) codeslogSC_last_date = NonelogSCfilehandle = Falsedef logSC_start():    global logSCfilehandle, logSC_last_date    if logSuccessfulCommands:        if not os.path.exists(log_path):            os.makedirs(log_path)                    try:            _log_current_date = strftime("%Y%m%d")            _logfilename = _log_current_date + "_received.log"            logSCfilehandle = open(log_path + '/' + _logfilename, 'a')            logSC_last_date = _log_current_date        except:            passdef logSC_rotate():    global logSCfilehandle, logSC_last_date    if logSuccessfulCommands:        _log_current_date = strftime("%Y%m%d")                if logSC_last_date != _log_current_date:            try:                logSCfilehandle.close()                _logfilename = _log_current_date + "_received.log"                logSCfilehandle = open(log_path + '/' + _logfilename, 'a')                logSC_last_date = _log_current_date            except:                pass                        def logSC_write(_msg):    global logSCfilehandle    if not quiet: print(_msg)    log_rotate()    if logSuccessfulCommands:        try:            logSCfilehandle.write("[" + str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + "] " + _msg + "\n")            logSCfilehandle.flush()        except:            # guat dann hoit ned...            pass            ###log_start()logSC_start()log_write("CUL2MQTT v" + str(version))log_write("=====================================")try:    with open(deviceConfigFile) as devfile:        devdata = yaml.load(devfile, Loader=yaml.FullLoader)        if debug:            log_write("")            log_write("")            log_write("==== parsing config file ====")            log_write("")            log_write(devdata)            log_write("")                for deviceid in devdata:            if debug:                log_write("")                log_write("")                log_write("Device: " + deviceid)                log_write(devdata[deviceid])                        if "RX" in devdata[deviceid].keys():                if devdata[deviceid]["RX"] != "":                    if debug:                        log_write("RX codes:")                    for key, value in devdata[deviceid]["RX"].items():                        if debug:                            log_write(str(key) + "->" + str(value))                        if value.startswith("i"):                            # is Intertechno RX code                            if value in RXcodesToDevFunction_IT.keys():                                log_write("")                                log_write("")                                log_write("ERROR: RX-string '" + str(value) + "' is already defined for another device! Must be unique.")                                raise                            else:                                RXcodesToDevFunction_IT[value] = deviceid, key                        else:                            # is other RX code - lets call it RAW                            if value in RXcodesToDevFunction_RAW.keys():                                log_write("")                                log_write("")                                log_write("ERROR: RX-string '" + str(value) + "' is already defined for another device! Must be unique.")                                raise                            else:                                RXcodesToDevFunction_RAW[value] = deviceid, key                        if "cmdTopic" in devdata[deviceid].keys():                if devdata[deviceid]["cmdTopic"] != "":                    cmdTopic = devdata[deviceid]["cmdTopic"]                    if debug:                        log_write("cmdTopic: " + cmdTopic)                    if cmdTopic in InTopicsToDevIds.keys():                        log_write("")                        log_write("")                        log_write("ERROR: cmdTopic '" + str(cmdTopic) + "' is already defined for another device! Must be unique.")                        raise                    else:                        InTopicsToDevIds[cmdTopic] = deviceid                    if debug:            log_write("")            log_write("")            log_write("")            log_write("RXcodesToDevFunction_IT:")            log_write(RXcodesToDevFunction_IT)            log_write("")            log_write("")            log_write("RXcodesToDevFunction_RAW:")            log_write(RXcodesToDevFunction_RAW)            log_write("")            log_write("")            log_write("InTopicsToDevIds:")            log_write(InTopicsToDevIds)            log_write("")            log_write("")            log_write("InTopicsToDevIds.keys():")            log_write(InTopicsToDevIds.keys())            log_write("")            log_write("")            log_write("devdata.keys():")            log_write(devdata.keys())            log_write("")            log_write("")            log_write("==== parsing config file complete ====")            log_write("")            log_write("")            log_write("")            log_write("")except ConstructorError as err:    log_write("ERROR on parsing configfile:")    log_write(err)    exit(1)except:    log_write("ERROR opening configfile")    log_write("Unexpected error: " + str(sys.exc_info()[0]))    exit(1)lastReceivedMaxAge = config['main'].getint('lastReceivedMaxAge') # ignore repeated messages when they are younger than x mslastReceivedTime = dict()lastSentTime = {}lastSentMinInterval = config['main'].getint('lastSentMinInterval') # ignore repeated messages when they are younger than x ms, should be > 1500 lastSentCmd = ""lastSentCmdTime = 0lastSentDev = ""lastSentDevCmd = ""def touch(fname, times=None):    with open(fname, 'a'):        os.utime(fname, times)def on_connect(client, userdata, flags, rc):    if verbose:        log_write("MQTT connected with result code " + str(rc))    if receive_from_mqtt_cul:        if mqtt_cul_topic_received != "":            client.subscribe(mqtt_cul_topic_received)                        if mqtt_cul_topic_send != "":                #client.publish                mqttc.publish(mqtt_cul_topic_send, culInitCmd, qos=0, retain=False)                for in_topic in InTopicsToDevIds.keys():        if in_topic != "":            client.subscribe(in_topic)            if verbose:                log_write("MQTT subscribed: " + in_topic)def on_disconnect(client, userdata, rc):    if rc != 0:        log_write("Unexpected MQTT disconnection. Will auto-reconnect")        def on_message(client, userdata, msg):    #print(msg.topic + ": " + str(msg.payload))    payload = msg.payload.decode("utf-8")        if verbose:        log_write("")        log_write("MQTT received: " + msg.topic + " -> " + str(payload))        # MQTT message is output from CUL    if receive_from_mqtt_cul and msg.topic == mqtt_cul_topic_received:        payload = payload.rstrip()                    # support output from Tasmota SerialBridge on tele/[DEVICE]/RESULT topic        # example: {"SerialReceived":"p10  144   80  480   48   32 1360  28  1  3 4   160  6336     0 735C1470"}        if payload.startswith('{"SerialReceived":'):            payload_json = json.loads(payload)            payload = payload_json.get('SerialReceived')            if payload != None:                cul_received(payload, "MQTT") # to lower case as MQTT CUL via Tasmota SerialBridge with Rule changes output to all uppercase                if verbose:                    log_write("MQTT-CUL RX: '" + payload + "'")        elif not payload.startswith('{'):            # ignore everything different starting with {             cul_received(payload.lower(), "MQTT") # to lower case as MQTT CUL via Tasmota SerialBridge with Rule changes output to all uppercase            if verbose:                log_write("MQTT-CUL RX: '" + payload.lower() + "'")            else:        for in_topic, dev in InTopicsToDevIds.items():            if msg.topic == in_topic:                if verbose: log_write("MQTT received - '" + msg.topic + "' = '" + payload + "' => DEV: " + dev)                                if 'name' in devdata[dev].keys():                    log_write('devName: ' + devdata[dev]['name'])                                global lastSentDev, lastSentDevCmd, lastSentCmdTime                now = int(round(time.time() * 1000))                if debug:                    log_write("dev="+dev+", lastSentDevCmd="+lastSentDevCmd)                if dev == lastSentDev and payload == lastSentDevCmd and (now - lastSentCmdTime) < 1000:                    if verbose:                        log_write("MQTT: ignored command as we just sent this.")                else:                    cul_send(dev, payload)                                    if 'statTopic' in devdata[dev].keys():                    if verbose: log_write('statTopic: ' + devdata[dev]['statTopic'])                    mqttc.publish(devdata[dev]['statTopic'], payload, qos=0, retain=False)                                    if 'add_statTopics_on' in devdata[dev].keys():                    if verbose: log_write("add_statTopics_on:")                    for res in devdata[dev]['add_statTopics_on']:                        if 'on_payload' in res and 'topic' in res and 'payload' in res:                            if payload == res['on_payload'] and payload != "" and res['topic'] != "" and res['payload'] != "":                                if verbose: log_write("  on '" + payload + "': '" + res['payload'] + "' => '" + res['topic'] + "'")                                mqttc.publish(res['topic'], res['payload'], qos=0, retain=False)                    def publish_device_statusupdate(device, cmd, value):    if device in devdata.keys():        if 'statTopic' in devdata[device].keys():            statTopic = devdata[device].get('statTopic')                        if value is not None and cmd == "%VALUE%":                 if verbose: log_write("    MQTT publish: '" + value + "' -> '" + statTopic + "'")                mqttc.publish(statTopic, value, qos=0, retain=False)            else:                 if verbose: log_write("    MQTT publish: '" + cmd + "' -> '" + statTopic + "'")                mqttc.publish(statTopic, cmd, qos=0, retain=False)                        if logSuccessfulCommands:                 logSC_write("received from device: " + device + ", cmd: " + cmd + ", value: " + str(value) + ", topic: " + statTopic)                    if 'add_statTopics_on' in devdata[device].keys():            if verbose: log_write("    MQTT publish add_statTopics_on:")            for res in devdata[device].get('add_statTopics_on'):                if 'on_payload' in res and 'topic' in res and 'payload' in res:                    if cmd == res['on_payload']:                        if verbose: log_write("        on '" + res['on_payload'] + "' -> publish '" + res['payload'] + "' on topic '" + res['topic'] + "'")                        mqttc.publish(res['topic'], res['payload'], qos=0, retain=False)                                if 'add_statTopics' in devdata[device].keys():            if verbose: log_write("    MQTT publish on add_statTopics:")            for res in devdata[device]['add_statTopics']:                if verbose: log_write("        '" + cmd + "' -> '" + res + "'")                mqttc.publish(res, cmd, qos=0, retain=False)        def parseRXCode(rx_code, source_cul):    receivedForDevice = None    receivedCmnd = None    receivedValue = None    skipPublishing = False        if rx_code.startswith("i"):        # parse Intertechno RX code        if debug: log_write("    PROTOCOL: Intertechno")        sucessfullyParsedITCode = False                # look if this code is in the device_config        if rx_code in RXcodesToDevFunction_IT:            if debug: log_write("    code found in device config")            receivedForDevice = RXcodesToDevFunction_IT[rx_code][0]            receivedCmnd = RXcodesToDevFunction_IT[rx_code][1]            sucessfullyParsedITCode = True            if debug: log_write("    DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", RX: " + rx_code)            if verbose:                 #log_write("")                log_write("    CUL '" + source_cul + "' received '" + rx_code + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd)                # if this had no result -> try again with last 8 bit stripped, as in many cases this is only RSSI and not part of the code        # in this case treat this last 8 bit as sensor value        # --> sensor with 16 bit address and 8 bit data (i.E. Arduino sending using RCSwitch library)        rx_code_stripped = rx_code[0:-2]        if not sucessfullyParsedITCode and rx_code_stripped in RXcodesToDevFunction_IT:            if debug: log_write("    code found in device config with last 8 bit stripped")            receivedForDevice = RXcodesToDevFunction_IT[rx_code_stripped][0]            receivedCmnd = RXcodesToDevFunction_IT[rx_code_stripped][1]                        receivedValue = rx_code[-2:]                        # convert value to decimal if enabled for this device            if "convertValueToDecimal" in devdata[receivedForDevice]:                if devdata[receivedForDevice]["convertValueToDecimal"] == True:                    if debug: log_write("    converting value to decimal")                    receivedValue = str(int(receivedValue, base=16))                        sucessfullyParsedITCode = True            if debug: log_write("    DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue + ", RX: " + rx_code_stripped)            if verbose:                 #log_write("")                log_write("    CUL '" + source_cul + "' received '" + rx_code_stripped + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue)                        # if this also had no result -> try again with last 12 bit stripped        # --> sensor with 12 bit address and 12 bit data (i.E. Arduino sending using RCSwitch library)        rx_code_stripped = rx_code[0:-3]        if not sucessfullyParsedITCode and rx_code_stripped in RXcodesToDevFunction_IT:            if debug: log_write("    code found in device config with last 12 bit stripped")            receivedForDevice = RXcodesToDevFunction_IT[rx_code_stripped][0]            receivedCmnd = RXcodesToDevFunction_IT[rx_code_stripped][1]                        receivedValue = rx_code[-3:]            tmpReceivedValue = int(receivedValue, base=16)                        if debug: log_write("    RAW receivedValue=" + hex(tmpReceivedValue))                                    # sensor type DS18B20            # -> convert raw data to temperature °C            if "isSensorType" in devdata[receivedForDevice]:                if devdata[receivedForDevice]["isSensorType"] == "DS18B20":                    if debug:                         log_write("    is sensor type DS18B20")                        log_write("    converting RAW data...")                                        if tmpReceivedValue == 0xfff or tmpReceivedValue == 0xffe:                        _battState = ""                        # this is a low battery warning                        # -> send warning on MQTT if configured and skip value transmitting                        if "battTopic" in devdata[receivedForDevice]:                            if devdata[receivedForDevice]["battTopic"] is not None:                                if tmpReceivedValue == 0xfff:                                    _battState = "OK"                                elif tmpReceivedValue == 0xffe:                                    _battState = "LOW"                                mqttc.publish(devdata[receivedForDevice]["battTopic"], _battState, qos=0, retain=False)                                skipPublishing = True # no data will be published this time                    else:                        # left shift 2 bits as sender right shifts it                        tmpReceivedValue = tmpReceivedValue << 2                                                if debug: log_write("    receivedValue=" + str(tmpReceivedValue))                                                # if MSB (bit 13) == 1 -> negative value                        if tmpReceivedValue >= 8192:                            tmpReceivedValue = tmpReceivedValue - 16384                                                tmpReceivedValue = round(0.0078125 * tmpReceivedValue, 1)                        if debug: log_write("    converted value=" + str(tmpReceivedValue))                                                if tmpReceivedValue >= -50 and tmpReceivedValue <= 125:                            receivedValue = str(tmpReceivedValue)                        else:                            log_write("    WARN: value out of range - skipping")                            skipPublishing = True # no data will be published this time                                                # not a "special" sensor but convertValueToDecimal is enabled            # -> convert value to decimal if enabled for this device            elif "convertValueToDecimal" in devdata[receivedForDevice]:                if devdata[receivedForDevice]["convertValueToDecimal"] == True:                    if debug: log_write("    converting value to decimal")                    receivedValue = str(int(receivedValue, base=16))                                    sucessfullyParsedITCode = True            if debug: log_write("    DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue + ", RX: " + rx_code_stripped)            if verbose:                 #log_write("")                log_write("    CUL '" + source_cul + "' received '" + rx_code_stripped + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", VALUE: " + receivedValue)                # if this also did not work, try to parse it as "default/cheap" Intertechno code...         if not sucessfullyParsedITCode:            if debug: log_write("    treat code as 'default/cheap' Intertechno code...")            try:                receivedForDevice, receivedCmnd = decodeInterTechnoRX(rx_code_stripped)            except:                log_write("    Error parsing code as 'default/cheap' Intertechno code.")            if debug:                 log_write(str(receivedForDevice) + ", " + str(receivedCmnd))                else:        # parse other/RAW RX code        if debug: log_write("    PROTOCOL: OTHER/RAW")        if rx_code in RXcodesToDevFunction_RAW:            receivedForDevice = RXcodesToDevFunction_RAW[rx_code][0]            receivedCmnd = RXcodesToDevFunction_RAW[rx_code][1]            if debug: log_write("    DEV: " + receivedForDevice + ", CMD: " + receivedCmnd + ", RX: " + rx_code)            if verbose:                 #log_write("")                log_write("    CUL '" + source_cul + "' received '" + rx_code + "' => DEV: " + receivedForDevice + ", CMD: " + receivedCmnd)        #if debug:     #    log_write("    DEV: " + str(receivedForDevice) + ", CMD: " + str(receivedCmnd) + ", RX: " + rx_code)        if receivedForDevice != None and receivedCmnd != None and receivedForDevice != False and receivedCmnd != False and not skipPublishing:        publish_device_statusupdate(receivedForDevice, receivedCmnd, receivedValue)def decodeInterTechnoRX(rx_code):    # decode old fixed code from Intertechno remotes    _housecode = None    _devaddr = None    _command = None    _itname = None        #print(rx_code[0:1])    #print(rx_code[1:3])    #print(rx_code[3:5])    #print(rx_code[5:7])        if rx_code[0:1] == "i":         if rx_code[1:3] == "00": _housecode = "A"        elif rx_code[1:3] == "40": _housecode = "B"        elif rx_code[1:3] == "10": _housecode = "C"        elif rx_code[1:3] == "50": _housecode = "D"        elif rx_code[1:3] == "04": _housecode = "E"        elif rx_code[1:3] == "44": _housecode = "F"        elif rx_code[1:3] == "14": _housecode = "G"        elif rx_code[1:3] == "54": _housecode = "H"        elif rx_code[1:3] == "01": _housecode = "I"        elif rx_code[1:3] == "41": _housecode = "J"        elif rx_code[1:3] == "11": _housecode = "K"        elif rx_code[1:3] == "51": _housecode = "L"        elif rx_code[1:3] == "05": _housecode = "M"        elif rx_code[1:3] == "45": _housecode = "N"        elif rx_code[1:3] == "15": _housecode = "O"        elif rx_code[1:3] == "55": _housecode = "P"                if rx_code[3:5] == "00": _devaddr = "1"        elif rx_code[3:5] == "40": _devaddr = "2"        elif rx_code[3:5] == "10": _devaddr = "3"        elif rx_code[3:5] == "50": _devaddr = "4"        elif rx_code[3:5] == "04": _devaddr = "5"        elif rx_code[3:5] == "44": _devaddr = "6"        elif rx_code[3:5] == "14": _devaddr = "7"        elif rx_code[3:5] == "54": _devaddr = "8"        elif rx_code[3:5] == "01": _devaddr = "9"        elif rx_code[3:5] == "41": _devaddr = "10"        elif rx_code[3:5] == "11": _devaddr = "11"        elif rx_code[3:5] == "51": _devaddr = "12"        elif rx_code[3:5] == "05": _devaddr = "13"        elif rx_code[3:5] == "45": _devaddr = "14"        elif rx_code[3:5] == "15": _devaddr = "15"        elif rx_code[3:5] == "55": _devaddr = "16"                if rx_code[5:7] == "15": _command = "ON"        elif rx_code[5:7] == "14": _command = "OFF"                if _housecode != None and _devaddr != None and _command != None:            _itname = "IT_" + _housecode + _devaddr            if debug: log_write("valid IT code: '" + _itname + "' => '" + _command + "'")            return _itname, _command                    else:            if debug: log_write("unknown or invalid IT code '" + rx_code + "'")            return False, False    else:        if debug: log_write("unknown or invalid IT code '" + rx_code + "'")def encodeInterTechnoRX(itname, cmd):    # decode old fixed code from InterTechno remotes    _housecode = None    _devaddr = None    _command = None        #print(itname[0:3])    #print(itname[3:4])    #print(itname[4:])        if itname[0:3] == "IT_":         if itname[3:4] == "A": _housecode = "00"        elif itname[3:4] == "B": _housecode = "40"        elif itname[3:4] == "C": _housecode = "10"        elif itname[3:4] == "D": _housecode = "50"        elif itname[3:4] == "E": _housecode = "04"        elif itname[3:4] == "F": _housecode = "44"        elif itname[3:4] == "G": _housecode = "14"        elif itname[3:4] == "H": _housecode = "54"        elif itname[3:4] == "I": _housecode = "01"        elif itname[3:4] == "J": _housecode = "41"        elif itname[3:4] == "K": _housecode = "11"        elif itname[3:4] == "L": _housecode = "51"        elif itname[3:4] == "M": _housecode = "05"        elif itname[3:4] == "N": _housecode = "45"        elif itname[3:4] == "O": _housecode = "15"        elif itname[3:4] == "P": _housecode = "55"                if itname[4:] == "1": _devaddr = "00"        elif itname[4:] == "2": _devaddr = "40"        elif itname[4:] == "3": _devaddr = "10"        elif itname[4:] == "4": _devaddr = "50"        elif itname[4:] == "5": _devaddr = "04"        elif itname[4:] == "6": _devaddr = "44"        elif itname[4:] == "7": _devaddr = "14"        elif itname[4:] == "8": _devaddr = "54"        elif itname[4:] == "9": _devaddr = "01"        elif itname[4:] == "10": _devaddr = "41"        elif itname[4:] == "11": _devaddr = "11"        elif itname[4:] == "12": _devaddr = "51"        elif itname[4:] == "13": _devaddr = "05"        elif itname[4:] == "14": _devaddr = "45"        elif itname[4:] == "15": _devaddr = "15"        elif itname[4:] == "16": _devaddr = "55"                if cmd == "ON": _command = "15"        elif cmd == "OFF": _command = "14"                if debug: print("IT housecode=", _housecode, "- devaddr=", _devaddr, "- command=", _command)                        if _housecode != None and _devaddr != None and _command != None:            _rxcode = "i" + _housecode + _devaddr + _command            #print("encoded IT RX code: '" + itname + "' => '" + cmd + "' = '" + _rxcode)            return _rxcode                    else:            if debug: log_write("    unknown or invalid IT code '" + rx_code + "'")            return False            else:        if debug: log_write("    unknown or invalid IT code '" + rx_code + "'")def cul_received(payload, source_cul):    global lastReceivedTime, lastReceivedMaxAge, lastDataReceiveTime, lastReceivedStatusFile, lastReceivedStatusFileUpdateTime        lastDataReceiveTime = time.time()    if debug:        print("DEBUG: lastDataReceiveTime=" + str(lastDataReceiveTime))        # touch lastReceivedStatusFile if configured    if lastReceivedStatusFile is not None:        if (time.time() - lastReceivedStatusFileUpdateTime) >= 60:            # try updating lastReceivedStatusFile if last time was >60s ago            lastReceivedStatusFileUpdateTime = time.time()            try:                touch(lastReceivedStatusFile)                if debug:                    print("DEBUG: lastReceivedStatusFile updated")            except:                if debug:                    print("ERROR: could not update lastReceivedStatusFile")    else:        if debug:            print("DEBUG: lastReceivedStatusFile not defined")                    if payload[:2] == 'is': # msg is reply from CUL to raw send command        pass            elif payload[:1] == 'i': # is a IT compatible code - so look it up in the code table                if culSendsRSSI:            inCmd = payload[:-2] # strip last 2 chars, depending on used CUL receive mode - if enabled this is only RSSI        else:            inCmd = payload                    if verbose:            log_write("    inCmd: " + inCmd + ", receiving CUL: " + source_cul)                # filter fast repeated codes (strip first char on Intertechno codes as the repetation will come in as RAW without "i" prefix)        ignoreCommand = False        if inCmd in lastReceivedTime.keys():            lastTime = int(lastReceivedTime[inCmd])            now = int(round(time.time() * 1000))            tdelta = (now - lastTime)            if debug: log_write("    TDELTA = " + str(tdelta))            if debug: log_write("    lastTime = " + str(lastTime))                        if tdelta < lastReceivedMaxAge:                if verbose: log_write("    ignoring code from CUL '" + source_cul + "', CMD: '" + inCmd + "' - already received " + str(tdelta) + " ms ago")                ignoreCommand = True                if not ignoreCommand:            lastReceivedTime[inCmd] = int(round(time.time() * 1000))            parseRXCode(inCmd, source_cul)                elif payload[:1] == 'p': # is RAW data        # example: "p11  288  864  800  320  288  832  33  1  4 1   288 10224     0 A4CEF09580"        # split string and extract last row as we dont need the rest        splitPayload = payload.split(' ')        actualPayload = splitPayload[len(splitPayload)-1]        if debug: log_write("    actualPayload: " + actualPayload)                        ignoreCommand = False                        # handle/filter repetations of Intertechno codes        isITrepetation = False        if ('i'+actualPayload) in lastReceivedTime.keys():            isITrepetation = True            if debug: log_write("    skipping repeated Intertechno code")            lastTime = int(lastReceivedTime['i'+actualPayload])            now = int(round(time.time() * 1000))            tdelta = (now - lastTime)            if debug: log_write("    TDELTA = " + str(tdelta))            if tdelta < lastReceivedMaxAge:                if verbose: log_write("    ignoring code from CUL '" + source_cul + "', CMD: '" + 'i'+actualPayload + "' - already received " + str(tdelta) + " ms ago")                ignoreCommand = True                        # filter fast repeated codes        if not isITrepetation:            if actualPayload in lastReceivedTime.keys():                lastTime = int(lastReceivedTime[actualPayload])                now = int(round(time.time() * 1000))                tdelta = (now - lastTime)                if debug: log_write("    TDELTA = " + str(tdelta))                if tdelta < lastReceivedMaxAge:                    if verbose: log_write("    ignoring code from CUL '" + source_cul + "', CMD: '" + actualPayload + "' - already received " + str(tdelta) + " ms ago")                    ignoreCommand = True                if not ignoreCommand:            if isITrepetation:                # treat as Intertechno code                lastReceivedTime['i'+actualPayload] = int(round(time.time() * 1000))                parseRXCode('i'+actualPayload, source_cul)            else:                lastReceivedTime[actualPayload] = int(round(time.time() * 1000))                parseRXCode(actualPayload, source_cul)                                        #if repeat_received_commands:            #    lastSentLength = len(lastSent)            #    i = 0            #    dontRepeat = False            #    while i < lastSentLength:            #        #print(str(i) + ": " + lastReceived[i])            #        if lastSent[i] == decCmd:            #            lastTime = int(lastSentTime[i])            #            now = int(round(time.time() * 1000))            #            tdelta = (now - lastTime)            #            #print("TDELTA = " + str(tdelta))            #            if tdelta < lastSentMaxAge:            #                print("ignoring command as it originated from ourselfs " + inCmd + " " + str(tdelta) + " ms ago")            #                dontRepeat = True            #                #break            #        i += 1            #    #cmdToSend = culSendCmds[decCmd]            #    if not dontRepeat:            #        if device != "" and cmd != "":            #            print("REPEATING COMMAND: " + cmd + " TO DEVICE " + device)            #            cul_send(device, cmd)            def IT_RXtoTXCode(itReceiveCode):    if debug:         statusstr = "IT_RXtoTXCode    "        statusstr += "RX: "        statusstr += itReceiveCode        #print("IT_RXtoTXCode ReceiveCode:  " + itReceiveCode)        itReceiveCode = itReceiveCode[1:] # remove first character "i"    itReceiveCodeLengthBytes = int(len(itReceiveCode)/2)        itReceiveCodeBytes = []    itTransmitTristate = "is"        for x in range(itReceiveCodeLengthBytes):        itReceiveCodeBytes.append(bin(int(itReceiveCode[x*2:(x*2+2)],16))[2:].zfill(8))            for x in range(len(itReceiveCodeBytes)):        #print("IT REC byte " + str(x) + " = " + str(itReceiveCodeBytes[x]))        for y in range(4):            quarterbyte = str(itReceiveCodeBytes[x][y*2:y*2+2])            if quarterbyte == "00":                tmpTristate = "0";            elif quarterbyte == "01":                tmpTristate = "F";            elif quarterbyte == "10":                tmpTristate = "D";            elif quarterbyte == "11":                tmpTristate = "1";            #print(quarterbyte +  " -> " + tmpTristate)            itTransmitTristate = itTransmitTristate + tmpTristate        if debug:         statusstr += " -> TX: "        statusstr += itTransmitTristate        #print("IT_RXtoTXCode TransmitCode: " + itTransmitTristate)        log_write(statusstr)        return(itTransmitTristate)        def cul_send(device, cmd):    global lastSentTime    culCmd = ""    culSendCmdsKeyName = device + ' ' + cmd    if debug: log_write("CUL send '" + cmd + "' to device '" + device + "'")                tx_code = False        if 'TX' in devdata[device]:        if debug: print("TX data available, cmd="+cmd)        if debug: print(devdata[device]['TX'])        if cmd in devdata[device]['TX'].keys():            tx_code = devdata[device]['TX'][cmd]            if verbose: log_write("    TX code for '" + cmd + "': " + tx_code)        if not tx_code:        if verbose: log_write("    deviceID: " + device)        if 'RX' in devdata[device].keys():            if verbose:                 log_write("    RX code configured, cmd=" + cmd)                log_write(devdata[device]['RX'])            if cmd in devdata[device]['RX'].keys():                rx_code = devdata[device]['RX'][cmd]                if debug: log_write("    RX code for '" + cmd + "': " + rx_code)                tx_code = IT_RXtoTXCode(rx_code)                if verbose: log_write("    TX code for '" + cmd + "': " + tx_code)            else:                 log_write("    RX code for '" + cmd + "' NOT FOUND")                        elif device.startswith("IT_"):            # InterTechno device with fixed code - encode RX code for IT device name and convert to TX code            rx_code = encodeInterTechnoRX(device, cmd)            if rx_code:                if debug: log_write("    RX code for '" + cmd + "': " + rx_code)                tx_code = IT_RXtoTXCode(rx_code)                if verbose: log_write("    TX code for '" + cmd + "': " + tx_code)                if not tx_code:        if verbose: log_write("    no valid TX code for this device/command")    else:        now = int(round(time.time() * 1000))                # look if this command has been sent in the past, and when        if culSendCmdsKeyName in lastSentTime.keys():            lastTime = lastSentTime[culSendCmdsKeyName]        else:            lastTime = 0                    lastTimeAge = now - lastTime        if verbose: log_write('    lastTime: ' + str(lastTimeAge) + 'ms ago')                if lastTimeAge > lastSentMinInterval: # only send if last time + min interval is exceeded            lastSentTime[culSendCmdsKeyName] = now # save what we send, so that we dont repeat our own sent messages if repeating is enabled                        TX_interface = TX_interface_prefer            if 'TX_interface' in devdata[device].keys():                if verbose: log_write("    TX_interface: " + devdata[device]['TX_interface'])                            if TX_interface == "UART" and not serialCULAvailable:                TX_interface = "MQTT"                        global lastSentCmd, lastSentCmdTime, lastSentDev, lastSentDevCmd            lastSentCmd = tx_code            lastSentCmdTime = now            lastSentDev = device            lastSentDevCmd = cmd                        if send_on_mqtt_cul and (TX_interface == "MQTT" or TX_interface == "both"):                log_write("    TX via MQTT: " + tx_code)                mqttc.publish(mqtt_cul_topic_send, tx_code, qos=0, retain=False)                            if serialCULAvailable and send_on_serial_cul and (TX_interface == "UART" or TX_interface == "both"):                log_write("    TX via UART: " + tx_code)                culCmd = tx_code + '\r\n'                ser.write(culCmd.encode('ascii'))                        else:            log_write("WARNING: CUL send command repeated too quickly.")                publish_device_statusupdate(device, cmd, None)# mainif receive_from_serial_cul or send_on_serial_cul:    if not os.path.exists(serialPort):        log_write("ERROR opening connection to serial CUL... device '" + serialPort + "' does not exist.")        if log_enable: log_write("CUL2MQTT v"+str(version)+" starting")                if receive_from_mqtt_cul:            if forceSerialCULConnected:                exit(2)            else:                log_write("resuming in MQTT-CUL only mode...")                TX_interface_prefer = "MQTT"                receive_from_serial_cul = False                send_on_serial_cul = False                serialCULAvailable = False                log_write("")                log_write("")    else:        log_write("opening connection to UART-CUL...")        serLine = ""        ser = serial.Serial(port=serialPort,baudrate=serialBaudrate,parity=serial.PARITY_NONE,stopbits=serial.STOPBITS_ONE,bytesize=serial.EIGHTBITS,timeout=serialTimeout)        sleep(serialCulInitTimeout)        ser.write('V\r\n'.encode('ascii'))  # get CUL version info        serLine = ser.readline()        serLine = serLine.decode('ascii').rstrip('\r\n')        if serLine.startswith("V ") and serLine.find("culfw") != -1:            log_write("connected. CUL version: " + serLine)            serialCULAvailable = True            sleep(0.1)            log_write('Initializing CUL with command: ' + culInitCmd.rstrip('\r\n'))            ser.write(culInitCmd.encode('ascii'))  # initialize CUL in normal receive mode            sleep(0.5)        else:            log_write("WARNING: could not connect UART-CUL")            receive_from_serial_cul = False            send_on_serial_cul = False            serialCULAvailable = False            TX_interface_prefer = "MQTT"            if forceSerialCULConnected:                exit(2)mqttc = mqtt.Client()mqttc.on_connect = on_connectmqttc.on_disconnect = on_disconnectmqttc.on_message = on_messageif mqtt_user is not None and mqtt_password is not None:    if len(mqtt_user) > 0 and len(mqtt_password) > 0:        mqttc.username_pw_set(mqtt_user, mqtt_password)    mqttc.connect(mqtt_server, mqtt_port, 60)mqttc.loop_start()try:    while True:        if receive_from_serial_cul:            serLine = ser.readline()            if len(serLine) > 0:                now = int(round(time.time() * 1000))                recvCmd = serLine.decode('ascii')                recvCmd = recvCmd.rstrip('\r\n')                                #if debug:                #    print("lastSentCmd: " + lastSentCmd + ", lastSentCmdTime=" + str(lastSentCmdTime))                                if recvCmd == lastSentCmd and (now - lastSentCmdTime) < filterSelfSentIncomingTimeout:                    pass                else:                    if verbose:                        log_write("")                        log_write("UART-CUL RX: '" + recvCmd + "'")                    cul_received(recvCmd, "UART")                    #touch("/tmp/culagent_running")                # check if receive timeout is exceeded         # - if so there is possibly something wrong        # -> quit program (will be restarted by systemd)        if dataReceiveTimeout is not None:            if (time.time() - lastDataReceiveTime) >= dataReceiveTimeout:                print("WARNING: received no data from CUL for >" + str(dataReceiveTimeout) + "s -> something has gone wrong -> quitting program.")                quit()        ##else:        ##    print("dataReceiveTimeout not configured")                            sleep(0.05)except KeyboardInterrupt:    print("\n")
 |