def isCurrentKNXAttribute(knxDest, knxFormat, newVal) -> bool: """ checks whether new value matches to stored value last sent on the bus :param knxFormat: DPT format that is expected for the given destination :param knxDest: KNX destionation under which the value is stored :param newVal: value in dpt representation :returns true if new value matches cached value """ ret = False try: curKNXVal = EIBClientFactory().getClient().GroupCache_Read(knxDest) if len(newVal) == 1 and len(curKNXVal) == 2: curKNXVal = curKNXVal[1:] curKNXVal = curKNXVal.upper().strip() newVal = newVal.upper().strip() # normalize numeric and boolean values if is_number(curKNXVal): curKNXVal = convert_number(curKNXVal) elif is_bool(curKNXVal): curKNXVal = bool(curKNXVal) if is_number(newVal): newVal = convert_number(newVal) elif is_bool(newVal): newVal = bool(newVal) # compare normalized values ret = curKNXVal == newVal except ValueError as ex: log('warning', 'Value comparison failed - {0}'.format(ex)) return ret
def updateOccurred(self, srcAddr, val): """ takes value from KNX and sends it to ZigBee device """ knxSrc = printGroup(self.gaddrInt) val = printValue(val, len(val)) # avoid error state in case KNX device is reset via the KNX app if not val: return val = int(val, 16) # transform data from python to zigbee protocol adequate form zbValue = zigbee_utils.getZigBeeValue(self.zbFormat, val) # sends update to zigbee device if self.zbClient.setAttribute(attr=self.zbAttr, val=zbValue): log( 'info', 'Value updated based on KNX value change {0}({1}): {2}(KNX value: {3}) for ZigBee client {4}' .format(self.attrName, knxSrc, zbValue, val, self.zbClient.uniqueID)) else: log( 'error', 'Value could not be updated based on KNX value change {0}({1}): {2}(KNX value: {3}) for ZigBee client {4}' .format(self.attrName, knxSrc, zbValue, val, self.zbClient.uniqueID))
def getClientState(self, id, type, attr, section=None): """ get attribute for a defined client """ ret = None # default is state sectionwith only a few exceptions if section is None: section = 'state' if not ZigBeeGateway.__state: # check if state information was requested at least once self.getState() try: # section can be omitted by specifying >>zigbeeSection: ''<< in configuration if len(section) > 0: ret = ZigBeeGateway.__state[type][str(id)][section][attr] else: ret = ZigBeeGateway.__state[type][str(id)][attr] except KeyError as ex: log( 'error', 'Configuration error - attribute "{0}" in section "{1}" for deConzID "{2}" type "{3}" not defined: {4}' .format(attr, section, id, type, ex)) return ret
def getAttribute(self, attrName, mbFormat, mbAddr): val = None if self.__mbcImpl.connect(): try: # check modbus data type # TODO implement more ModBus datatypes if mbFormat == 'float': # configuration supports summing up multiple values automatically # single value from corresponding modbus client if isinstance(mbAddr, int): val = modbus_utils.ReadFloat(self.__mbcImpl, mbAddr) # sum of multiple values from corresponding modbus client elif isinstance(mbAddr, list): val = 0 # TODO implement functions for ids in mbAddr: val = val + modbus_utils.ReadFloat( self.__mbcImpl, ids) except Exception as ex: log( 'error', 'Error reading ModBus value - {0}: {1}'.format( attrName, ex)) self.__mbcImpl.close() else: # log connection error log( 'error', 'Could not connect to ModBus server {0}:{1}'.format( self.__mbcImpl.host, self.__mbcImpl.port)) return val
def updateOccurred(self, srcAddr, val): """ takes value from KNX and sends it to another knx device """ knxSrc = printGroup(self.gaddrInt) # get DPT implementation # dc = DPTXlatorFactoryFacade().create(self.knxFormat) # if dc.checkFrame(val): # val = dc.frameToData(val) val = int(printValue(val, len(val)), 16) # currently no conversion from one DPT type to another is foreseen # sends update to the other knx device if val is not None and \ self.knxClient.setAttribute(attrName=self.attrName, val=val, dest=self.knxDest, format=self.knxFormat, function=self.function): log('info', 'Value updated based on KNX value change {0}({1}): {2} for KNX client {3}'.format(self.attrName, knxSrc, val, self.knxDest)) else: log('error', 'Value could not be updated based on KNX value change {0}({1}): {2} for KNX client {3}'.format( self.attrName, knxSrc, val, self.knxDest))
def __init__(self, host, port, user, passwd, name, topic, knxDest, knxFormat, function, flags): super(_MQTTClient, self).__init__() self.attrName = name self.knxDest = knxDest self.knxFormat = knxFormat self.function = function self.flags = flags # set up MQTT client connection to broker self.client = mqtt.Client("KNXBridgeDaemon-"+name) self.client.on_message = self.updateReceived # authenticate if user and passwd: self.client.username_pw_set(username=user, password=passwd) elif user: self.client.username_pw_set(username=user) # establish connection if host and port: self.client.connect(host=host, port=port) else: self.client.connect(host=host) # listener target/endpoint try: self.client.subscribe(topic) except ValueError as ex: log('error', 'Could not connect to MQTT server {0} for endpoint {1} [{2}]'.format(host, topic, ex))
def writeKNXAttribute(self, attrName, knxDest, knxFormat, val, function=None, flags=None) -> bool: """ adds additional reachable check for client """ ret = None if self.reachable: ret = super().writeKNXAttribute(attrName, knxDest, knxFormat, val, function, flags) else: log( 'error', 'Could not connect to ZigBee client {0}[{1}]'.format( attrName, self.uniqueID)) return ret
def valueToData(self, value): """ customize original implementation by returning hex-ified and chunked value representation for direct usage in cmd :returns: 4 typles á 00-FF hex representation separated by spaces :raises: NotImplementedError in case value is not of type int or float """ ret = self.__dptimpl.valueToData(value) if not isinstance(ret, (int, float)): log( 'error', "KNXUtil.valueToData() - Data type {0} not yet implemented". format(type(value))) raise NotImplementedError ret = hex(int(ret)) # fill up with leading 0's if hex not filling corresponding byte representation if len(ret[2:]) < (2 * self.typeSize): for i in range(0, (2 * self.typeSize) - len(ret[2:])): ret = ret[:2] + '0' + ret[2:] # print byte-wise representation separated by blanks return ' '.join(ret[i:i + 2] for i in range(2, len(ret), 2))
def readKNXAttribute(self, attrName, knxSrc, knxFormat, function=None): """ reads values via EIB/KNX client :returns pythonic value from bus """ val = None dpt = EIBClientFactory().getClient().GroupCache_Read(knxSrc) # convert value from string representation into hex dpt = int(dpt, 16) # get DPT implementation for python type conversion dc = DPTXlatorFactoryFacade().create(knxFormat) try: if dc.checkData(dpt): val = dc.dataToValue(dpt) log( 'info', f'Value retrieved (group cache) "{attrName}"[{knxSrc}] value={val}({dpt})' ) except DPTXlatorValueError as ex: # log failure log( 'error', f'Value could not be read "{attrName}"[{knxSrc}] value={dpt} - Check type definition for DPT type "{knxFormat}" and value "{dpt}" - {ex}' ) except (TypeError, ValueError) as ex: # log failure log( 'error', f'Value could not be read "{attrName}"[{knxSrc}] value={dpt} - {ex}' ) return val
def writeKNXAttribute(self, attrName: str, knxDest: str, knxFormat: str, val, function=None, flags=None) -> bool: """ writes values via the KNXD command line tool :returns true if successful """ dpt = None # get DPT implementation dc = DPTXlatorFactoryFacade().create(knxFormat) # perform transformations if defined before sending to bus if function: val = self.performFunction(dc.dpt, function, val, attrName, knxDest, knxFormat) # check value, some functions like an exclusive equal comparison may return None # for valid reason with no further write action to be performed if val is None: return False # convert to DPT representation try: if dc.checkValue(val): dpt = dc.valueToData(val) except DPTXlatorValueError: # log failure log( 'error', f'Value could not be updated "{attrName}"[{knxDest}] value={val} - Check type definition for DPT type "{knxFormat}" and value "{val}"' ) except (TypeError, ValueError) as ex: # log failure log( 'error', f'Value could not be updated "{attrName}"[{knxDest}] value={val} - {ex}' ) if dpt is None: return False # do not load the bus with unnecessary request, check against cached value if (flags and Flags.FLAGS_FORCE in flags) or \ not self.isCurrentKNXAttribute(knxDest, knxFormat, dpt): # send value to the knx bus os.popen('knxtool groupwrite ip:{0} {1} {2}'.format( KNXGateway().hostIP, knxDest, dpt)) # log success if flags and Flags.FLAGS_FORCE in flags: log( 'change', f'Updated value (enforced) on KNX bus "{attrName}"[{knxDest}] value={val}[DPT:{dpt}]' ) else: log( 'change', f'Updated value on KNX bus "{attrName}"[{knxDest}] value={val}[DPT:{dpt}]' ) else: # log success log('info', f'Value is up to date "{attrName}"[{knxDest}] value={val}') return True
def __executeFunctionImpl(deviceInstance, dpt, function, val, attrName, knxDest, knxFormat): errDetail = None if function[:3] == 'val': # replace current value by static value try: val = function[4:-1] if is_number(val): val = float(val) elif is_bool(val): val = convert_bool(val) except ValueError: val = function[4:-1] elif function[:3] == 'inv': # invert current value - restricted to boolean currently if is_bool(val): # generic 0/1 representation required for dpxlator DPT conversion val = not val else: errDetail = 'wrong value type' elif function[:3] == 'max': # returns the greater value, useful for greater 0 assurancce if is_number(val): try: val = convert_number(val) val = max(val, function[4:-1]) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'min': # returns the lower value if is_number(val): try: val = convert_number(val) val = min(val, function[4:-1]) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'rnd': # rounds the current value to the given precision if is_number(val): try: val = convert_number(val) val = round(val, function[4:-1]) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'div': # divide current value by given value if is_number(val): try: val = convert_number(val) div = float(function[4:-1]) if div is not 0: val = val / div else: if float(function[4:-1]) == 0: errDetail = 'divider is zero' else: errDetail = 'wrong function definition' except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'mul': # multiply current value with given value if is_number(val): try: val = convert_number(val) div = float(function[4:-1]) val = val * div except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'add': # adds given value to current value if is_number(val): try: val = convert_number(val) div = float(function[4:-1]) val = val + div except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:3] == 'sub': # substracts given value from current value if is_number(val): gv = function[4:-1] # check for live KNX value if gv[:1] == '/': deviceInstance.readKNXAttribute("functions live value", gv, dpt) try: val = convert_number(val) val = val - float(gv) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:2] == 'lt': # checks if current value is less then given value if is_number(val): try: val = float(val) < float(function[3:-1]) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:2] == 'gt': # checks if current value is greater then given value if is_number(val): try: val = float(val) > float(function[3:-1]) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:6] == 'eqExcl': # checks if current value matches given value # in case of string values simply put the pattern into brackets without further escaping # example: eqExcl("Check against this string") # return only True if matching, otherwise None for no further processing if is_number(val): try: if float(val) == float(function[7:-1]): val = True else: val = None except ValueError: errDetail = 'wrong function definition' elif is_bool(val): try: if convert_bool(val) == convert_bool(function[7:-1]): val = True else: val = None except ValueError: errDetail = 'wrong function definition' elif isinstance(val, str): try: if str(val) == str(function[7:-1]): val = True else: val = None except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:2] == 'eq': # checks if current value matches given value, return either true or false if is_number(val): try: val = (float(val) == float(function[3:-1])) except ValueError: errDetail = 'wrong function definition' elif is_bool(val): try: val = (convert_bool(val) == convert_bool(function[3:-1])) except ValueError: errDetail = 'wrong function definition' elif isinstance(val, str): try: val = (str(val) == str(function[3:-1])) except ValueError: errDetail = 'wrong function definition' else: errDetail = 'wrong value type' elif function[:9] == 'timedelta': # checks delta in seconds between now and given date # :returns: true if delta is outside defined delta in seconds try: errDetail = 'wrong function definition' delta = abs(int(function[12:-1])) errDetail = 'wrong value type (date cannot be parsed)' # retrieve both date value and set them to UTC for comparison timenow = datetime.now(timezone.utc) clienttime = dateparser.parse(val) if function[9:11] == 'LT': val = timedelta(seconds=delta) > abs(timenow - clienttime) elif function[9:11] == 'GT': val = timedelta(seconds=delta) < abs(timenow - clienttime) errDetail = None except ValueError as ex: errDetail += str(ex) elif function[:7] == 'timechg': # adds/deducts the defined delta in seconds to given date # :returns: the new time with the time in seconds added/deducted try: errDetail = 'wrong function definition' delta = int(function[8:-1]) errDetail = 'wrong value type (no date)' # calculate time with delta and convert it to original value type val = type(val)(dateparser.parse(val) + timedelta(seconds=delta)) errDetail = None except ValueError: pass elif function[:6] == 'asynch': # asynchronous method call with not interfere with current execution # but it will start another thread after defined duration with defined value as function # syntax: asynch(<duration in sec> <function call>) # important: use blank as separator not comma or semicolon! # examples: asynch(60 val(true)) try: tok = re.split("[ ]", function[7:-1]) duration = tok[0] func = tok[1] asynchVal = executeFunction(deviceInstance, dpt, func, val, attrName, knxDest, knxFormat) Timer(int(duration), _asynchWrite, kwargs={ "deviceInstance": deviceInstance, "attrName": attrName, "knxDest": knxDest, "knxFormat": knxFormat, "val": asynchVal }).start() except Exception as e: errDetail = 'Could not start asynchronous function - ' + str(e) if errDetail: log( 'error', 'Could not apply function "{0}" to value {1} - {2}'.format( function, val, errDetail)) return val
def update(self, freq): global UPDATEFREQ ##### initialization of clients ##### # ModBus clients will be implicitely update as part of the getAttribute call # initialize ZigBee Gateway with latest client state if ZigBeeGateway(): ZigBeeGateway().getState() # iterate list of attributes to be updated for attr in self.attrs: # first time initialization steps if UPDATEFREQ['initial'] & freq == UPDATEFREQ['initial']: # setup knx-based event trigger based on EIB/KNX client listener # ModBus - currently not implemented if attr['type'] == 'knx2modbus': raise NotImplementedError # set up ZigBee listener elif attr['type'] == 'knx2zigbee': # find corresponding ZigBee device if attr['zigbeeApplID'] in self.zigbeeClients: client = self.zigbeeClients[attr['zigbeeApplID']] client.installListener( attr['name'], attr['knxAddr'], attr['knxFormat'], attr['zigbeeAttr'], attr['zigbeeFormat'], getAttrSafe(attr, 'zigbeeSection')) elif attr['type'] == 'knx2knx': # knx2knx devices require dedicated handling only as part of the registration for KNX bus monitoring # create client here without keeping handle to the instance client = KNX2KNXClient() client.installListener(attr['name'], attr['knxAddr'], attr['knxFormat'], attr['knxDest'], getAttrSafe(attr, 'function')) elif attr['type'] == 'mqtt2knx': # mqtt client defines its own thread which permanently listens to update events # avoid registering to targets which flood your KNX bus due to high frequency of update if attr['mqttApplID'] in self.mqttAppliances: appliance = self.mqttAppliances[attr['mqttApplID']] appliance.setupClient(attr['name'], attr['mqttTopic'], attr['knxAddr'], attr['knxFormat'], getAttrSafe(attr, 'function'), getAttrSafe(attr, 'flags')) # check attribute update frequency matches current thread definition if 'updFreq' in attr and UPDATEFREQ[attr['updFreq']] & freq > 0: client = None newVal = None # check update type - currently only modbus read, knx write is supported if attr['type'] == 'modbus2knx': # find corresponding ModBus device if attr['modbusApplID'] in self.modbusClients: client = self.modbusClients[attr['modbusApplID']] # get latest ModBus value for attribute newVal = client.getAttribute(attr['name'], attr['modbusFormat'], attr['modbusAddrDec']) else: log( 'error', 'Configuration error - modbusApplID({0}) not defined' .format(attr['zigbeeApplID'])) # handle ZigBee attributes elif attr['type'] == 'zigbee2knx': # find corresponding ZigBee device if attr['zigbeeApplID'] in self.zigbeeClients: client = self.zigbeeClients[attr['zigbeeApplID']] # get latest ZigBee value for attribute newVal = client.getAttribute( attr['name'], attr['zigbeeFormat'], attr['zigbeeAttr'], getAttrSafe(attr, 'zigbeeSection')) else: log( 'error', 'Configuration error - zigbeeApplID({0}) not defined' .format(attr['zigbeeApplID'])) # write value to bus if client is not None and newVal is not None: client.writeKNXAttribute(attr['name'], attr['knxAddr'], attr['knxFormat'], newVal, getAttrSafe(attr, 'function'), getAttrSafe(attr, 'flags')) # run periodically update of values - each update frequency initiating its own thread # initial run by main will initiate all threads at once ut = None if freq & UPDATEFREQ['critical'] > 0: # CRITICAL - runs every 3 seconds - ONLY USE IN EXCEPTIONABLE CASES!! ut = Timer(3, gateway.update, (UPDATEFREQ['critical'], )) ut.start() if freq & UPDATEFREQ['very high'] > 0: # VERY HIGH - runs every 10 seconds ut = Timer(10, gateway.update, (UPDATEFREQ['very high'], )) ut.start() if freq & UPDATEFREQ['high'] > 0: # HIGH - runs every 60 seconds Timer(60, gateway.update, (UPDATEFREQ['high'], )).start() if freq & UPDATEFREQ['medium'] > 0: # MEDIUM - runs every 10 minutes Timer(600, gateway.update, (UPDATEFREQ['medium'], )).start() if freq & UPDATEFREQ['very low'] > 0: # LOW - runs every 60 minutes Timer(3600, gateway.update, (UPDATEFREQ['very low'], )).start() if freq & UPDATEFREQ['low'] > 0: # LOW - runs every 24 hours Timer(86400, gateway.update, (UPDATEFREQ['low'], )).start() # return reference to very high thread for synchronization return ut