Quellcode für lib.smarthome

#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2011-2014 Marcus Popp                          marcus@popp.mx
# Copyright 2016      Christian Strassburg            c.strassburg@gmx.de
# Copyright 2016-     Martin Sinn                           m.sinn@gmx.de
# Copyright 2020-     Bernd Meiners                 bernd.meiners@mail.de
#########################################################################
#  This file is part of SmartHomeNG.
#
#  SmartHomeNG is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  SmartHomeNG is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with SmartHomeNG. If not, see <http://www.gnu.org/licenses/>.
#########################################################################

__docformat__ = 'reStructuredText'

#########################################################################
#
# TO DO:
# - remove all remarks with old code (that has been moved to lib modules)
#
#########################################################################


#####################################################################
# Check Python Version
#####################################################################
import sys

#####################################################################
# prevent user root
#####################################################################
import os

#####################################################################
# Import minimum set of Python Core Modules
#####################################################################
import datetime
import gc
import locale


#####################################################################
# Import Python Core Modules
#####################################################################

import json
import logging
import logging.handlers
import logging.config
# import platform  # TODO: remove? unused
import shutil

import signal
import subprocess
import threading
import time
import traceback
# try:
#     import psutil  # TODO: remove? unused
# except ImportError:
#     pass

BASE = os.path.sep.join(os.path.realpath(__file__).split(os.path.sep)[:-2])
PIDFILE = os.path.join(BASE, 'var', 'run', 'smarthome.pid')


#####################################################################
# Import SmartHomeNG Modules
#####################################################################
import lib.config
import lib.connection
import lib.daemon
import lib.item
import lib.log
import lib.logic
import lib.module
import lib.network
import lib.plugin
import lib.scene
import lib.scheduler
import lib.tools
import lib.utils
import lib.orb
import lib.backup
import lib.translation
from lib.shtime import Shtime
import lib.shyaml
from lib.shpypi import Shpypi
from lib.triggertimes import TriggerTimes
from lib.constants import (YAML_FILE, CONF_FILE, DEFAULT_FILE, DIRS, BASES, BASE_LOG, BASE_LOGIC, BASE_MODULE, BASE_PLUGIN, BASE_SH, DIR_CACHE, DIR_ENV, DIR_ETC, DIR_ITEMS, DIR_LIB, DIR_LOGICS, DIR_MODULES, DIR_PLUGINS, DIR_SCENES, DIR_STRUCTS, DIR_TPL, DIR_UF, DIR_VAR)
import lib.userfunctions as uf
from lib.systeminfo import Systeminfo


_sh_instance = None


#####################################################################
# Classes
#####################################################################

[Doku]class SmartHome(): """ SmartHome ist the main class of SmartHomeNG. All other objects can be addressed relative to the main oject, which is an instance of this class. Mostly it is referred to as ``sh``, ``_sh`` or ``smarthome``. """ # default values, if values are not specified in smarthome.yaml _default_language = 'de' _fallback_language_order = 'en,de' _threadinfo_export = False # for scheduler _restart_on_num_workers = 30 # --- BASE = os.path.sep.join(os.path.realpath(__file__).split(os.path.sep)[:-2])
[Doku] def initialize_vars(self): # the APIs available though the smarthome object instance: self.shtime = None self.items = None self.plugins = None self.logics = None self.scheduler = None self.plugin_load_complete = False self.item_load_complete = False self.plugin_start_complete = False self._smarthome_conf_basename = None self.__event_listeners = {} self.__all_listeners = [] self.modules = None self.__children = []
[Doku] def initialize_dir_vars(self): self._base_dir = BASE self.base_dir = self._base_dir # **base_dir** is deprecated. Use method get_basedir() instead. - for external modules using that var (backend, ...?) self._etc_dir = os.path.join(self._extern_conf_dir, DIR_ETC) # self._conf_dir contains the base dir for config folders if self._config_etc: self._conf_dir = self._etc_dir else: self._conf_dir = self._extern_conf_dir # shng system dirs self._var_dir = os.path.join(self._base_dir, DIR_VAR) self._lib_dir = os.path.join(self._base_dir, DIR_LIB) self._plugins_dir = os.path.join(self._base_dir, DIR_PLUGINS) self._modules_dir = os.path.join(self._base_dir, DIR_MODULES) self._templates_dir = os.path.join(self._base_dir, DIR_TPL) # env and var dirs self._env_dir = os.path.join(self._lib_dir, DIR_ENV + os.path.sep) self._cache_dir = os.path.join(self._var_dir, DIR_CACHE + os.path.sep) # user config dirs self._items_dir = os.path.join(self._conf_dir, DIR_ITEMS + os.path.sep) self._structs_dir = os.path.join(self._conf_dir, DIR_STRUCTS) self._logic_dir = os.path.join(self._conf_dir, DIR_LOGICS + os.path.sep) # TODO: remove self._logic_dir later for uniformness (dirs with plural naming) self._logics_dir = self._logic_dir self._functions_dir = os.path.join(self._conf_dir, DIR_UF + os.path.sep) self._scenes_dir = os.path.join(self._conf_dir, DIR_SCENES + os.path.sep) # system config files self._smarthome_conf_basename = os.path.join(self._etc_dir, BASE_SH) self._log_conf_basename = os.path.join(self._etc_dir, BASE_LOG) self._module_conf_basename = os.path.join(self._etc_dir, BASE_MODULE) self._plugin_conf_basename = os.path.join(self._etc_dir, BASE_PLUGIN) self._logic_conf_basename = os.path.join(self._etc_dir, BASE_LOGIC) self._env_logic_conf_basename = os.path.join(self._env_dir, BASE_LOGIC)
[Doku] def create_directories(self): """ Create directories used by SmartHomeNG if they don't exist """ os.makedirs(self._items_dir, mode=0o775, exist_ok=True) os.makedirs(self._scenes_dir, mode=0o775, exist_ok=True) os.makedirs(self._functions_dir, mode=0o775, exist_ok=True) os.makedirs(self._structs_dir, mode=0o775, exist_ok=True) os.makedirs(self._var_dir, mode=0o775, exist_ok=True) os.makedirs(self._cache_dir, mode=0o775, exist_ok=True) os.makedirs(os.path.join(self._var_dir, 'backup'), mode=0o775, exist_ok=True) os.makedirs(os.path.join(self._var_dir, 'db'), mode=0o775, exist_ok=True) os.makedirs(os.path.join(self._var_dir, 'log'), mode=0o775, exist_ok=True) os.makedirs(os.path.join(self._var_dir, 'run'), mode=0o775, exist_ok=True)
def __init__(self, MODE, extern_conf_dir='', config_etc=False): """ Initialization of main smarthome object """ self.shng_status = {'code': 0, 'text': 'Initializing'} self._logger = logging.getLogger(__name__) self._logger_main = logging.getLogger(__name__) global _sh_instance if _sh_instance is not None: import inspect curframe = inspect.currentframe() calframe = inspect.getouterframes(curframe, 4) self._logger.critical("A second 'smarthome' object has been created. There should only be ONE instance of class 'smarthome'!!! Called from: {} ({})".format(calframe[1][1], calframe[1][3])) else: _sh_instance = self # keep for checking on restart command self._mode = MODE self._config_etc = config_etc self._extern_conf_dir = BASE if extern_conf_dir != '': self._extern_conf_dir = extern_conf_dir self.initialize_vars() self.initialize_dir_vars() self.create_directories() self.logs = lib.log.Logs(self) # initialize object for memory logs os.chdir(self._base_dir) self.PYTHON_VERSION = lib.utils.get_python_version() self.python_bin = sys.executable # get systeminfo closs self.systeminfo = Systeminfo # set default timezone to UTC self.shtime = Shtime(self) threading.current_thread().name = 'Main' self.alive = True # import bin.shngversion as shngversion # VERSION = shngversion.get_shng_version() # self.branch = shngversion.get_shng_branch() # self.version = shngversion.get_shng_version() self.connections = None self._pidfile = PIDFILE # check config files self.checkConfigFiles() if MODE == 'unittest': return ############################################################# # Reading smarthome.yaml config = lib.config.parse_basename(self._smarthome_conf_basename, configtype='SmartHomeNG') if config != {}: for attr in config: if not isinstance(config[attr], dict): # ignore sub items vars(self)['_' + attr] = config[attr] del config # clean up else: # no valid smarthome.yaml found print("No base configuration - terminating SmartHomeNG") print("Hint: Are Language (preferably: DE_de) and character (UTF-8) set configured in operating system?") exit(1) if hasattr(self, '_module_paths'): sys.path.extend(self._module_paths if type(self._module_paths) is list else [self._module_paths]) ############################################################# # Setting (local) tz if set in smarthome.yaml # (to ensure propper logging) if hasattr(self, '_tz'): self.shtime.set_tz(self._tz) ############################################################# # test if needed Python packages are installed # - core requirements = libs # self.shpypi = Shpypi(self) # core_reqs = shpypi.test_core_requirements(logging=False) # if core_reqs == 0: # print("Trying to restart shng") # print() # exit(0) # elif core_reqs == -1: # print("Unable to install core requirements") # print() # exit(1) ############################################################# # setup logging logsetup = self.init_logging(self._log_conf_basename, MODE) ############################################################# # get shng version information # shngversion.get_plugins_version() may only be called after logging is initialized import bin.shngversion as shngversion # VERSION = shngversion.get_shng_version() self.branch = shngversion.get_shng_branch() self.version = shngversion.get_shng_version() self.plugins_version = shngversion.get_plugins_version() self.shng_status = {'code': 1, 'text': 'Initializing: Logging initialized'} if hasattr(self, '_tz'): # set _tz again (now with logging enabled), # so that shtime.set_tz can produce log output self.shtime.set_tz(self._tz) del self._tz ############################################################# # Fork process and write pidfile if self._mode == 'default': lib.daemon.daemonize(PIDFILE) else: lib.daemon.write_pidfile(os.getpid(), PIDFILE) print(f"MODE = {self._mode}") print(f"-------------------- Init SmartHomeNG {self.version} --------------------") ############################################################# # Write startup message to log(s) pid = lib.daemon.read_pidfile(PIDFILE) virtual_text = '' if lib.utils.running_virtual(): virtual_text = ' in virtual environment' self._logger_main.notice(f"-------------------- Init SmartHomeNG {self.version} --------------------") if logsetup is not True: self._logger_main.warning(f"Problem with logging config: {logsetup}") self._logger_main.notice(f"Running in Python interpreter 'v{self.PYTHON_VERSION}'{virtual_text}, from directory {self._base_dir}") self._logger_main.notice(f" - operating system '{self.systeminfo.get_osname()}' (pid={pid})") if self.systeminfo.get_rasppi_info() == '': self._logger_main.notice(f" - on '{self.systeminfo.get_cpubrand()}'") else: self._logger_main.notice(f" - on '{self.systeminfo.get_rasppi_info()}'") if logging.getLevelName('NOTICE') == 31: self._logger_main.notice(f" - Loglevel NOTICE is set to value {logging.getLevelName('NOTICE')} because handler of root logger is set to level WARNING or higher - Set level of handler '{self.logs.root_handler_name}' to 'NOTICE'!") default_encoding = locale.getpreferredencoding() # returns cp1252 on windows if not (default_encoding in ['UTF8', 'UTF-8']): self._logger.warning(f"Encoding should be UTF8 but is instead {default_encoding}") if self._extern_conf_dir != BASE: self._logger.notice(f"Using config dir {self._extern_conf_dir}") ############################################################# # Initialize multi-language support lib.translation.initialize_translations(self._base_dir, self._default_language, self._fallback_language_order) self._logger.info("Translation initialized") # make reload_translations() method publicly available for call by eval-syntax-checker self.reload_translations = lib.translation.reload_translations ############################################################# # Test if plugins are installed if not os.path.isdir(self._plugins_dir): self._logger.critical("Plugin folder does not exist!") self._logger.critical(f"Please create folder '{self._plugins_dir}' and install plugins.") self._logger.critical("Aborting") exit(1) if not os.path.isdir(os.path.join(self._plugins_dir, 'database')): self._logger.critical(f"No plugins found in folder '{self._plugins_dir}'. Please install plugins.") self._logger.critical("Aborting") exit(1) ############################################################# # test if needed Python packages for configured plugins # are installed self.shpypi = Shpypi.get_instance() if self.shpypi is None: self.shpypi = Shpypi(self) if hasattr(self, '_shpypi_crontab'): self.shpypi.set_scheduler_crontab(self._shpypi_crontab) base_reqs = self.shpypi.test_base_requirements(self) if base_reqs == 0: self.restart('SmartHomeNG (Python package installation)') exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG elif base_reqs == -1: self._logger.critical("Python package requirements for modules are not met and unable to install base requirements") self._logger.critical("Do you have multiple Python3 Versions installed? Maybe PIP3 looks into a wrong Python environment. Try to configure pip_command in etc/smarthome.yaml") self._logger.critical("Aborting") exit(1) plugin_reqs = self.shpypi.test_conf_plugins_requirements(self._plugin_conf_basename, self._plugins_dir) if plugin_reqs == 0: self.restart('SmartHomeNG (Python package installation)') exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG elif plugin_reqs == -1: self._logger.critical("Python package requirements for configured plugins are not met and unable to install those requirements") self._logger.critical("Do you have multiple Python3 Versions installed? Maybe PIP3 looks into a wrong Python environment. Try to configure pip_command in etc/smarthome.yaml") self._logger.critical("Aborting") exit(1) self.shng_status = {'code': 2, 'text': 'Initializing: Requirements checked'} # Add Signal Handling # signal.signal(signal.SIGHUP, self.reload_logics) signal.signal(signal.SIGINT, self.stop) signal.signal(signal.SIGTERM, self.stop) ############################################################# # Check Time while datetime.date.today().isoformat() < '2024-01-01': # XXX update date time.sleep(5) self._logger.info("Waiting for updated time.") ############################################################# # Catching Exceptions sys.excepthook = self._excepthook ############################################################# # Initialize holidays self.shtime._initialize_holidays() self._logger_main.notice(" - " + self.shtime.log_msg) ############################################################# # check processor speed (if not done before) self.cpu_speed_class = self.systeminfo.get_cpu_speed(self._var_dir) if self.cpu_speed_class is None: self.shng_status = {'code': 3, 'text': 'Checking processor speed'} self.cpu_speed_class = self.systeminfo.check_cpu_speed(self._var_dir) ############################################################# # test if a valid locale is set in the operating system if os.name != 'nt': try: if not any(utf in os.environ['LANG'].lower() for utf in ['utf-8', 'utf8']): self._logger.error("Locale for the enviroment is not set to a valid value. Set the LANG environment variable to a value supporting UTF-8") except: self._logger.error("Locale for the enviroment is not set. Defaulting to en_US.UTF-8") os.environ["LANG"] = 'en_US.UTF-8' os.environ["LC_ALL"] = 'en_US.UTF-8' ############################################################# # Link Tools self.tools = lib.tools.Tools() ############################################################# # Link Sun and Moon self.sun = False self.moon = False if lib.orb.ephem is None: self._logger.warning("Could not find/use ephem!") elif not hasattr(self, '_lon') and hasattr(self, '_lat'): self._logger.warning('No latitude/longitude specified => you could not use the sun and moon object.') else: if not hasattr(self, '_elev'): self._elev = None self.sun = lib.orb.Orb('sun', self._lon, self._lat, self._elev) self.moon = lib.orb.Orb('moon', self._lon, self._lat, self._elev) @property def lat(self) -> float: """ Read-Only Property: latitude (from smarthome.yaml) :return: latitude (from smarthome.yaml) """ return float(self._lat) @property def lon(self) -> float: """ Read-Only Property: longitude (from smarthome.yaml) :return: longitude (from smarthome.yaml) """ return float(self._lon)
[Doku] def get_defaultlanguage(self): """ Returns the configured default language of SmartHomeNG """ return self._default_language
[Doku] def set_defaultlanguage(self, language): """ Returns the configured default language of SmartHomeNG """ self._default_language = language lib.translation.set_default_language(language)
[Doku] def get_basedir(self): """ Function to return the base directory of the running SmartHomeNG installation :return: Base directory as an absolute path :rtype: str """ return self._base_dir
[Doku] def get_confdir(self): """ Function to return the config directory (that contain 'etc', 'logics' and 'items' subdirectories) :return: Config directory as an absolute path :rtype: str """ return self._extern_conf_dir
[Doku] def get_etcdir(self) -> str: """ Function to return the etc config directory :return: Config directory as an absolute path """ return self._etc_dir
[Doku] def get_structsdir(self) -> str: """ Function to return the structs config directory :return: Config directory as an absolute path """ return self._structs_dir
[Doku] def get_vardir(self): """ Function to return the var directory used by SmartHomeNG :return: var directory as an absolute path :rtype: str """ return self._var_dir
[Doku] def get_config_dir(self, config): """ Function to return a config dir used by SmartHomeNG replace / prevent a plethora of get_<foo>dir() functions Returns '' for invalid config strings. :return: directory as an absolute path :rtype: str """ # method would work fine without this check, but... # make sure we don't allow "any" function call here if config not in DIRS: return '' if hasattr(self, f'get_{config}dir'): return getattr(self, f'get_{config}dir')() elif hasattr(self, f'_{config}_dir'): return getattr(self, f'_{config}_dir') return ''
[Doku] def get_config_file(self, config, extension=YAML_FILE): """ Function to return a config file used by SmartHomeNG Returns '' for invalid config strings. :return: file name as an absolute path :rtype: str """ # method would work fine without this check, but... # make sure we don't allow "any" function call here if config not in BASES: return '' return os.path.join(self.get_etcdir(), config + extension)
[Doku] def getBaseDir(self): """ Function to return the base directory of the running SmartHomeNG installation **getBaseDir()** is deprecated. Use method get_basedir() instead. :return: Base directory as an absolute path :rtype: str """ self._deprecated_warning('sh.get_basedir()') return self._base_dir
[Doku] def checkConfigFiles(self): """ This function checks if the needed configuration files exist. It checks for CONF and YAML files. If they dont exist, it is checked if a default configuration exist. If so, the default configuration is copied to corresponding configuration file. The check is done for the files that have to exist (with some content) or SmartHomeNG won't start: - smarthome.yaml / smarthome.conf - logging.yaml - plugin.yaml / plugin.conf - module.yaml / module.conf - logic.yaml / logic.conf """ configs = ['holidays', 'logging', 'logic', 'module', 'plugin', 'admin', 'smarthome'] for c in configs: default = os.path.join(self._base_dir, 'etc', c + YAML_FILE + DEFAULT_FILE) conf_basename = os.path.join(self._etc_dir, c) if ((c == 'logging' and not (os.path.isfile(conf_basename + YAML_FILE))) or (c != 'logging' and not (os.path.isfile(conf_basename + YAML_FILE)) and not (os.path.isfile(conf_basename + CONF_FILE)))): if os.path.isfile(default): shutil.copy2(default, conf_basename + YAML_FILE)
[Doku] def init_logging(self, conf_basename='', MODE='default'): """ This function initiates the logging for SmartHomeNG. """ if conf_basename == '': conf_basename = self._log_conf_basename # conf_dict = lib.shyaml.yaml_load(conf_basename + YAML_FILE, True) logsetup = self.logs.configure_logging() if logsetup is not True: conf_basename = self._log_conf_basename + YAML_FILE + '.default' print(" Trying default logging configuration from:") print(f" {conf_basename}") print() # conf_dict = lib.shyaml.yaml_load(conf_basename + YAML_FILE + '.default', True) if not self.logs.configure_logging('logging.yaml.default'): print("ABORTING") print() exit(1) print("Starting with default logging configuration") if MODE == 'interactive': # remove default stream handler logging.getLogger().disabled = True elif MODE == 'verbose': logging.getLogger().setLevel(logging.INFO) elif MODE == 'debug': logging.getLogger().setLevel(logging.DEBUG) elif MODE == 'quiet': logging.getLogger().setLevel(logging.WARNING) return logsetup
################################################################# # Process Methods #################################################################
[Doku] def start(self): """ This function starts the threads of the main smarthome object. The main thread that is beeing started is called ``Main`` """ self.shng_status = {'code': 10, 'text': 'Starting'} threading.currentThread().name = 'Main' ############################################################# # Prepare TriggerTimes for Scheduler ############################################################# self.triggertimes = TriggerTimes(self) ############################################################# # Start Scheduler ############################################################# self.scheduler = lib.scheduler.Scheduler.get_instance() if self.scheduler is None: self.scheduler = lib.scheduler.Scheduler(self) self.trigger = self.scheduler.trigger self.scheduler.start() # set warn level to a higher number of workers on fast cpus if self.cpu_speed_class == 'fast': self.scheduler.set_worker_warn_count(60) elif self.cpu_speed_class == 'medium': self.scheduler.set_worker_warn_count(35) else: # leave it on standard (20 workers) pass ############################################################# # Init Connections ############################################################# self.connections = lib.connection.Connections() # self.connections = lib.network.Connections() # switch on removing lib.connection ############################################################# # Init and start loadable Modules ############################################################# self.shng_status = {'code': 11, 'text': 'Starting: Initializing and starting loadable modules'} self._logger.info("Init loadable Modules") self.modules = lib.module.Modules(self, configfile=self._module_conf_basename) self.modules.start() ############################################################# # Init and import user-functions ############################################################# uf.init_lib(self.get_basedir(), self) ############################################################# # Init Item-Wrapper ############################################################# self.items = lib.item.Items(self) ############################################################# # Init Plugins ############################################################# self.shng_status = {'code': 12, 'text': 'Starting: Initializing plugins'} os.chdir(self._base_dir) if self._mode != 'default': print("---> Start initialization of plugins") self._logger.info("Start initialization of plugins") self.plugins = lib.plugin.Plugins(self, configfile=self._plugin_conf_basename) self.plugin_load_complete = True ############################################################# # Init Items (load item definitions) ############################################################# self.shng_status = {'code': 13, 'text': 'Starting: Loading item definitions'} if self._mode != 'default': print("---> Start initialization of items") self._logger.info("Start initialization of items") self.items.load_itemdefinitions(self._env_dir, self._items_dir, self._etc_dir, self._plugins_dir) self.item_count = self.items.item_count() if self._mode != 'default': print(f"---> Items initialization finished, {self.items.item_count()} items loaded") self._logger.info(f"Items initialization finished, {self.items.item_count()} items loaded") self.item_load_complete = True ############################################################# # Init Logics ############################################################# self.shng_status = {'code': 15, 'text': 'Starting: Initializing logics'} self.logics = lib.logic.Logics(self, self._logic_conf_basename, self._env_logic_conf_basename) # signal.signal(signal.SIGHUP, self.logics.reload_logics) ############################################################# # Init Scenes ############################################################# self.scenes = lib.scene.Scenes(self) ############################################################# # Start Connections - remove with lib.connection ############################################################# self.scheduler.add('sh.connections', self.connections.check, cycle=10, offset=0) self._export_threadinfo() ############################################################# # Start Plugins ############################################################# self.shng_status = {'code': 16, 'text': 'Starting: Starting plugins'} self.plugins.start() self.plugin_start_complete = True ############################################################# # Start connection monitoring - enable on removing lib.connection ############################################################# # self.scheduler.add('sh.connection_monitor', self.connections.check, cycle=10, offset=0) ############################################################# # Execute Maintenance Method ############################################################# self.scheduler.add('sh.garbage_collection', self._maintenance, prio=8, cron=['init', '4 2 * *'], offset=0) if self._threadinfo_export: self.scheduler.add('sh.thread_info', self._export_threadinfo, prio=8, cycle=120, offset=0) ############################################################# # Main Loop ############################################################# self.shng_status = {'code': 20, 'text': 'Running'} if self._mode != 'default': print("-------------------- SmartHomeNG initialization finished --------------------") self._logger_main.notice("-------------------- SmartHomeNG initialization finished --------------------") # modify/replace on removing lib.connection while self.alive: try: self.connections.poll() except Exception as e: self._logger.exception(f"Connection polling failed: {e}")
[Doku] def stop(self, signum=None, frame=None): """ This method is used to stop SmartHomeNG and all it's threads """ self.shng_status = {'code': 31, 'text': 'Stopping'} self.alive = False self._logger.info(f"stop: Number of Threads: {threading.activeCount()}") if self.items is not None: self.items.stop() if self.scheduler is not None: self.scheduler.stop() if self.plugins is not None: self.plugins.stop() if self.modules is not None: self.modules.stop() if self.connections is not None: self.connections.close() self.shng_status = {'code': 32, 'text': 'Stopping: Stopping threads'} for thread in threading.enumerate(): if thread.name != 'Main': try: thread.join(1) except Exception as e: pass if threading.active_count() > 1: header_logged = False for thread in threading.enumerate(): if thread.name != 'Main' and thread.name[0] != '_' \ and thread.name != 'modules.websocket.websocket_server' \ and not thread.name.startswith('ThreadPoolExecutor'): if not header_logged: self._logger.warning("The following threads have not been terminated properly by their plugins (please report to the plugin's author):") header_logged = True self._logger.warning(f"-Thread: {thread.name}, still alive") # if header_logged: # self._logger.warning("SmartHomeNG stopped") # else: self._logger_main.notice("-------------------- SmartHomeNG stopped --------------------") self.shng_status = {'code': 33, 'text': 'Stopped'} lib.daemon.remove_pidfile(PIDFILE) logging.shutdown() exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG
[Doku] def restart(self, source=''): """ This method is used to restart the python interpreter and SmartHomeNG If SmartHomeNG was started in one of the foreground modes (-f, -i, -d), just quit and let the user restart manually. """ if self._mode in ['foreground', 'debug', 'interactive']: if source != '': source = ', initiated by ' + source self._logger_main.notice(f"-------------------- SmartHomeNG should restart{source} but is in {self._mode} mode and thus will just try to stop --------------------") self.stop() if self.shng_status['code'] == 30: self._logger.warning("Another RESTART is issued, while SmartHomeNG is restarting. Reason: "+source) else: self.shng_status = {'code': 30, 'text': 'Restarting'} if source != '': source = ', initiated by ' + source self._logger_main.notice(f"-------------------- SmartHomeNG restarting{source} --------------------") # python_bin could contain spaces (at least on windows) python_bin = sys.executable if ' ' in python_bin: python_bin = f'"{python_bin}"' command = python_bin + ' ' + os.path.join(self._base_dir, 'bin', 'smarthome.py') + ' -r' self._logger.info(f"Restart command = '{command}'") try: p = subprocess.Popen(command, shell=True) exit(5) # exit code 5 -> for systemctl to restart SmartHomeNG except subprocess.SubprocessError as e: self._logger.error(f"Restart command '{command}' failed with error {e}")
[Doku] def list_threads(self, txt): cp_threads = 0 http_threads = 0 for thread in threading.enumerate(): if thread.name.find("CP Server") == 0: cp_threads += 1 if thread.name.find("HTTPServer") == 0: http_threads +=1 self._logger.info(f"list_threads: {txt} - Number of Threads: {threading.activeCount()} (CP Server={cp_threads}, HTTPServer={http_threads}") for thread in threading.enumerate(): if thread.name.find("CP Server") != 0 and thread.name.find("HTTPServer") != 0: self._logger.info(f"list_threads: {txt} - Thread {thread.name}") return
################################################################# # Item Methods ################################################################# def __iter__(self): return self.items.get_toplevel_items() ################################################################# # Event Methods #################################################################
[Doku] def add_event_listener(self, events, method): """ This Function adds listeners for a list of events. This function is called from plugins interfacing with visus (e.g. visu_websocket) :param events: List of events to add listeners for :param method: Method used by the visu-interface :type events: list :type method: object """ for event in events: if event in self.__event_listeners: self.__event_listeners[event].append(method) else: self.__event_listeners[event] = [method] self.__all_listeners.append(method)
[Doku] def return_event_listeners(self, event='all'): """ This function returns the listeners for a specified event. :param event: Name of the event or 'all' to return all listeners :type event: str :return: List of listeners :rtype: list """ if event == 'all': return self.__all_listeners elif event in self.__event_listeners: return self.__event_listeners[event] else: return []
################################################################# # Helper Methods ################################################################# def _maintenance(self): self._logger.debug("_maintenance: Started") references = sum(self._object_refcount().values()) self._logger.info(f"_maintenance: Object references: {references}") self._garbage_collection() references = sum(self._object_refcount().values()) self._logger.info(f"_maintenance: Object references: {references}") def _excepthook(self, typ, value, tb): mytb = "".join(traceback.format_tb(tb)) self._logger.error(f"Unhandled exception: {value}\n{typ}\nrunning SmartHomeNG {self.version}\nException: {mytb}") def _garbage_collection(self): c = gc.collect() self._logger.debug(f"Garbage collector: collected {c} objects.") def _export_threadinfo(self): filename = os.path.join(self.base_dir, 'var', 'run', 'threadinfo.json') if self._threadinfo_export: all_threads = [] for t in threading.enumerate(): # create thread list to be written to ../var/run at = {} at['id'] = t.ident try: at['native_id'] = t.native_id except: at['native_id'] = '' at['name'] = t.name all_threads.append(at) # write thread list to ../var/run with open(filename, 'w', encoding='utf-8') as f: json.dump(all_threads, f, ensure_ascii=False, indent=4) else: if os.path.exists(filename): os.remove(filename)
[Doku] def object_refcount(self): """ Function to return the number of defined objects in SmartHomeNG :return: Number of objects :rtype: int """ objects = self._object_refcount() objects = [(x[1], x[0]) for x in list(objects.items())] try: objects.sort(reverse=True) except Exception as ex: self._logger.info(f"object_refcount - sort: Exception {ex}") return objects
def _object_refcount(self): objects = {} for module in list(sys.modules.values()): for sym in dir(module): try: obj = getattr(module, sym) if isinstance(obj, type): objects[obj] = sys.getrefcount(obj) except: pass return objects ##################################################################### # Diplay DEPRECATED warning ##################################################################### def _deprecated_warning(self, n_func=''): """ Display function deprecated warning """ if hasattr(self, '_deprecated_warnings'): if lib.utils.Utils.to_bool(self._deprecated_warnings) == False: return else: return # if parameter is not defined d_func = 'sh.' + str(sys._getframe(1).f_code.co_name) + '()' if n_func != '': n_func = '- use the ' + n_func + ' instead' try: d_test = ' (' + str(sys._getframe(2).f_locals['self'].__module__) + ')' except: d_test = '' called_by = str(sys._getframe(2).f_code.co_name) in_class = '' try: in_class = 'class ' + str(sys._getframe(2).f_locals['self'].__class__.__name__) + d_test except: in_class = 'a logic?' + d_test if called_by == '<module>': called_by = str(sys._getframe(3).f_code.co_name) level = 3 while True: level += 1 try: c_b = str(sys._getframe(level).f_code.co_name) except ValueError: c_b = '' if c_b == '': break called_by += ' -> ' + c_b # called_by = str(sys._getframe(3).f_code.co_name) if not hasattr(self, 'dep_id_list'): self.dep_id_list = [] id_str = d_func + '|' + in_class + '|' + called_by if not id_str in self.dep_id_list: self._logger.warning(f"DEPRECATED: Used function '{d_func}', called in '{in_class}' by '{called_by}' {n_func}") self.dep_id_list.append(id_str) return ##################################################################### # THE FOLLOWING METHODS ARE DEPRECATED ##################################################################### # obsolete by utils.
[Doku] def string2bool(self, string): """ Returns the boolean value of a string DEPRECATED - Use lib.utils.Utils.to_bool(string) instead :param string: string to convert :type string: str :return: Parameter converted to bool :rtype: bool """ self._deprecated_warning('lib.utils.Utils.to_bool(string) function') try: return lib.utils.Utils.to_bool(string) except Exception as e: return None
################################################################# # Item Methods #################################################################
[Doku] def add_item(self, path, item): """ Function to to add an item to the dictionary of items. If the path does not exist, it is created DEPRECATED - Use the Items-API instead :param path: Path of the item :param item: The item itself :type path: str :type item: object """ self._deprecated_warning('Items-API') return self.items.add_item(path, item)
[Doku] def return_item(self, string): """ Function to return the item for a given path DEPRECATED - Use the Items-API instead :param string: Path of the item to return :type string: str :return: Item :rtype: object """ self._deprecated_warning('Items-API') return self.items.return_item(string)
[Doku] def return_items(self): """" Function to return a list with all items DEPRECATED - Use the Items-API instead :return: List of all items :rtype: list """ self._deprecated_warning('Items-API') return self.items.return_items()
[Doku] def match_items(self, regex): """ Function to match items against a regular expresseion DEPRECATED - Use the Items-API instead :param regex: Regular expression to match items against :type regex: str :return: List of matching items :rtype: list """ self._deprecated_warning('Items-API') return self.items.match_items(regex)
# regex, __, attr = regex.partition(':') # regex = regex.replace('.', '\.').replace('*', '.*') + '$' # regex = re.compile(regex) # attr, __, val = attr.partition('[') # val = val.rstrip(']') # if attr != '' and val != '': # return [self.__item_dict[item] for item in self.__items if regex.match(item) and attr in self.__item_dict[item].conf and ((type(self.__item_dict[item].conf[attr]) in [list,dict] and val in self.__item_dict[item].conf[attr]) or (val == self.__item_dict[item].conf[attr]))] # elif attr != '': # return [self.__item_dict[item] for item in self.__items if regex.match(item) and attr in self.__item_dict[item].conf] # else: # return [self.__item_dict[item] for item in self.__items if regex.match(item)]
[Doku] def find_items(self, conf): """" Function to find items that match the specified configuration DEPRECATED - Use the Items-API instead :param conf: Configuration to look for :type conf: str :return: list of matching items :rtype: list """ self._deprecated_warning('Items-API') return self.items.find_items(conf)
[Doku] def find_children(self, parent, conf): """ Function to find children with the specified configuration DEPRECATED - Use the Items-API instead :param parent: parent item on which to start the search :param conf: Configuration to look for :type parent: str :type conf: str :return: list or matching child-items :rtype: list """ self._deprecated_warning('Items-API') return self.items.find_children(parent, conf)
################################################################# # Module Methods #################################################################
[Doku] def return_modules(self): """ Returns a list with the names of all loaded modules DEPRECATED - Use the Modules-API instead :return: list of module names :rtype: list """ self._deprecated_warning('Modules-API') return self.modules.return_modules()
[Doku] def get_module(self, name): """ Returns the module object for the module named by the parameter or None, if the named module is not loaded DEPRECATED - Use the Modules-API instead :param name: Name of the module to return :type name: str :return: list of module names :rtype: object """ self._deprecated_warning('Modules-API') return self.modules.get_module(name)
################################################################# # Plugin Methods #################################################################
[Doku] def return_plugins(self): """ Returns a list with the instances of all loaded plugins DEPRECATED - Use the Plugins-API instead :return: list of plugin names :rtype: list """ self._deprecated_warning('Plugins-API') return self.plugins.return_plugins()
################################################################# # Logic Methods #################################################################
[Doku] def reload_logics(self, signum=None, frame=None): """ Function to reload all logics DEPRECATED - Use the Logics-API instead """ self._deprecated_warning('Logics-API') self.logics.reload_logics(signum, frame)
[Doku] def return_logic(self, name): """ Returns (the object of) one loaded logic with given name DEPRECATED - Use the Logics-API instead :param name: name of the logic to get :type name: str :return: object of the logic :rtype: object """ self._deprecated_warning('Logics-API') self.logics.return_logic(name)
[Doku] def return_logics(self): """ Returns a list with the names of all loaded logics DEPRECATED - Use the Logics-API instead :return: list of logic names :rtype: list """ self._deprecated_warning('Logics-API') self.logics.return_logics()
################################################################# # Time Methods #################################################################
[Doku] def now(self): """ Returns the actual time in a timezone aware format DEPRECATED - Use the Shtime-API instead :return: Actual time for the local timezone :rtype: datetime """ self._deprecated_warning('Shtime-API') return self.shtime.now()
[Doku] def tzinfo(self): """ Returns the info about the actual local timezone DEPRECATED - Use the Shtime-API instead :return: Timezone info :rtype: str """ self._deprecated_warning('Shtime-API') return self.shtime.tzinfo()
[Doku] def utcnow(self): """ Returns the actual time in GMT DEPRECATED - Use the Shtime-API instead :return: Actual time in GMT :rtype: datetime """ self._deprecated_warning('Shtime-API') return self.shtime.utcnow()
[Doku] def utcinfo(self): """ Returns the info about the GMT timezone DEPRECATED - Use the Shtime-API instead :return: Timezone info :rtype: str """ self._deprecated_warning('Shtime-API') return self.shtime.utcinfo()
[Doku] def runtime(self): """ Returns the uptime of SmartHomeNG DEPRECATED - Use the Shtime-API instead :return: Uptime in days, hours, minutes and seconds :rtype: str """ self._deprecated_warning('Shtime-API') return self.shtime.runtime()
[Doku] @staticmethod def get_instance(): """ returns the instance of running smarthome class """ global _sh_instance return _sh_instance