#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2016- Martin Sinn m.sinn@gmx.de
# Copyright 2011-2013 Marcus Popp marcus@popp.mx
#########################################################################
# 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/>.
##########################################################################
"""
This library implements logics in SmartHomeNG.
The main class ``Logics`` implements the handling for all logics. This class has a couple
of static methods. These methods implement the API for handling logics from within SmartHomeNG and from plugins.
This API enables plugins to configure new logics or change the configuration of existing plugins.
Each logic is represented by an instance of the class ``Logic``.
The methods of the class Logics implement the API for logics.
They can be used the following way: To call eg. **enable_logic(name)**, use the following syntax:
.. code-block:: python
from lib.logic import Logics
logics = Logics.get_instance()
# to access a method (eg. enable_logic()):
logics.enable_logic(name)
:Note: Do not use the functions or variables of the main smarthome object any more. They are deprecated. Use the methods of the class **Logics** instead.
:Note: This library is part of the core of SmartHomeNG. Regular plugins should not need to use this API. It is manily implemented for plugins near to the core like **backend** or **blockly**!
"""
import logging
import os
from collections import OrderedDict
import ast
import lib.config
from lib.shtime import Shtime
import lib.shyaml as shyaml
from lib.utils import Utils
from lib.constants import PLUGIN_PARSE_LOGIC
from lib.constants import (YAML_FILE, CONF_FILE, DIR_LOGICS, DIR_ETC, BASE_LOGIC, BASE_ADMIN)
from lib.item import Items
from lib.plugin import Plugins
from lib.scheduler import Scheduler
logger = logging.getLogger(__name__)
_logics_instance = None # Pointer to the initialized instance of the Logics class (for use by static methods)
[Doku]class Logics():
"""
This is the main class for the implementation og logics in SmartHomeNG. It implements the API for the
handling of those logics.
"""
plugins = None
scheduler = None
_config_type = None
_logicname_prefix = 'logics.' # prefix for scheduler names
_groups = {}
def __init__(self, smarthome, userlogicconf, envlogicconf):
logger.info('Start Logics')
self.shtime = Shtime.get_instance()
self.items = Items.get_instance()
self.plugins = Plugins.get_instance()
self.scheduler = Scheduler.get_instance()
self._sh = smarthome
self._userlogicconf = userlogicconf
self._env_dir = smarthome._env_dir
self._envlogicconf = envlogicconf
self._etc_dir = smarthome.get_config_dir(DIR_ETC)
self._logic_dir = smarthome.get_config_dir(DIR_LOGICS)
self._logic_conf = smarthome.get_config_file(BASE_LOGIC)
self._workers = []
self._logics = {}
#self._bytecode = {}
self.alive = True
global _logics_instance
if _logics_instance is not None:
import inspect
curframe = inspect.currentframe()
calframe = inspect.getouterframes(curframe, 4)
logger.critical("A second 'logics' object has been created. There should only be ONE instance of class 'Logics'!!! Called from: {} ({})".format(calframe[1][1], calframe[1][3]))
_logics_instance = self
self.scheduler = Scheduler.get_instance()
_config = {}
self._systemlogics = self._read_logics(envlogicconf, self._env_dir)
_config.update(self._systemlogics)
self._userlogics = self._read_logics(userlogicconf, self._logic_dir)
_config.update(self._userlogics)
for name in _config:
if name != '_groups':
self._load_logic(name, _config)
# load /etc/admin.yaml
admconf_filename = self._sh.get_config_file(BASE_ADMIN)
_admin_conf = shyaml.yaml_load_roundtrip(admconf_filename)
if _admin_conf.get('logics', None) is None:
self._groups = {}
else:
self._groups = _admin_conf['logics']['groups']
def _save_groups(self):
# load /etc/admin.yaml
admconf_filename = self._sh.get_config_file(BASE_ADMIN)
_admin_conf = shyaml.yaml_load_roundtrip(admconf_filename)
_admin_conf['logics']['groups'] = self._groups
shyaml.yaml_save_roundtrip(admconf_filename, _admin_conf, create_backup=True)
def _read_logics(self, filename, directory):
"""
Read the logics configuration file
:param filename: name of the logics configurtion file
:param directory: directory where the logics are stored
"""
logger.debug("Reading Logics from {}.*".format(filename))
config = lib.config.parse_basename(filename, configtype='logics')
if config != {}:
if os.path.isfile(filename+YAML_FILE):
self._config_type = YAML_FILE
else:
self._config_type = CONF_FILE
for name in config:
if 'filename' in config[name]:
config[name]['pathname'] = directory + config[name]['filename']
return config
def _load_logic(self, name, config):
"""
Load a logic, specified by section name in config
"""
# logger.debug("_load_logic: Logics.is_logic_loaded(name) = {}.".format( str(Logics.is_logic_loaded(name)) ))
if self.is_logic_loaded(name):
return False
logger.debug("Logic: {}".format(name))
logic = Logic(self._sh, name, config[name], self)
if hasattr(logic, '_bytecode'):
self._logics[name] = logic
self.scheduler.add(self._logicname_prefix+name, logic, logic._prio, logic._crontab, logic._cycle)
else:
return False
# plugin hook
# for plugin in self._sh._plugins:
for plugin in self.plugins.return_plugins():
if hasattr(plugin, PLUGIN_PARSE_LOGIC):
update = plugin.parse_logic(logic)
if update:
logic.add_method_trigger(update)
# item hook
if hasattr(logic, 'watch_item'):
if isinstance(logic.watch_item, str):
logic.watch_item = [logic.watch_item]
for entry in logic.watch_item:
# for item in self._sh.match_items(entry):
for item in self.items.match_items(entry):
item.add_logic_trigger(logic)
return True
def __iter__(self):
for logic in self._logics:
yield logic
def __getitem__(self, name):
if name in self._logics:
return self._logics[name]
def _delete_logic(self, name):
if name in self._logics:
del self._logics[name]
[Doku] def return_logics(self):
"""
Returns a list with the names of all loaded logics
:return: list of logic names
:rtype: list
"""
for logic in self:
yield logic
[Doku] def get_loaded_logics(self):
"""
Returns a list with the names of all loaded logics
:return: list of logic names
:rtype: list
"""
logics = []
for logic in self:
logics.append(logic)
return sorted(logics)
# ------------------------------------------------------------------------------------
# Following (static) methods of the class Logics implement the API for logics in shNG
# ------------------------------------------------------------------------------------
[Doku] @staticmethod
def get_instance():
"""
Returns the instance of the Logics class, to be used to access the logics-api
Use it the following way to access the api:
.. code-block:: python
from lib.logic import Logics
logics = Logics.get_instance()
# to access a method (eg. enable_logic()):
logics.enable_logic(name)
:return: logics instance
:rtype: object or None
"""
if _logics_instance == None:
return None
else:
return _logics_instance
[Doku] def scheduler_add(self, name, obj, prio=3, cron=None, cycle=None, value=None, offset=None, next=None):
"""
This methods adds a scheduler entry for a logic-scheduler
A plugin identifiction is added to the scheduler name
The parameters are identical to the scheduler.add method from lib.scheduler
"""
if name != '':
name = '.'+name
name = self._logicname_prefix+self.get_fullname()+name
logger.debug("scheduler_add: name = {}".format(name))
self.scheduler.add(name, obj, prio, cron, cycle, value, offset, next, from_smartplugin=True)
[Doku] def scheduler_change(self, name, **kwargs):
"""
This methods changes a scheduler entry of a logic-scheduler
"""
if name != '':
name = '.'+name
name = self._logicname_prefix+self.get_fullname()+name
logger.debug("scheduler_change: name = {}".format(name))
self.scheduler.change(name, kwargs)
[Doku] def scheduler_remove(self, name):
"""
This methods rmoves a scheduler entry of a logic-scheduler
A plugin identifiction is added to the scheduler name
The parameters are identical to the scheduler.remove method from lib.scheduler
"""
if name != '':
name = '.'+name
name = self._logicname_prefix+self.get_fullname()+name
logger.debug("scheduler_remove: name = {}".format(name))
self.scheduler.remove(name, from_smartplugin=False)
[Doku] def get_logics_dir(self):
"""
Returns the path of the dirctory, where the user-logics are stored
:return: path to logics directory
:rtype: str
"""
return self._logic_dir
def _get_etc_dir(self):
"""
Returns the path of the dirctory, where the SmartHomeNG configuration (/etc) is stored
It is not a public method because handling of the configuration file /etc/logic.yaml
should be done by the api implementation. Only special plugins should access the
files in /etc themself.
:return: path to SmartHomeNG configuration directory
:rtype: str
"""
return self._etc_dir
def _get_logic_conf_basename(self):
"""
Returns the basename of the logic configuration file
"""
# return self._sh._logic_conf_basename
return self._userlogicconf
[Doku] def reload_logics(self):
"""
Function to reload all logics
It generates new bytecode for every logic that is loaded. The configured triggers
are not loaded from the configuration, so the triggers that where active before the
reload remain active.
"""
for logic in self:
self[logic]._generate_bytecode()
[Doku] def is_logic_loaded(self, name):
"""
Test if a logic is loaded. Given is the name of the section in /etc/logic.yaml
:param name: logic name (name of the configuration section section)
:type name: str
:return: True: Logic is loaded
:rtype: bool
"""
if self.return_logic(name) == None:
return False
else:
return True
[Doku] def return_logic(self, name):
"""
Returns (the object of) one loaded logic with given name
:param name: name of the logic to get
:type name: str
:return: object of the logic
:rtype: object
"""
return self[name]
[Doku] def get_logic_info(self, name, ordered=False):
"""
Returns a dict with information about the logic
:param name: name of the logic to get info for
:type name: str
:return: information about the logic
:rtype: dict
"""
if ordered:
info = OrderedDict()
else:
info = {}
logic = self.return_logic(name)
if logic == None:
return info
info['name'] = logic._name
info['enabled'] = logic._enabled
if self.scheduler.return_next(self._logicname_prefix+logic.name):
info['next_exec'] = self.scheduler.return_next(self._logicname_prefix+logic.name).strftime('%Y-%m-%d %H:%M:%S%z')
info['cycle'] = logic._cycle
info['crontab'] = logic._crontab
try:
info['watch_item'] = logic.watch_item
except:
info['watch_item'] = ''
info['userlogic'] = self.is_userlogic(logic.name)
info['logictype'] = self.return_logictype(logic.name)
info['filename'] = logic._filename
info['pathname'] = logic._pathname
try:
info['description'] = logic.description
except:
info['description'] = ''
info['visu_access'] = self.visu_access(logic.name)
# info['watch_item_list'] = []
return info
[Doku] def visu_access(self, name):
"""
Return if visu may access the logic
"""
try:
if self.return_logic(name).visu_acl.lower() in ('true', 'yes', 'rw'):
return True
except Exception as e:
pass
return False
[Doku] def is_logic_enabled(self, name):
"""
Returns True, if the logic is enabled
"""
mylogic = self.return_logic(name)
if mylogic is None:
logger.warning("logics.is_logic_enabled: No logic found with name {}".format(name))
return False
return mylogic.is_enabled()
[Doku] def enable_logic(self, name):
"""
Enable a logic
"""
mylogic = self.return_logic(name)
if mylogic is None:
logger.warning("logics.enable_logic: No logic found with name {}".format(name))
return False
mylogic.enable()
# self.set_config_section_key(name, 'enabled', True)
self.set_config_section_key(name, 'enabled', None)
return mylogic._enabled
[Doku] def disable_logic(self, name):
"""
Disable a logic
"""
mylogic = self.return_logic(name)
if mylogic is None:
logger.warning("logics.disable_logic: No logic found with name {}".format(name))
return False
mylogic.disable()
self.set_config_section_key(name, 'enabled', False)
return mylogic._enabled
[Doku] def toggle_logic(self, name):
"""
Toggle a logic (Invert the enabled/disabled state)
"""
mylogic = self.return_logic(name)
if mylogic is None:
logger.warning("logics.toggle_logic: No logic found with name {}".format(name))
return False
if mylogic._enabled:
mylogic.disable()
else:
logger.info("toggle_logic: name = {}".format(name))
mylogic.enable()
return mylogic._enabled
[Doku] def trigger_logic(self, name, by='unknown', source=None, value=None):
"""
Trigger a logic
"""
logger.debug("trigger_logic: Trigger logic = '{}'".format(name))
if name in self.return_loaded_logics():
if by == 'unknown':
by = 'Backend'
self.scheduler.trigger(self._logicname_prefix+name, by=by, source=source, value=value)
else:
logger.warning("trigger_logic: Logic '{}' not found/loaded".format(name))
[Doku] def is_userlogic(self, name):
"""
Returns True if userlogic and False if systemlogic or unknown
"""
try:
pathname = str(self.return_logic(name)._pathname)
except:
return False
return os.path.basename(os.path.dirname(pathname)) == DIR_LOGICS
[Doku] def load_logic(self, name):
"""
Load a specified logic
Load a logic as defined in the configuration section. After loading the logic's code,
the defined schedules and/or triggers are set.
If a logic is already loaded, it is unloaded and then reloaded.
:param name: Name of the logic (name of the configuration section)
:type name: str
:return: Success
:rtype: bool
"""
logger.info("load_logics: Start")
if self.is_logic_loaded(name):
self.unload_logic(name)
_config = self._read_logics(self._get_logic_conf_basename(), self.get_logics_dir())
if not (name in _config):
logger.warning("load_logic: FAILED: Logic '{}', _config = {}".format( name, str(_config) ))
logger.info("load_logics: Failed")
return False
logger.info("load_logic: Logic '{}', _config = {}".format( name, str(_config) ))
logger.info("load_logics: End")
return self._load_logic(name, _config)
[Doku] def unload_logic(self, name):
"""
Unload a specified logic
This function unloads a logic. Before unloading, it remove defined schedules and triggers for ``watch_item`` s.
:param name: Name of the section that defines the logic in the configuration file
:type name: str
"""
logger.info("Unload Logic: {}".format(name))
mylogic = self.return_logic(name)
if mylogic == None:
return False
mylogic._enabled = False
mylogic._cycle = None
mylogic._crontab = None
# Scheduler entfernen
self.scheduler.remove(self._logicname_prefix+name)
# watch_items entfernen
if hasattr(mylogic, 'watch_item'):
if isinstance(mylogic.watch_item, str):
mylogic.watch_item = [mylogic.watch_item]
for entry in mylogic.watch_item:
# item hook
# for item in self._sh.match_items(entry):
for item in self.items.match_items(entry):
try:
item.remove_logic_trigger(mylogic)
except:
logger.error("unload_logic: logic = '{}' - cannot remove logic_triggers".format(name))
mylogic.watch_item = []
self._delete_logic(name)
return True
[Doku] def get_logiccrontab(self, name):
"""
Return the crontab string of a logic
"""
logger.debug("get_logiccrontab: Get crontab of logic = '{}'".format(name))
mylogic = self.return_logic(name)
if mylogic is None:
return None
else:
return mylogic._crontab
[Doku] def return_logictype(self, name):
"""
Returns the type of a specified logic (Python, Blockly, None)
:param name: Name of the logic (name of the configuration section)
:type name: str
:return: Logic type ('Python', 'Blockly' or None)
:rtype: str or None
"""
logic_type = 'None'
filename = ''
if name in self._userlogics:
try:
filename = self._userlogics[name].get('filename', '')
except:
logger.warning("return_logictype: self._userlogics[name] = '{}'".format(str(self._userlogics[name])))
logger.warning("return_logictype: self._userlogics = '{}'".format(str(self._userlogics)))
elif name in self._systemlogics:
filename = self._systemlogics[name].get('filename', '')
logic_type = 'Python'
else:
logger.info("return_logictype: name {} is not loaded".format(name))
# load /etc/logic.yaml if logic is not in the loaded logics
config = shyaml.yaml_load_roundtrip(self._logic_conf)
if config is not None:
if name in config:
filename = config[name].get('filename', '')
if filename != '':
blocklyname = os.path.splitext(os.path.basename(filename))[0]+'.blockly'
if os.path.isfile(os.path.join(self.get_logics_dir(), filename)):
logic_type = 'Python'
if os.path.isfile(os.path.join(self.get_logics_dir(), blocklyname)):
logic_type = 'Blockly'
logger.debug("return_logictype: name '{}', filename '{}', logic_type '{}'".format(name, filename, logic_type))
return logic_type
[Doku] def return_defined_logics(self, withtype=False):
"""
Returns the names of defined logics from file /etc/logic.yaml as a list
If ``withtype`` is specified and set to True, the function returns a dict with names and
logictypes ('Python', 'Blockly')
:param withtype: If specified and set to True, the function will additionally return the logic types
:type withtype: bool
:return: list of defined logics or dict of defined logics with type
:rtype: list or dict
"""
if withtype:
logic_list = {}
else:
logic_list = []
# load /etc/logic.yaml
config = shyaml.yaml_load_roundtrip(self._logic_conf)
if config is not None:
for section in config:
if section != '_groups':
logic_dict = {}
filename = config[section]['filename']
blocklyname = os.path.splitext(os.path.basename(filename))[0]+'.xml'
logic_type = 'None'
if os.path.isfile(os.path.join(self.get_logics_dir(), filename)):
logic_type = 'Python'
if os.path.isfile(os.path.join(self.get_logics_dir(), blocklyname)):
logic_type = 'Blockly'
logger.debug(f"return_defined_logics: section '{section}', logic_type '{logic_type}'")
if withtype:
logic_list[section] = logic_type
else:
logic_list.append(section)
return logic_list
[Doku] def return_loaded_logics(self):
"""
Returns a list with the names of all logics that are currently loaded
:return: list of logic names
:rtype: list
"""
logic_list = []
for logic in self._logics:
logic_list.append(logic)
return logic_list
[Doku] def return_config_type(self):
"""
Return the used config type
After initialization this function returns '.conf', if the used logic configuration file in /etc
is in the old file format or '.yaml' if the used configuration file is in YAML format.
To use the following functions for reading and manipulating the logic configuration, the
configuration file **has to be** in YAML format. Otherwise the functions will not work/return empty results.
:return: '.yaml', '.conf' or None
:rtype: str or None
"""
return self._config_type
[Doku] def read_config_section(self, section):
"""
Read a section from /etc/logic.yaml
This funtion returns the data from one section of the configuration file as a list of
configuration entries. A configuration entry is a list with three items:
- key configuration key
- value configuration value (string or list)
- comment comment for the value (string or list)
:param section: Name of the logic (section)
:type section: str
:return: config_list: list of configuration entries. Each entry of this list is a list with three string entries: ['key', 'value', 'comment']
:rtype: list of lists
"""
if self.return_config_type() != YAML_FILE:
logger.error("read_config_section: Editing of configuration only possible with new (yaml) config format")
return False
# load /etc/logic.yaml
_conf = shyaml.yaml_load_roundtrip(self._logic_conf)
config_list = []
if _conf is not None:
section_dict = _conf.get(section, {})
# logger.warning("read_config_section: read_config_section('{}') = {}".format(section, str(section_dict) ))
for key in section_dict:
if isinstance(section_dict[key], list):
value = section_dict[key]
comment = [] # 'Comment 6: ' + loaded['a']['c'].ca.items[0][0].value 'Comment 7: ' + loaded['a']['c'].ca.items[1][0].value
for i in range(len(value)):
if i in section_dict[key].ca.items:
try:
c = section_dict[key].ca.items[i][0].value
except:
logger.info("c: {}, Key: {}".format(c, key))
c = ''
if len(c) > 0 and c[0] == '#':
c = c[1:]
else:
c = ''
comment.append(c.strip())
else:
value = section_dict[key]
c = ''
if key in section_dict.ca.items:
try:
c = section_dict.ca.items[key][2].value # if not list: loaded['a'].ca.items['b'][2].value
except:
logger.info("c2: {}, Key: {}".format(c, key))
if len(c) > 0 and c[0] == '#':
c = c[1:]
comment = c.strip()
# logger.warning("-> read_config_section: section_dict['{}'] = {}, comment = '{}'".format(key, str(section_dict[key]), comment ))
config_list.append([key, value, comment])
return config_list
[Doku] def set_config_section_key(self, section, key, value):
"""
Sets the value of key for a given logic (section)
:param section: logic to set the key for
:param key: key for which the value should be set
:param value: value to set
"""
# load /etc/logic.yaml
conf = shyaml.yaml_load_roundtrip(self._logic_conf)
logger.info("set_config_section_key: section={}, key={}, value={}".format(section, key, str(value)))
if value == None:
if conf[section].get(key, None) != None:
del conf[section][key]
else:
conf[section][key] = value
# save /etc/logic.yaml
shyaml.yaml_save_roundtrip(self._logic_conf, conf, True)
# activate visu_acl without reloading the logic
if key == 'visu_acl':
mylogic = self.return_logic(section)
if mylogic is not None:
logger.info(" - key={}, value={}".format(key, value))
# if value is None:
# value = 'false'
mylogic.visu_acl = str(value)
return
[Doku] def update_config_section(self, active, section, config_list):
"""
Update file /etc/logic.yaml
This method creates/updates a section in /etc/logic.yaml. If the section exist, it is cleared
before new configuration imformation is written to the section
:param active: True: logic is/should be active, False: Triggers are not written to /etc/logic.yaml
:param section: name of section to configure in logics configurationfile
:param config_list: list of configuration entries. Each entry of this list is a list with three string entries: ['key', 'value', 'comment']
:type active: bool
:type section: str
:type config_list: list of lists
"""
if section == '':
logger.error("update_config_section: No section name specified. Not updatind logics configuration.")
return False
if self.return_config_type() != YAML_FILE:
logger.error("update_config_section: Editing of configuration only possible with new (yaml) config format")
return False
# load /etc/logic.yaml
conf = shyaml.yaml_load_roundtrip(self._logic_conf)
if conf is None:
conf = shyaml.get_emptynode()
# empty section
if conf.get(section, None) == None:
conf[section] = shyaml.get_emptynode()
if conf[section].get('filename', None) != None:
del conf[section]['filename']
if conf[section].get('cycle', None) != None:
del conf[section]['cycle']
if conf[section].get('crontab', None) != None:
del conf[section]['crontab']
if conf[section].get('watch_item', None) != None:
del conf[section]['watch_item']
# add entries to section
logger.info("update_config_section: section {}".format(section))
for c in config_list:
# process config entries
key = c[0].strip()
value = c[1]
comment = c[2]
logger.info(" - key={}, value={}, comment={}".format(key, str(value), str(comment)))
if isinstance(value, str):
value = value.strip()
comment = comment.strip()
if value != '' and value[0] == '[' and value[-1] == ']':
# convert a list of triggers to list, if given as a string
value = ast.literal_eval(value)
if comment != '':
comment = ast.literal_eval(comment)
else:
# process single trigger
if active or (key == 'filename'):
conf[section][key] = value
if comment != '':
conf[section].yaml_add_eol_comment(comment, key, column=50)
elif isinstance(value, int) or isinstance(value, bool) or isinstance(value, float):
comment = comment.strip()
# process single trigger
if active:
conf[section][key] = value
if comment != '':
conf[section].yaml_add_eol_comment(comment, key, column=50)
else:
logger.warning("update_config_section: unsupported datatype for key '{}'".format(key))
if active:
if isinstance(value, list):
# process a list of triggers
conf[section][key] = shyaml.get_commentedseq(value)
listvalue = True
for i in range(len(value)):
if comment != '':
if comment[i] != '':
conf[section][key].yaml_add_eol_comment(comment[i], i, column=50)
if conf[section] == shyaml.get_emptynode():
conf[section] = None
shyaml.yaml_save_roundtrip(self._logic_conf, conf, True)
def _count_filename_uses(self, conf, filename):
"""
Count the number of logics (sections) that reference this filename
"""
count = 0
if conf:
for name in conf:
section = conf.get(name, None)
fn = section.get('filename', None)
if fn is not None and fn.lower() == filename.lower():
count += 1
return count
[Doku] def filename_used_count(self, filename):
# load /etc/logic.yaml
conf = shyaml.yaml_load_roundtrip(self._logic_conf)
count = self._count_filename_uses(conf, filename)
return count
[Doku] def delete_logic(self, name, with_code=False):
"""
Deletes a complete logic
The python code and the section from the configuration file /etc/logic.yaml are
removed. If it is a blockly logic, the blockly code is removed too.
If a code file is references by more than the logic that is being deleted, the code
file will not be deleted. It will only be deleted when the last logic referencing
this code file is being deleted.
:param name: name of the logic
:type name: str
:return: True, if deletion fas successful
:rtype: bool
"""
#logger.notice(f"delete_logic: This routine implements the deletion of logic '{name}' with_code={with_code} (still in testing)")
# Logik entladen
if self.is_logic_loaded(name):
logger.info(f"delete_logic: Logic '{name}' unloaded")
self.unload_logic(name)
# load /etc/logic.yaml
conf = shyaml.yaml_load_roundtrip(self._logic_conf)
section = conf.get(name, None)
if section is None:
logger.warning(f"delete_logic: Section '{name}' not found in logic configuration.")
return False
# delete code file in ../logics
filename = section.get('filename', None)
if filename is None:
logger.warning(f"delete_logic: Filename of logic is not defined in section '{name}' of logic configuration.")
else:
count = self._count_filename_uses(conf, filename)
blocklyname = os.path.join(self.get_logics_dir(), os.path.splitext(os.path.basename(filename))[0]+'.blockly')
filename = os.path.join(self._logic_dir, filename)
if count < 2:
# Deletion of the parts of the logic
if with_code:
if os.path.isfile(blocklyname):
os.remove(blocklyname)
logger.warning(f"delete_logic: Blockly-Logic file '{blocklyname}' deleted")
if os.path.isfile(filename):
os.remove(filename)
logger.info(f"delete_logic: Logic file '{filename}' deleted")
else:
logger.warning(f"delete_logic: Skipped deletion of logic file '{filename}' because it is used by {count-1} other logic(s)")
# delete logic configuration from ../etc/logic.yaml
if conf.get(name, None) is not None:
del conf[name]
logger.info(f"delete_logic: Section '{name}' from configuration deleted")
# save /etc/logic.yaml
shyaml.yaml_save_roundtrip(self._logic_conf, conf, True)
return True
# ------------------------------------------------------------------------------------
# Class Logic
# ------------------------------------------------------------------------------------
[Doku]class Logic():
"""
Class for the representation of a loaded logic
"""
_logicname_prefix = 'logics.'
def __init__(self, smarthome, name, attributes, logics):
self.sh = smarthome # initialize to use 'logic.sh' in logics
self.logger = logger # initialize to use 'logic.logger' in logics
self._logic_groupnames = []
self._name = name
self._logic_description = ''
self.shtime = logics.shtime
self._logics = logics # access to the logics api
self._enabled = True if 'enabled' not in attributes else Utils.to_bool(attributes['enabled'])
#self.enabled = self._enabled
self._crontab = None
self._cycle = None
self._prio = 3
#self.last = None
self._last_run = None
self._trigger_dict = None
self._watch_item = []
self._conf = attributes
self.scheduler = Logics.get_instance().scheduler
self.__methods_to_trigger = []
if attributes != 'None':
# Fills crontab, cycle and other parameters
for attribute in attributes:
if attribute == 'logic_groupname':
if isinstance(attributes[attribute], list):
vars(self)['_logic_groupnames'] = attributes[attribute]
else:
vars(self)['_logic_groupnames'] = [attributes[attribute]]
if attribute == 'logic_description':
vars(self)['_logic_description'] = attributes[attribute]
if attribute == 'pathname':
vars(self)['_pathname'] = attributes[attribute]
elif attribute == 'filename':
vars(self)['_filename'] = attributes[attribute]
elif attribute == 'watch_item':
vars(self)['_watch_item'] = attributes[attribute]
elif attribute == 'cycle':
vars(self)['_cycle'] = attributes[attribute]
elif attribute == 'crontab':
vars(self)['_crontab'] = attributes[attribute]
elif attribute != 'enabled':
vars(self)[attribute] = attributes[attribute]
self._prio = int(self._prio)
self._generate_bytecode()
else:
self.logger.error("Logic {} is not configured correctly (configuration has no attibutes)".format(self._name))
[Doku] def id(self):
"""
Returns the id of the loaded logic
"""
return self._name
def __str__(self):
return self._name
def __call__(self, caller='Logic', source=None, value=None, dest=None, dt=None):
if self._enabled:
self.scheduler.trigger(self._logicname_prefix+self._name, self, prio=self._prio, by=caller, source=source, dest=dest, value=value, dt=dt)
@property
def name(self):
"""
Property: name
:param value: name of the logic
:type value: str
:return: name of the logic
:rtype: str
"""
return self._name
@name.setter
def name(self, value):
self.logger.warning(f"'logic.name' is a readonly property and the value '{value}' can not be assigned to it")
#if not isinstance(value, str):
# self._cast_warning(value)
# value = '{}'.format(value)
#if value == '':
# self._item._name = self._item._path
#else:
# self._item._name = value
return
@property
def groupnames(self):
"""
Property: groupname
:param value: groupname of the logic
:type value: str
:return: groupname of the logic
:rtype: str
"""
return self._logic_groupnames
@groupnames.setter
def groupnames(self, value):
# self.logger.warning(f"'logic.groupnames' is a readonly property and the value '{value}' can not be assigned to it")
if not isinstance(value, (list, str)):
self.logger.warning(f"'logic.groupnames': Only a string or a list can be assigned to - '{value}' can not be assigned to it")
else:
self._logic_groupnames = value
return
@property
def description(self):
"""
Property: groupname
:param value: description of the logic
:type value: str
:return: description of the logic
:rtype: str
"""
return self._logic_description
@description.setter
def description(self, value):
# self.logger.warning(f"'logic.description' is a readonly property and the value '{value}' can not be assigned to it")
if not isinstance(value, str):
self.logger.warning(f"'logic.description': Only a string or a list can be assigned to - '{value}' can not be assigned to it")
else:
self._logic_description = value
return
[Doku] def log_readonly_warning(self, prop, value):
self.logger.warning(f"'logic.{prop}' is a readonly property and the value '{value}' can not be assigned to it")
@property
def lname(self):
"""
Property: lname
:param value: string with the name of the logic for information in value assignements to items
:type value: str
:return: name of the item
:rtype: str
"""
return "Logic ' "+self._name+"'" # string is to be used in item assignements sh.xxx(<value>, logic.lname)
@lname.setter
def lname(self, value):
self.log_readonly_warning('lname', value)
return
@property
def filename(self):
"""
Property: filename
:return: filename of the logic
:rtype: str
"""
return self._filename
@filename.setter
def filename(self, value):
self.log_readonly_warning('filename', value)
return
@property
def pathname(self):
"""
Property: pathname
:return: pathname of the logic
:rtype: str
"""
return self._pathname
@pathname.setter
def pathname(self, value):
self.log_readonly_warning('pathname', value)
return
@property
def conf(self):
"""
Property: conf
:return: conf of the logic
:rtype: collections.OrderedDict
"""
return self._conf
@conf.setter
def conf(self, value):
self.log_readonly_warning('conf', value)
return
@property
def cycle(self):
"""
Property: cycle
:return: cycle attribute of the logic
:rtype: str
"""
return self._cycle
@cycle.setter
def cycle(self, value):
self.log_readonly_warning('cycle', value)
return
@property
def crontab(self):
"""
Property: crontab
:return: crontab attribute of the logic
:rtype: str
"""
return self._crontab
@crontab.setter
def crontab(self, value):
self.log_readonly_warning('crontab', value)
return
@property
def prio(self):
"""
Property: prio
:return: prio attribute of the logic
:rtype: str
"""
return self._prio
@prio.setter
def prio(self, value):
#self.log_readonly_warning('prio', value)
self._prio = value
return
@property
def trigger_dict(self):
"""
Property: trigger_dict
:return: trigger_dict attribute of the logic
:rtype: dict
"""
return self._trigger_dict
@trigger_dict.setter
def trigger_dict(self, value):
#self.log_readonly_warning('trigger_dict', value)
self._trigger_dict = value
return
@property
def watch_item(self):
"""
Property: watch_item
:return: watch_item attribute of the logic
:rtype: list
"""
return self._watch_item
@watch_item.setter
def watch_item(self, value):
#self.log_readonly_warning('watch_item', value)
self._watch_item = value
return
[Doku] def enable(self):
"""
Enables the loaded logic
"""
self._enabled = True
[Doku] def disable(self):
"""
Disables the loaded logic
"""
self._enabled = False
[Doku] def is_enabled(self):
"""
Is the loaded logic enabled?
"""
return self._enabled
[Doku] def last_run(self):
"""
Returns the timestamp of the last run of the logic or None (if the logic wasn't triggered)
:return: timestamp of last run
:rtype: datetime timestamp
"""
return self._last_run
[Doku] def set_last_run(self):
"""
Sets the timestamp of the last run of the logic to now
This method is called by the scheduler
"""
# self._last_run = self._sh.now()
self._last_run = self.shtime.now()
[Doku] def trigger(self, by='Logic', source=None, value=None, dest=None, dt=None):
if self._enabled:
self.scheduler.trigger(self._logicname_prefix+self._name, self, prio=self._prio, by=by, source=source, dest=dest, value=value, dt=dt)
else:
self.logger.info("trigger: Logic '{}' not triggered because it is disabled".format(self._name))
def _generate_bytecode(self):
if hasattr(self, '_pathname'):
if not os.access(self._pathname, os.R_OK):
self.logger.warning("{}: Could not access logic file ({}) => ignoring.".format(self._name, self._pathname))
return
try:
f = open(self._pathname, encoding='UTF-8')
code = f.read()
f.close()
code = code.lstrip('\ufeff') # remove BOM
self._bytecode = compile(code, self._pathname, 'exec')
except Exception as e:
self.logger.exception("Exception: {}".format(e))
else:
self.logger.warning("{}: No pathname specified => ignoring.".format(self._name))
[Doku] def add_method_trigger(self, method):
self.__methods_to_trigger.append(method)
[Doku] def get_method_triggers(self):
return self.__methods_to_trigger