diff --git a/database/__init__.py b/database/__init__.py index 581e336c2..1a02a6475 100755 --- a/database/__init__.py +++ b/database/__init__.py @@ -398,16 +398,16 @@ def copy_databasefile(self): database_name = '' # copy the database file - self.logger.warning( f"Starting to copy SQLite3 database file from {database_name} to {self._copy_database_name}") + self.logger.info( f"Starting to copy SQLite3 database file from {database_name} to {self._copy_database_name}") import shutil try: shutil.copy2(database_name, self._copy_database_name) - self.logger.warning("Finished copying SQLite3 database file") + self.logger.info("Finished copying SQLite3 database file") except Exception as e: self.logger.error( f"Error copying SQLite3 database file: {e}") - param_dict = {"copy_database": False} - self.update_config_section(param_dict) + #param_dict = {"copy_database": False} + #self.update_config_section(param_dict) return diff --git a/db_addon/__init__.py b/db_addon/__init__.py index 8a1b7865b..ebdb2d76e 100644 --- a/db_addon/__init__.py +++ b/db_addon/__init__.py @@ -95,7 +95,6 @@ def __init__(self, sh): self.item_attribute_search_str = 'database' # attribute, on which an item configured for database can be identified self.last_connect_time = 0 # mechanism for limiting db connection requests self.alive = None # Is plugin alive? - self.suspended = False # Is plugin activity suspended self.active_queue_item: str = '-' # String holding item path of currently executed item self.onchange_delay_time = 30 # delay time in seconds between change of database item start of reevaluation of db_addon item self.database_item_list = [] # list of needed database items @@ -136,19 +135,19 @@ def run(self): # check existence of db-plugin, get parameters, and init connection to db if not self._check_db_existence(): self.logger.error(f"Check of existence of database plugin incl connection check failed. Plugin not loaded") - return self.deinit() + return # create db object self._db = lib.db.Database("DatabaseAddOn", self.db_driver, self.connection_data) if not self._db.api_initialized: self.logger.error("Initialization of database API failed") - return self.deinit() + return self.logger.debug("Initialization of database API successful") # check initialization of db if not self._initialize_db(): self.logger.error("Connection to database failed") - return self.deinit() + return self._db.close() # check db connection settings @@ -717,11 +716,8 @@ def update_item(self, item, caller=None, source=None, dest=None): if self.alive and caller != self.get_shortname(): # handle database items if item in self._database_items(): - if self.suspended: - self.logger.info(f"Plugin is suspended. No updated will be processed.") - else: - self.logger.debug(f" Updated Item {item.property.path} with value {item()} will be put to queue in approx. {self.onchange_delay_time}s resp. after startup.") - self.update_item_delay_deque.append([item, item(), int(time.time() + self.onchange_delay_time)]) + self.logger.debug(f" Updated Item {item.property.path} with value {item()} will be put to queue in approx. {self.onchange_delay_time}s resp. after startup.") + self.update_item_delay_deque.append([item, item(), int(time.time() + self.onchange_delay_time)]) # handle admin items elif self.has_iattr(item.conf, 'db_addon_admin'): @@ -923,10 +919,6 @@ def _create_due_items() -> list: if self.debug_log.execute: self.logger.debug(f"execute_items called with {option=}") - if self.suspended: - self.logger.info(f"Plugin is suspended. No items will be calculated.") - return - suspended_items = self._suspended_items() if len(suspended_items) > 0: self.logger.info(f"{len(suspended_items)} are suspended and will not be calculated.") @@ -1514,28 +1506,6 @@ def fetch_raw(self, query: str, params: dict = None) -> Union[list, None]: return self._fetchall(query, params) - def suspend(self, state: bool = False) -> bool: - """ - Will pause value evaluation of plugin - - """ - - if state: - self.logger.info("Plugin is set to 'suspended'. Queries to database will not be made until suspension is cleared.") - self.suspended = True - self._clear_queue() - else: - self.logger.info("Plugin suspension cleared. Queries to database will be resumed.") - self.suspended = False - - # write back value to item, if one exists - for item in self.get_item_list('db_addon', 'admin'): - item_config = self.get_item_config(item) - if item_config['db_addon_fct'] == 'suspend': - item(self.suspended, self.get_shortname()) - - return self.suspended - ############################################## # Calculation methods / Using Item Object ############################################## diff --git a/modbus_tcp/__init__.py b/modbus_tcp/__init__.py index bc75596c2..90559c425 100755 --- a/modbus_tcp/__init__.py +++ b/modbus_tcp/__init__.py @@ -47,13 +47,14 @@ AttrObjectType = 'modBusObjectType' AttrDirection = 'modBusDirection' + class modbus_tcp(SmartPlugin): """ This class provides a Plugin for SmarthomeNG to read and or write to modbus devices. """ - PLUGIN_VERSION = '1.0.12' + PLUGIN_VERSION = '1.0.13' def __init__(self, sh, *args, **kwargs): """ @@ -85,9 +86,11 @@ def __init__(self, sh, *args, **kwargs): if not (self._cycle or self._crontab): self.logger.error(f"{self.get_fullname()}: no update cycle or crontab set. Modbus will not be queried automatically") - self._slaveUnit = int(self.get_parameter_value('slaveUnit')) + self._slaveUnit = self.get_parameter_value('slaveUnit') self._slaveUnitRegisterDependend = False + self._pause_item_path = self.get_parameter_value('pause_item') + self._sh = sh self._regToRead = {} self._regToWrite = {} @@ -99,8 +102,6 @@ def __init__(self, sh, *args, **kwargs): self.init_webinterface(WebInterface) - return - def run(self): """ Run method for the plugin @@ -108,67 +109,25 @@ def run(self): self.logger.debug(f"Plugin '{self.get_fullname()}': run method called") if self.alive: return + self.alive = True - self.set_suspend(by='run()') - + if self._cycle or self._crontab: - self.error_count = 0 # Initialize error count - if not self.suspended: - self._create_cyclic_scheduler() + self.error_count = 0 # Initialize error count + self.scheduler_add('poll_device_' + self._host, self.poll_device, cycle=self._cycle, cron=self._crontab, prio=5) self.logger.debug(f"Plugin '{self.get_fullname()}': run method finished ") - def _create_cyclic_scheduler(self): - self.scheduler_add('poll_device_' + self._host, self.poll_device, cycle=self._cycle, cron=self._crontab, prio=5) - - def _remove_cyclic_scheduler(self): - self.scheduler_remove('poll_device_' + self._host) - def stop(self): """ Stop method for the plugin """ self.alive = False self.logger.debug(f"Plugin '{self.get_fullname()}': stop method called") - self._remove_cyclic_scheduler() + self.scheduler_remove('poll_device_' + self._host) self._Mclient.close() self.connected = False self.logger.debug(f"Plugin '{self.get_fullname()}': stop method finished") - # sh.plugins.return_plugin('pluginName').suspend() - def set_suspend(self, suspend_active=None, by=None): - """ - enable / disable suspend mode: open/close connections, schedulers - """ - - if suspend_active is None: - if self._suspend_item is not None: - # if no parameter set, try to use item setting - suspend_active = bool(self._suspend_item()) - else: - # if not available, default to "resume" (non-breaking default) - suspend_active = False - - # print debug logging - if suspend_active: - msg = 'Suspend mode enabled' - else: - msg = 'Suspend mode disabled' - if by: - msg += f' (set by {by})' - self.logger.debug(msg) - - # activate selected mode, use smartplugin methods - if suspend_active: - self.suspend(by) - else: - self.resume(by) - - if suspend_active: - self._remove_cyclic_scheduler() - else: - self._create_cyclic_scheduler() - - def parse_item(self, item): """ Default plugin parse_item method. Is called when the plugin is initialized. @@ -178,10 +137,10 @@ def parse_item(self, item): :param item: The item to process. """ - # check for suspend item - if item.property.path == self._suspend_item_path: - self.logger.debug(f'suspend item {item.property.path} registered') - self._suspend_item = item + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item self.add_item(item, updating=True) return self.update_item @@ -207,7 +166,7 @@ def parse_item(self, item): if self.has_iattr(item.conf, AttrObjectType): objectType = self.get_iattr_value(item.conf, AttrObjectType) - reg = str(objectType) # dictionary key: objectType.regAddr.slaveUnit // HoldingRegister.528.1 + reg = str(objectType) # dictionary key: objectType.regAddr.slaveUnit // HoldingRegister.528.1 reg += '.' reg += str(regAddr) reg += '.' @@ -274,7 +233,7 @@ def poll_device(self): changes on it's own, but has to be polled to get the actual status. It is called by the scheduler which is set within run() method. """ - if self.suspended: + if not self.alive: return with self.lock: @@ -302,7 +261,6 @@ def poll_device(self): try: for reg, regPara in self._regToRead.items(): with self.lock: - regAddr = regPara['regAddr'] value = self.__read_Registers(regPara) # self.logger.debug(f"value read: {value} type: {type(value)}") if value is not None: @@ -330,8 +288,6 @@ def poll_device(self): except Exception as e: self.logger.error(f"something went wrong in the poll_device function: {e}") - - # called each time an item changes. def update_item(self, item, caller=None, source=None, dest=None): """ Item has been updated @@ -349,21 +305,16 @@ def update_item(self, item, caller=None, source=None, dest=None): slaveUnit = self._slaveUnit dataDirection = 'read' - # check for suspend item - if item is self._suspend_item: + # check for pause item + if item is self._pause_item: if caller != self.get_shortname(): - self.logger.debug(f'Suspend item changed to {item()}') - self.set_suspend(item(), by=f'suspend item {item.property.path}') + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() return - if self.suspended: - if self.suspend_log_update is None or self.suspend_log_update is False: # debug - Nachricht nur 1x ausgeben - self.logger.info('Plugin is suspended, data will not be written') - self.suspend_log_update = True - return - else: - self.suspend_log_update = False - if caller == self.get_fullname(): # self.logger.debug(f'item was changed by the plugin itself - caller:{caller} source:{source} dest:{dest}') return @@ -389,7 +340,7 @@ def update_item(self, item, caller=None, source=None, dest=None): # else: # self.logger.debug(f'update_item:{item} default modBusObjectTyp: {objectType}') - reg = str(objectType) # Dict-key: HoldingRegister.528.1 *** objectType.regAddr.slaveUnit *** + reg = str(objectType) # Dict-key: HoldingRegister.528.1 *** objectType.regAddr.slaveUnit *** reg += '.' reg += str(regAddr) reg += '.' @@ -417,8 +368,6 @@ def update_item(self, item, caller=None, source=None, dest=None): self.connected = False return - startTime = datetime.now() - regCount = 0 try: self.__write_Registers(regPara, item()) except Exception as e: @@ -431,19 +380,13 @@ def __write_Registers(self, regPara, value): bo = regPara['byteOrder'] wo = regPara['wordOrder'] dataTypeStr = regPara['dataType'] - dataType = ''.join(filter(str.isalpha, dataTypeStr)) # vom dataType die Ziffen entfernen z.B. uint16 = uint - registerCount = 0 # Anzahl der zu schreibenden Register (Words) + dataType = ''.join(filter(str.isalpha, dataTypeStr)) # vom dataType die Ziffen entfernen z.B. uint16 = uint try: - bits = int(''.join(filter(str.isdigit, dataTypeStr))) # bit-Zahl aus aus dataType z.B. uint16 = 16 + bits = int(''.join(filter(str.isdigit, dataTypeStr))) # bit-Zahl aus aus dataType z.B. uint16 = 16 except: bits = 16 - if dataType.lower() == 'string': - registerCount = int(bits / 2) # bei string: bits = bytes !! string16 -> 16Byte - 8 registerCount - else: - registerCount = int(bits / 16) - if regPara['factor'] != 1: # self.logger.debug(f"value {value} divided by: {regPara['factor']}") value = value * (1 / regPara['factor']) @@ -480,11 +423,11 @@ def __write_Registers(self, regPara, value): builder.add_string(value) elif dataType.lower() == 'bit': if objectType == 'Coil' or objectType == 'DiscreteInput': - if not isinstance(value, bool): # test is boolean + if not isinstance(value, bool): # test is boolean self.logger.error(f"Value is not boolean: {value}") return else: - if set(value).issubset({'0', '1'}) and bool(value): # test is bit-string '00110101' + if set(value).issubset({'0', '1'}) and bool(value): # test is bit-string '00110101' builder.add_bits(value) else: self.logger.error(f"Value is not a bitstring: {value}") @@ -541,13 +484,13 @@ def __read_Registers(self, regPara): bits = 16 if dataType.lower() == 'string': - registerCount = int(bits / 2) # bei string: bits = bytes !! string16 -> 16Byte - 8 registerCount + registerCount = int(bits / 2) # bei string: bits = bytes !! string16 -> 16Byte - 8 registerCount else: registerCount = int(bits / 16) - if self.connected == False: + if not self.connected: self.logger.error(f"not connected to {self._host}:{self._port}") - return None + return # self.logger.debug(f"read {objectType}.{address}.{slaveUnit} (address.slaveUnit) regCount:{registerCount}") if objectType == 'Coil': @@ -560,11 +503,11 @@ def __read_Registers(self, regPara): result = self._Mclient.read_holding_registers(address, registerCount, slave=slaveUnit) else: self.logger.error(f"{AttrObjectType} not supported: {objectType}") - return None + return if result.isError(): self.logger.error(f"read error: {result} {objectType}.{address}.{slaveUnit} (address.slaveUnit) regCount:{registerCount}") - return None + return if objectType == 'Coil': value = result.bits[0] @@ -615,4 +558,3 @@ def __read_Registers(self, regPara): return decoder.decode_bits() else: self.logger.error(f"Number of bits or datatype not supported : {dataTypeStr}") - return None diff --git a/modbus_tcp/plugin.yaml b/modbus_tcp/plugin.yaml index e78b1e7cd..c3e3d8c0a 100755 --- a/modbus_tcp/plugin.yaml +++ b/modbus_tcp/plugin.yaml @@ -11,13 +11,13 @@ plugin: keywords: modbus_tcp modbus smartmeter inverter heatpump #documentation: http://smarthomeng.de/user/plugins/modbus_tcp/user_doc.html support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1154368-einbindung-von-modbus-tcp - version: 1.0.12 # Plugin version - sh_minversion: '1.8' # minimum shNG version to use this plugin + version: 1.0.13 # Plugin version + sh_minversion: '1.10' # minimum shNG version to use this plugin #sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: '3.6' # py_maxversion: # maximum Python version to use for this plugin (leave empty if latest) - multi_instance: True # plugin supports multi instance - restartable: unknown + multi_instance: true # plugin supports multi instance + restartable: true classname: modbus_tcp # class containing the plugin parameters: @@ -26,6 +26,7 @@ parameters: description: de: 'IP Adresse des Modbus-Geraetes' en: 'IP address from the modbus-device' + mandatory: true port: type: int @@ -34,6 +35,7 @@ parameters: description: de: 'modbus Port' en: 'modbus port' + mandatory: true cycle: type: int @@ -41,7 +43,7 @@ parameters: valid_min: 0 description: de: 'Update Zyklus in Sekunden. Wenn der Wert 0 ist, wird keine Abfrage über cycle ausgeführt' - en: 'Update cycle in seconds. If value is 0 then noch query will be made by means of cycle' + en: 'Update cycle in seconds. If value is 0 then no query will be made by means of cycle' crontab: type: str @@ -50,12 +52,20 @@ parameters: en: 'Update by means of a crontab' slaveUnit: - type: num + type: int default: 1 description: de: 'Slave-Addresse der zu lesenden Modbus-Einheit' en: 'slave-address of the Modbus-Unit to read' + pause_item: + type: str + default: '' + description: + de: 'Item, um die Ausführung des Plugins zu steuern' + en: 'item for controlling plugin execution' + + item_attributes: modBusObjectType: type: str diff --git a/russound/__init__.py b/russound/__init__.py index ad6a07379..1d4520f10 100755 --- a/russound/__init__.py +++ b/russound/__init__.py @@ -42,7 +42,7 @@ class Russound(SmartPlugin): the update functions for the items """ - PLUGIN_VERSION = '1.7.2' + PLUGIN_VERSION = '1.7.3' def __init__(self, sh, *args, **kwargs): """ @@ -52,16 +52,10 @@ def __init__(self, sh, *args, **kwargs): if '.'.join(VERSION.split('.', 2)[:2]) <= '1.5': self.logger = logging.getLogger(__name__) - super().__init__(sh, args, kwargs) - try: - # sh = self.get_sh() to get it. - self.host = self.get_parameter_value('host') - self.port = self.get_parameter_value('port') - except KeyError as e: - self.logger.critical( - "Plugin '{}': Inconsistent plugin (invalid metadata definition: {} not defined)".format(self.get_shortname(), e)) - self._init_complete = False - return + super().__init__() + self.host = self.get_parameter_value('host') + self.port = self.get_parameter_value('port') + self._pause_item_path = self.get_parameter_value('pause_item') # Initialization code goes here self.terminator = RESP_DELIMITER @@ -69,10 +63,8 @@ def __init__(self, sh, *args, **kwargs): self._client.set_callbacks(data_received=self.found_terminator) self.params = {} self.sources = {} - self.suspended = False - + self.init_webinterface() - return def run(self): """ @@ -80,21 +72,30 @@ def run(self): """ self.logger.debug("Run method called") if not self._client.connect(): - self.logger.debug(f'Connection to {self.host}:{self.port} not possible. Plugin deactivated.') + self.logger.debug(f'Connection to {self.host}:{self.port} not possible. Plugin stopped.') + self.stop() return + self.alive = True + if self._pause_item: + self._pause_item(False, self.get_fullname()) def activate(self): - self.logger.debug("Activate method called, queries to russound will be resumes and data will be written again") - self.resume() - + self.logger.debug("Activate method called, but is deprecated. Please move to run()") + self.run() + def stop(self): """ Stop method for the plugin """ self.logger.debug("Stop method called") self.alive = False - self._client.close() + if self._pause_item: + self._pause_item(True, self.get_fullname()) + try: + self._client.close() + except Exception: + pass def connect(self): self._client.open() @@ -121,10 +122,10 @@ def parse_item(self, item): # self.logger.debug("Source {0} added".format(s)) # return None - if item.property.path == self._suspend_item_path: - self._suspend_item = item - self.logger.info(f'set suspend_item to {item.property.path}') - return + if item.property.path == self._pause_item_path: + self._pause_item = item + self.logger.info(f'set pause_item to {item.property.path}') + return self.update_item if self.has_iattr(item.conf, 'rus_path'): self.logger.debug("parse item: {}".format(item)) @@ -177,9 +178,6 @@ def parse_item(self, item): return self.update_item - def parse_logic(self, logic): - pass - def _restrict(self, val, minval, maxval): if val < minval: return minval @@ -200,19 +198,21 @@ def update_item(self, item, caller=None, source=None, dest=None): :param source: if given it represents the source :param dest: if given it represents the dest """ + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + if self.alive and caller != self.get_shortname(): # code to execute if the plugin is not stopped # and only, if the item has not been changed by this this plugin: self.logger.info("Update item: {}, item has been changed outside this plugin (caller={}, source={}, dest={})".format(item.property.path, caller, source, dest)) - if item.property.path == self._suspend_item_path: - if self._suspend_item is not None: - if item(): - self.suspend(f'suspend item {item.property.path}') - else: - self.resume(f'suspend item {item.property.path}') - return - if self.has_iattr(item.conf, 'rus_path'): path = self.get_iattr_value(item.conf, 'rus_path') p = self.params[path] @@ -282,18 +282,11 @@ def _send_cmd(self, cmd): if not self.alive: self.logger.error('Trying to send data but plugin is not running') return - - if self.suspended: - self.logger.debug('Plugin is suspended, data will not be written') - return self.logger.debug("Sending request: {0}".format(cmd)) # if connection is closed we don't wait for sh.con to reopen it # instead we reconnect immediatly -# - # if not self.connected: - # self.connect() if not self._client.connected: self._client.connect() diff --git a/russound/plugin.yaml b/russound/plugin.yaml index a10bc0484..673a04b41 100755 --- a/russound/plugin.yaml +++ b/russound/plugin.yaml @@ -12,8 +12,8 @@ plugin: documentation: https://www.smarthomeng.de/developer/plugins/russound/user_doc.html # url of documentation (wiki) page support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1800440-support-thread-für-das-russound-plugin - version: 1.7.2 # Plugin version - sh_minversion: '1.9.0' # minimum shNG version to use this plugin + version: 1.7.3 # Plugin version + sh_minversion: '1.10.0' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) multi_instance: false # plugin supports multi instance restartable: true @@ -36,12 +36,12 @@ parameters: en: 'Russound port' fr: "Port de Russound" - standby_item: + pause_item: type: str default: '' description: - de: 'Item zum Aktivieren des Suspend-Modus' - en: 'item for activating suspend mode' + de: 'Item zum Anhalten/Fortsetzen des Plugins' + en: 'item for stopping/starting the plugin' item_attributes: rus_path: diff --git a/smartvisu/__init__.py b/smartvisu/__init__.py index c89069c3b..732eca49d 100755 --- a/smartvisu/__init__.py +++ b/smartvisu/__init__.py @@ -46,7 +46,7 @@ class SmartVisu(SmartPlugin): - PLUGIN_VERSION="1.8.14" + PLUGIN_VERSION="1.8.15" ALLOW_MULTIINSTANCE = True visu_definition = None diff --git a/smartvisu/plugin.yaml b/smartvisu/plugin.yaml index 2b0fbb9fa..bd6878872 100755 --- a/smartvisu/plugin.yaml +++ b/smartvisu/plugin.yaml @@ -12,7 +12,7 @@ plugin: #documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1586800-support-thread-für-das-smartvisu-plugin - version: 1.8.14 # Plugin version + version: 1.8.15 # Plugin version sh_minversion: '1.9.3.5' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: '3.6' # minimum Python version to use for this plugin diff --git a/smartvisu/webif/__init__.py b/smartvisu/webif/__init__.py index 01c20f72c..798bf4303 100755 --- a/smartvisu/webif/__init__.py +++ b/smartvisu/webif/__init__.py @@ -105,6 +105,12 @@ def index(self, reload=None): client['hostname'] = clientinfo.get('hostname', '') client['browser'] = clientinfo.get('browser', '') client['browserversion'] = clientinfo.get('browserversion', '') + + client['osname'] = clientinfo.get('os_name', '') + client['osversion'] = clientinfo.get('os_vers', '') + client['osversionname'] = clientinfo.get('os_vname', '') + client['platformtype'] = clientinfo.get('pl_type', '') + client['platformvendor'] = clientinfo.get('pl_vendor', '') clients.append(client) clients_sorted = sorted(clients, key=lambda k: k['name']) @@ -168,6 +174,12 @@ def get_data_html(self, dataSet=None): value_dict['hostname'] = clientinfo.get('hostname', '') value_dict['browser'] = clientinfo.get('browser', '') value_dict['browserversion'] = clientinfo.get('browserversion', '') + + value_dict['osname'] = clientinfo.get('os_name', '') + value_dict['osversion'] = clientinfo.get('os_vers', '') + value_dict['osversionname'] = clientinfo.get('os_vname', '') + value_dict['platformtype'] = clientinfo.get('pl_type', '') + value_dict['platformvendor'] = clientinfo.get('pl_vendor', '') client_list.append(value_dict) plglogics = [] diff --git a/smartvisu/webif/templates/index.html b/smartvisu/webif/templates/index.html index ba957b0df..801f371a6 100755 --- a/smartvisu/webif/templates/index.html +++ b/smartvisu/webif/templates/index.html @@ -203,6 +203,14 @@ let hostname = clients[client]['hostname']; let browser = clients[client]['browser']; let browserversion = clients[client]['browserversion']; + + let osname = clients[client]['osname']; + let osversion = clients[client]['osversion']; + let osversionname = clients[client]['osversionname']; + let platformtype = clients[client]['platformtype']; + let platformvendor = clients[client]['platformvendor']; + + if (hostname === '') hostname = platformvendor + ' ' + platformtype + ' (' + osname + ' ' + osversion + ')' //let newRow = clienttable.row.add( [ null, name, ip, port, protocol, sw + ' ' + swversion, browser + ' ' + browserversion, hostname] ); clientdata.push([ null, name, ip, port, protocol, sw + ' ' + swversion, browser + ' ' + browserversion, hostname]); } diff --git a/stateengine/StateEngineAction.py b/stateengine/StateEngineAction.py index 36497db3e..d115c289c 100755 --- a/stateengine/StateEngineAction.py +++ b/stateengine/StateEngineAction.py @@ -594,6 +594,7 @@ class SeActionSetItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__eval_item = None self.__status = None self.__delta = 0 self.__value = StateEngineValue.SeValue(self._abitem, "value") @@ -606,6 +607,7 @@ def __repr__(self): def _getitem_fromeval(self): if self.__item is None: return + self.__eval_item = self.__item self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, self.__mindelta) if self.__item is None: @@ -643,8 +645,6 @@ def write_to_logger(self): self._log_debug("item: {0}", self.__item.property.path) else: self._log_debug("item is not defined! Check log file.") - if self.__status is not None: - self._log_debug("status: {0}", self.__status.property.path) self.__mindelta.write_to_logger() self.__value.write_to_logger() @@ -710,6 +710,7 @@ def _execute_set_add_remove(self, state, actionname, namevar, repeat_text, item, self.update_webif_actionstatus(state, re.findall(pat, actionname)[0], 'True') # noinspection PyCallingNonCallable item(value, caller=self._caller, source=source) + self.__item = self.__eval_item def get(self): orig_item = self.__item @@ -733,6 +734,7 @@ def get(self): value = None except Exception: value = None + self.__item = orig_item mindelta = self.__mindelta.get() if mindelta is None: result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, @@ -979,6 +981,7 @@ class SeActionForceItem(SeActionBase): def __init__(self, abitem, name: str): super().__init__(abitem, name) self.__item = None + self.__eval_item = None self.__status = None self.__value = StateEngineValue.SeValue(self._abitem, "value") self.__mindelta = StateEngineValue.SeValue(self._abitem, "mindelta") @@ -1045,6 +1048,7 @@ def _can_execute(self, state): def _getitem_fromeval(self): if self.__item is None: return + self.__eval_item = self.__item self.__item, self.__value, self.__mindelta, _issue = self.check_getitem_fromeval(self.__item, self.__value, self.__mindelta) if self.__item is None: @@ -1103,6 +1107,7 @@ def real_execute(self, state, actionname: str, namevar: str = "", repeat_text: s self.update_webif_actionstatus(state, self._name, 'True') # noinspection PyCallingNonCallable self.__item(value, caller=self._caller, source=source) + self.__item = self.__eval_item def get(self): orig_item = self.__item @@ -1126,6 +1131,7 @@ def get(self): value = None except Exception: value = None + self.__item = orig_item result = {'function': str(self._function), 'item': item, 'item_from_eval': item_from_eval, 'value': value, 'conditionset': str(self.conditionset.get()), 'previousconditionset': str(self.previousconditionset.get()), 'previousstate_conditionset': str(self.previousstate_conditionset.get()), 'actionstatus': {}} diff --git a/stateengine/StateEngineCondition.py b/stateengine/StateEngineCondition.py index ce3dd0e75..2e1993b55 100755 --- a/stateengine/StateEngineCondition.py +++ b/stateengine/StateEngineCondition.py @@ -68,12 +68,16 @@ def __repr__(self): def check_items(self, check, value=None, item_state=None): item_issue, status_issue, eval_issue, status_eval_issue = None, None, None, None item_value, status_value, eval_value, status_eval_value = None, None, None, None + if check == "attribute": + _orig_value = value + else: + _orig_value = None if check == "se_item" or (check == "attribute" and self.__item is None and self.__eval is None): if value is None: value = StateEngineTools.find_attribute(self._sh, item_state, "se_item_" + self.__name) - if value is not None: + if isinstance(value, str): match = re.match(r'^(.*):', value) - if isinstance(value, str) and value.startswith("eval:"): + if value.startswith("eval:"): _, _, value = value.partition(":") self.__eval = value self.__item = None @@ -87,9 +91,11 @@ def check_items(self, check, value=None, item_state=None): self.__item = value item_value = value if check == "se_status" or (check == "attribute" and self.__status is None and self.__status_eval is None): + if check == "attribute": + value = _orig_value if value is None: value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_" + self.__name) - if value is not None: + if isinstance(value, str): match = re.match(r'^(.*):', value) if isinstance(value, str) and value.startswith("eval:"): _, _, value = value.partition(":") @@ -108,7 +114,7 @@ def check_items(self, check, value=None, item_state=None): if check == "se_eval" or (check == "attribute" and self.__eval is None): if value is None: value = StateEngineTools.find_attribute(self._sh, item_state, "se_eval_" + self.__name) - if value is not None: + if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): _, _, value = value.partition("eval:") @@ -120,9 +126,11 @@ def check_items(self, check, value=None, item_state=None): self.__eval = value eval_value = value if check == "se_status_eval" or (check == "attribute" and self.__status_eval is None): + if check == "attribute": + value = _orig_value if value is None: value = StateEngineTools.find_attribute(self._sh, item_state, "se_status_eval_" + self.__name) - if value is not None: + if isinstance(value, str): match = re.match(r'^(.*):', value) if value.startswith("eval:"): _, _, value = value.partition("eval:") @@ -454,7 +462,7 @@ def __convert(convert_value, convert_current): else: self.__value.set_cast(StateEngineTools.cast_str) convert_value = StateEngineTools.cast_str(convert_value) - convert_current = StateEngineTools.cast_str(convert_value) + convert_current = StateEngineTools.cast_str(convert_current) if not type(_oldvalue) == type(convert_value): self._log_debug("Value {} was type {} and therefore not the same" " type as item value {}. It got converted to {}.", @@ -469,7 +477,6 @@ def __convert(convert_value, convert_current): self.__updatedbynegate if valuetype == "updatedby" else\ self.__triggeredbynegate if valuetype == "triggeredby" else\ self.__negate - if isinstance(value, list): text = "Condition '{0}': {1}={2} negate={3} current={4}" _key = ['{}'.format(state.id), 'conditionsets', '{}'.format(self._abitem.get_variable('current.conditionset_name')), '{}'.format(self.__name), 'current', '{}'.format(valuetype)] @@ -850,26 +857,26 @@ def check_eval(eval_or_status_eval): if self.__status is not None: # noinspection PyUnusedLocal - self._log_debug("Trying to get {} of status item {}", eval_type, self.__status) + self._log_debug("Trying to get {} of status item {}", eval_type, self.__status.property.path) return self.__status.property.last_change_age if eval_type == 'age' else\ self.__status.property.last_change_by if eval_type == 'changedby' else\ self.__status.property.last_update_by if eval_type == 'updatedby' else\ self.__status.property.last_trigger_by if eval_type == 'triggeredby' else\ self.__status.property.value + elif self.__status_eval is not None: + self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) + return_value = check_eval(self.__status_eval) + return return_value elif self.__item is not None: # noinspection PyUnusedLocal - self._log_debug("Trying to get {} of item {}", eval_type, self.__item) + self._log_debug("Trying to get {} of item {}", eval_type, self.__item.property.path) return self.__item.property.last_change_age if eval_type == 'age' else\ self.__item.property.last_change_by if eval_type == 'changedby' else\ self.__item.property.last_update_by if eval_type == 'updatedby' else\ self.__item.property.last_trigger_by if eval_type == 'triggeredby' else\ self.__item.property.value - if self.__status_eval is not None: - self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__status_eval) - return_value = check_eval(self.__status_eval) - return return_value elif self.__eval is not None: - self._log_debug("Trying to get {} of statuseval {}", eval_type, self.__eval) + self._log_debug("Trying to get {} of eval {}", eval_type, self.__eval) return_value = check_eval(self.__eval) return return_value diff --git a/stateengine/StateEngineConditionSet.py b/stateengine/StateEngineConditionSet.py index 8b9d42c7f..fa9f46837 100755 --- a/stateengine/StateEngineConditionSet.py +++ b/stateengine/StateEngineConditionSet.py @@ -89,6 +89,9 @@ def update(self, item, grandparent_item): if item is not None: for attribute in item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") + if name.startswith("eval_"): + _, name = StateEngineTools.partition_strip(name, "_") + func = f"{func}_eval" if name == "": continue try: @@ -97,6 +100,7 @@ def update(self, item, grandparent_item): self.__conditions[name] = StateEngineCondition.SeCondition(self._abitem, name) issue = self.__conditions[name].set(func, item.conf[attribute]) self.__conditions.move_to_end(name, last=True) + if issue not in [[], None, [None]]: self.__unused_attributes.update({name: {'attribute': attribute, 'issue': issue}}) elif name not in self.__used_attributes.keys(): @@ -110,6 +114,9 @@ def update(self, item, grandparent_item): for attribute in grandparent_item.conf: func, name = StateEngineTools.partition_strip(attribute, "_") + if name.startswith("eval_"): + _, name = StateEngineTools.partition_strip(name, "_") + func = f"{func}_eval" if name == "": continue cond1 = name not in self.__used_attributes.keys() diff --git a/stateengine/StateEngineTools.py b/stateengine/StateEngineTools.py index f47861f2a..65e5cc5c0 100755 --- a/stateengine/StateEngineTools.py +++ b/stateengine/StateEngineTools.py @@ -262,7 +262,10 @@ def cast_time(value): # attribute: name of attribute to find def find_attribute(smarthome, base_item, attribute, recursion_depth=0): # 1: parent of given item could have attribute - parent_item = base_item.return_parent() + try: + parent_item = base_item.return_parent() + except Exception: + return None try: _parent_conf = parent_item.conf if parent_item is not None and attribute in _parent_conf: diff --git a/zigbee2mqtt/__init__.py b/zigbee2mqtt/__init__.py index b8cd20b02..2256f9df9 100755 --- a/zigbee2mqtt/__init__.py +++ b/zigbee2mqtt/__init__.py @@ -25,6 +25,7 @@ from datetime import datetime import json +from logging import DEBUG from lib.model.mqttplugin import MqttPlugin @@ -47,7 +48,7 @@ class Zigbee2Mqtt(MqttPlugin): """ Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '2.0.1' + PLUGIN_VERSION = '2.0.2' def __init__(self, sh, **kwargs): """ Initializes the plugin. """ @@ -55,6 +56,8 @@ def __init__(self, sh, **kwargs): # Call init code of parent class (MqttPlugin) super().__init__() + # self.logger = logging.getLogger(__name__) + self.logger.info(f'Init {self.get_shortname()} plugin {self.PLUGIN_VERSION}') # get the parameters for the plugin (as defined in metadata plugin.yaml): @@ -62,8 +65,9 @@ def __init__(self, sh, **kwargs): self.cycle = self.get_parameter_value('poll_period') self.read_at_init = self.get_parameter_value('read_at_init') self._z2m_gui = self.get_parameter_value('z2m_gui') + self._pause_item_path = self.get_parameter_value('pause_item') - # bool_values is only good if used internally, because MQTT data is + # bool_values is only good if used internally, because MQTT data is # usually sent in JSON. So just make this easy... self.bool_values = [False, True] @@ -115,6 +119,8 @@ def run(self): self.logger.debug("Run method called") self.alive = True + if self._pause_item: + self._pause_item(False, self.get_fullname()) # start subscription to all topics self.start_subscriptions() @@ -134,8 +140,10 @@ def stop(self): """ Stop method for the plugin """ self.alive = False + if self._pause_item: + self._pause_item(True, self.get_fullname()) self.logger.debug("Stop method called") - self.scheduler_remove('z2m_c') + self.scheduler_remove('z2m_cycle') # stop subscription to all topics self.stop_subscriptions() @@ -154,15 +162,12 @@ def parse_item(self, item): can be sent to the knx with a knx write function within the knx plugin. """ - # remove this block when its included in smartplugin.py, - # replace with super().parse_item(item) - # check for suspend item - if item.property.path == self._suspend_item_path: - self.logger.debug(f'suspend item {item.property.path} registered') - self._suspend_item = item + # check for pause item + if item.property.path == self._pause_item_path: + self.logger.debug(f'pause item {item.property.path} registered') + self._pause_item = item self.add_item(item, updating=True) return self.update_item - # end block if self.has_iattr(item.conf, Z2M_ATTR): self.logger.debug(f"parsing item: {item}") @@ -174,13 +179,6 @@ def parse_item(self, item): attr = self.get_iattr_value(item.conf, Z2M_ATTR) - if item.type() == 'bool': - bval = self.get_iattr_value(item.conf, Z2M_BVAL) - if bval == []: - bval = None - if bval is None or type(bval) is not list: - bval = self.bool_values - # invert read-only/write-only logic to allow read/write write = not self.get_iattr_value(item.conf, Z2M_RO, False) read = not self.get_iattr_value(item.conf, Z2M_WO, False) or not write @@ -198,6 +196,9 @@ def parse_item(self, item): 'write': write, } if item.type() == 'bool': + bval = self.get_iattr_value(item.conf, Z2M_BVAL) + if bval is None or bval == [] or type(bval) is not list: + bval = self.bool_values data['bool_values'] = bval self._devices[device][attr].update(data) @@ -216,7 +217,7 @@ def parse_item(self, item): def remove_item(self, item): if item not in self._plg_item_dict: - return + return False mapping = self.get_item_mapping(item) if mapping: @@ -239,9 +240,9 @@ def remove_item(self, item): except ValueError: pass - super().remove_item(item) + return super().remove_item(item) - def update_item(self, item, caller='', source=None, dest=None): + def update_item(self, item, caller=None, source=None, dest=None): """ Item has been updated @@ -252,7 +253,17 @@ def update_item(self, item, caller='', source=None, dest=None): """ self.logger.debug(f"update_item: {item} called by {caller} and source {source}") - if self.alive and not self.suspended and not caller.startswith(self.get_shortname()): + # check for pause item + if item is self._pause_item: + if caller != self.get_shortname(): + self.logger.debug(f'pause item changed to {item()}') + if item() and self.alive: + self.stop() + elif not item() and not self.alive: + self.run() + return + + if self.alive and caller and not caller.startswith(self.get_shortname()): if item in self._items_write: @@ -311,7 +322,7 @@ def update_item(self, item, caller='', source=None, dest=None): attr: value }) else: - payload = None + payload = '' self.publish_z2m_topic(device, topic_3, topic_4, topic_5, payload, item, bool_values=bool_values) else: @@ -417,7 +428,13 @@ def on_mqtt_msg(self, topic: str, payload, qos=None, retain=None): if item is not None: item(value, src) - self.logger.info(f"{device}: Item '{item}' set to value {value}") + if device == 'bridge' and (isinstance(value, list) or isinstance(value, dict)): + if self.logger.isEnabledFor(DEBUG): + self.logger.debug(f"{device}: Item '{item}' set to value {value}") + else: + self.logger.info(f"{device}: Item '{item}' set to value {str(value)[:80]}[...] (enable debug log for full output)") + else: + self.logger.info(f"{device}: Item '{item}' set to value {value}") else: self.logger.info(f"{device}: No item for attribute '{attr}' defined to set to {value}") diff --git a/zigbee2mqtt/plugin.yaml b/zigbee2mqtt/plugin.yaml index d9de7f569..3f9d56658 100755 --- a/zigbee2mqtt/plugin.yaml +++ b/zigbee2mqtt/plugin.yaml @@ -12,8 +12,8 @@ plugin: documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1856775-support-thread-f%C3%BCr-das-zigbee2mqtt-plugin - version: 2.0.1 # Plugin version - sh_minversion: '1.9.5.6' # minimum shNG version to use this plugin + version: 2.0.2 # Plugin version + sh_minversion: '1.10.0' # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: '3.8' # minimum Python version to use for this plugin multi_instance: true # plugin supports multi instance @@ -45,12 +45,12 @@ parameters: de: Einlesen aller Werte beim Start en: Read all values at init - suspend_item: + pause_item: type: str default: '' description: - de: Pfad zum Suspend-Item - en: Path to suspend item + de: Pfad zum Pause-Item + en: Path to pause item z2m_gui: type: str