'.format(conditionset)
tooltip_count = 0
for k, condition in enumerate(self.__states[state]['conditionsets'].get(conditionset)):
condition_dict = self.__states[state]['conditionsets'][conditionset].get(condition)
current = condition_dict.get('current')
match = condition_dict.get('match')
-
status_none = str(condition_dict.get('status')) == 'None'
item_none = str(condition_dict.get('item')) == 'None' or not status_none
status_eval_none = condition_dict.get('status_eval') == 'None'
@@ -224,24 +188,21 @@ def _conditionlabel(self, state, conditionset, i):
'updatedbynegate', 'triggeredbynegate', 'status', 'current', 'match', 'status_eval']
if cond1 and compare not in excluded_values:
- try:
- list_index = list(self.__states.keys()).index(self.__active_state)
- except Exception:
- list_index = 0
if condition not in conditions_done:
current_clean = ", ".join(f"{k} = {v}" for k, v in current.items())
- text = " Current {}".format(current_clean) if current and len(current) > 0 else " Not evaluated."
+ text = " Current {}".format(current_clean) if current is not None and len(current) > 0 else " Not evaluated."
conditionlist += ('
'
- info_status = str(condition_dict.get('status') or '')
- info_item = str(condition_dict.get('item') or '')
- info_eval = str(condition_dict.get('eval') or '')
- info_status_eval = str(condition_dict.get('status_eval') or '')
+ info_status = str(condition_dict.get('status') or 'n/a')
+ info_item = str(condition_dict.get('item') or 'n/a')
+ info_eval = str(condition_dict.get('eval') or 'n/a')
+ info_status_eval = str(condition_dict.get('status_eval') or 'n/a')
info_compare = str(condition_dict.get(compare) or '')
+ info_compare = self._strip_regex(info_compare)
if not status_none:
textlength = len(info_status)
if textlength > self.__textlimit:
@@ -294,7 +255,8 @@ def _conditionlabel(self, state, conditionset, i):
elif not item_none:
info = info_item
else:
- info = ""
+ info = "n/a"
+
conditionlist += '{}
'.format(info)
comparison = ">=" if not min_none and compare == "min"\
else "<=" if not max_none and compare == "max"\
@@ -332,10 +294,9 @@ def _conditionlabel(self, state, conditionset, i):
conditionlist += ' (negate)' if condition_dict.get('negate') == 'True' and "age" \
not in compare and not compare == "value" else ''
conditionlist += ' (negate)' if condition_dict.get('agenegate') == 'True' and "age" in compare else ''
- active = i < list_index or (i == list_index and conditionset in ['', self.__active_conditionset])
- match_info = '' if match_info == 'yes' and active\
- else '' if match_info == 'no' and active\
- else '' if match_info and len(match_info) > 0 and active\
+ match_info = '' if match_info == 'yes'\
+ else '' if match_info == 'no'\
+ else '' if match_info and len(match_info) > 0 \
else ''
conditionlist += '
{}
'.format(match_info)
conditionlist += '
>'
@@ -344,16 +305,16 @@ def _conditionlabel(self, state, conditionset, i):
def _add_actioncondition(self, state, conditionset, action_type, new_y, cond1, cond2):
cond4 = conditionset in ['', self.__active_conditionset] and state == self.__active_state
cond5 = self.__states[state]['conditionsets'].get(conditionset) is not None
- cond_enter = action_type == 'actions_enter' and self.__states[state].get('enter') is False
- cond_stay = action_type == 'actions_stay' and self.__states[state].get('stay') is False
+ cond_enter = action_type in ['actions_enter', 'actions_enter_or_stay'] and self.__states[state].get('enter') is False
+ cond_stay = action_type in ['actions_stay', 'actions_enter_or_stay'] and self.__states[state].get('stay') is False
color_enter = "gray" if (cond1 and cond2 and cond5) or \
(cond_enter and cond4 and cond5) else "olivedrab" if cond4 else "indianred2"
color_stay = "gray" if (cond1 and cond2 and cond5) or \
(cond_stay and cond4 and cond5) else "olivedrab" if cond4 else "indianred2"
- label = 'first enter' if action_type == 'actions_enter' else 'staying at state'
+ label = 'first enter' if action_type in ['actions_enter', 'actions_enter_or_stay'] else 'staying at state'
- position = '{},{}!'.format(0.63, new_y)
+ position = '{},{}!'.format(0.38, new_y)
color = color_enter if label == 'first enter' else color_stay
self.__nodes['{}_{}_state_{}'.format(state, conditionset, action_type)] = \
pydotplus.Node('{}_{}_state_{}'.format(state, conditionset, action_type), style="filled", fillcolor=color,
@@ -386,8 +347,6 @@ def drawgraph(self, filename):
new_y = 2
previous_state = ''
previous_conditionset = ''
- previousconditionset = ''
- previousstate_conditionset = ''
for i, state in enumerate(self.__states):
#self._log_debug('Adding state for webif {}', self.__states[state])
if isinstance(self.__states[state], (OrderedDict, dict)):
@@ -404,13 +363,14 @@ def drawgraph(self, filename):
new_y -= 1 * self.__scalefactor
position = '{},{}!'.format(0, new_y)
if not i == 0:
- condition_node = 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \
- else list(self.__states[previous_state]['conditionsets'].keys())[-1]
+ condition_node = 'pass' if self.__nodes.get('{}_pass'.format(previous_state)) \
+ else 'leave' if self.__nodes.get('{}_leave'.format(previous_state)) \
+ else list(self.__states[previous_state]['conditionsets'].keys())[-1]
lastnode = self.__nodes['{}_{}'.format(previous_state, condition_node)]
self.__nodes['{}_above'.format(state)] = pydotplus.Node('{}_above'.format(state), pos=position,
shape="square", width="0", label="")
self.__graph.add_node(self.__nodes['{}_above'.format(state)])
- position = '{},{}!'.format(0.5, new_y)
+ position = '{},{}!'.format(0.3, new_y)
self.__nodes['{}_above_right'.format(state)] = pydotplus.Node('{}_above_right'.format(state),
pos=position, shape="square", width="0", label="")
self.__graph.add_node(self.__nodes['{}_above_right'.format(state)])
@@ -430,7 +390,7 @@ def drawgraph(self, filename):
label='<
{}
'
'
{}
>'.format(
state, self.__states[state]['name']))
- position = '{},{}!'.format(0.5, new_y)
+ position = '{},{}!'.format(0.3, new_y)
self.__nodes['{}_right'.format(state)] = pydotplus.Node('{}_right'.format(state), pos=position,
shape="square", width="0", label="")
self.__graph.add_node(self.__nodes[state])
@@ -439,66 +399,82 @@ def drawgraph(self, filename):
actionlist_enter = ''
actionlist_stay = ''
actionlist_leave = ''
+ actionlist_pass = ''
condition_tooltip = ''
action_tooltip = ''
j = 0
- new_x = 0.9
+ new_x = 0.55
actions_enter = self.__states[state].get('actions_enter') or []
actions_enter_or_stay = self.__states[state].get('actions_enter_or_stay') or []
actions_stay = self.__states[state].get('actions_stay') or []
actions_leave = self.__states[state].get('actions_leave') or []
+ actions_pass = self.__states[state].get('actions_pass') or []
action_tooltip_count_enter = 0
action_tooltip_count_stay = 0
action_tooltip_count_leave = 0
+ action_tooltip_count_pass = 0
action_tooltip_enter = ""
action_tooltip_stay = ""
action_tooltip_leave = ""
+ action_tooltip_pass = ""
for j, conditionset in enumerate(self.__states[state]['conditionsets']):
-
- if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0:
- actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \
- self._actionlabel(state, 'actions_enter', conditionset, previousconditionset, previousstate_conditionset)
-
- if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0:
- actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \
- self._actionlabel(state, 'actions_stay', conditionset, previousconditionset, previousstate_conditionset)
-
- if len(actions_leave) > 0:
- actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \
- self._actionlabel(state, 'actions_leave', conditionset, previousconditionset, previousstate_conditionset)
-
- new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor
- position = '{},{}!'.format(0.5, new_y)
- conditionset_positions.append(new_y)
- #self._log_debug('conditionset: {} {}, previous {}', conditionset, position, previous_conditionset)
-
- conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset, i)
cond3 = conditionset == ''
try:
cond1 = i >= list(self.__states.keys()).index(self.__active_state)
- except Exception as ex:
- #self._log_debug('Condition 1 problem {}'.format(ex))
+ except Exception:
cond1 = True
try:
cond4 = i == list(self.__states.keys()).index(self.__active_state)
- except Exception as ex:
- #self._log_debug('Condition 4 problem {}'.format(ex))
+ except Exception:
cond4 = True
#self._log_debug('i {}, index of active state {}', i, list(self.__states.keys()).index(self.__active_state))
try:
cond2 = (j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset)
or i > list(self.__states.keys()).index(self.__active_state))
- except Exception as ex:
- #self._log_debug('Condition 2 problem {}'.format(ex))
+ except Exception:
cond2 = False if cond3 and cond4 else True
color = "gray" if cond1 and cond2 else "olivedrab" \
if (conditionset == self.__active_conditionset or cond3) and state == self.__active_state else "indianred2"
+ try:
+ cond5 = i >= list(self.__states.keys()).index(self.__active_state)
+ except Exception:
+ cond5 = True
+
+ cond6 = conditionset in ['', self.__active_conditionset] and state == self.__active_state
+ cond_enter = True if self.__states[state].get('enter') is True else False
+ cond_stay = True if self.__states[state].get('stay') is True else False
+ active = True if cond_enter and cond6 else False
+
+ if len(actions_enter) > 0 or len(actions_enter_or_stay) > 0:
+ actionlist_enter, action_tooltip_enter, action_tooltip_count_enter = \
+ self._actionlabel(state, 'actions_enter', conditionset, active)
+ active = True if cond_stay and cond6 else False
+ if len(actions_stay) > 0 or len(actions_enter_or_stay) > 0:
+ actionlist_stay, action_tooltip_stay, action_tooltip_count_stay = \
+ self._actionlabel(state, 'actions_stay', conditionset, active)
+ cond_leave = True if self.__states[state].get('leave') is True else False
+ active = True if cond_leave else False
+
+ if len(actions_leave) > 0:
+ actionlist_leave, action_tooltip_leave, action_tooltip_count_leave = \
+ self._actionlabel(state, 'actions_leave', conditionset, active)
+ cond_pass = True if self.__states[state].get('pass') is True else False
+ active = False if (cond5 and not cond_pass) or cond_leave else True
+ if len(actions_pass) > 0:
+ actionlist_pass, action_tooltip_pass, action_tooltip_count_pass = \
+ self._actionlabel(state, 'actions_pass', conditionset, active)
+
+ new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor
+ position = '{},{}!'.format(0.3, new_y)
+ conditionset_positions.append(new_y)
+ conditionlist, condition_tooltip, condition_tooltip_count = self._conditionlabel(state, conditionset)
+
label = 'no condition' if conditionset == '' else conditionset
self.__nodes['{}_{}'.format(state, conditionset)] = pydotplus.Node(
'{}_{}'.format(state, conditionset), style="filled", fillcolor=color, shape="diamond",
label=label, pos=position)
- #self._log_debug('Node {} {} drawn', state, conditionset)
- position = '{},{}!'.format(0.2, new_y)
+ #self._log_debug('Node {} {} drawn. Conditionlist {}', state, conditionset, conditionlist)
+ position = '{},{}!'.format(0.1, new_y)
xlabel = '1 tooltip' if condition_tooltip_count == 1\
else '{} tooltips'.format(condition_tooltip_count)\
if condition_tooltip_count > 1 else ''
@@ -507,9 +483,12 @@ def drawgraph(self, filename):
'{}_{}_conditions'.format(state, conditionset), style="filled", fillcolor=color,
shape="rect", label=conditionlist, pos=position, tooltip=condition_tooltip, xlabel=xlabel)
self.__graph.add_node(self.__nodes['{}_{}_conditions'.format(state, conditionset)])
+ # Create a dotted line between conditionlist and conditionset name
+ parenthesis_edge = pydotplus.Edge(self.__nodes['{}_{}_conditions'.format(state, conditionset)], self.__nodes['{}_{}'.format(state, conditionset)], arrowhead="none", color="black", style="dotted", constraint="false")
+ self.__graph.add_edge(parenthesis_edge)
self.__graph.add_node(self.__nodes['{}_{}'.format(state, conditionset)])
- new_x = 0.9
+ new_x = 0.55
if not actionlist_enter == '':
position = '{},{}!'.format(new_x, new_y)
xlabel = '1 tooltip' if action_tooltip_count_enter == 1\
@@ -537,11 +516,12 @@ def drawgraph(self, filename):
self.__graph.add_node(self.__nodes['{}_{}_actions_stay'.format(state, conditionset)])
self._add_actioncondition(state, conditionset, 'actions_stay', new_y, cond1, cond2)
- position = '{},{}!'.format(0.9, new_y)
+ position = '{},{}!'.format(0.55, new_y)
cond1 = self.__nodes.get('{}_{}_actions_enter'.format(state, conditionset)) is None
cond2 = self.__nodes.get('{}_{}_actions_stay'.format(state, conditionset)) is None
cond3 = self.__nodes.get('{}_{}_actions_leave'.format(state, conditionset)) is None
- if cond1 and cond2 and cond3:
+ cond4 = self.__nodes.get('{}_{}_actions_pass'.format(state, conditionset)) is None
+ if cond1 and cond2 and cond3 and cond4:
self.__nodes['{}_{}_right'.format(state, conditionset)] = pydotplus.Node('{}_{}_right'.format(
state, conditionset), shape="circle", width="0.7", pos=position, label="", fillcolor="black",
style="filled", tooltip="No Action")
@@ -557,7 +537,7 @@ def drawgraph(self, filename):
xlabel = ""
if j == 0:
self.__graph.add_edge(pydotplus.Edge(self.__nodes[state], self.__nodes['{}_right'.format(state)],
- style='bold', color='black', dir='none',
+ style='bold', color='black', dir='none',
xlabel=xlabel, edgetooltip='check first conditionset'))
self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_right'.format(state)],
self.__nodes['{}_{}'.format(state, conditionset)],
@@ -571,18 +551,13 @@ def drawgraph(self, filename):
if len(actions_leave) > 0:
new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor
- position = '{},{}!'.format(0.5, new_y)
- #self._log_debug('leaveconditions {}', position)
- try:
- cond1 = j > list(self.__states[state]['conditionsets'].keys()).index(self.__active_conditionset)
- except Exception:
- cond1 = True
+ position = '{},{}!'.format(0.3, new_y)
try:
cond2 = i >= list(self.__states.keys()).index(self.__active_state)
- except Exception:
+ except Exception as ex:
cond2 = True
cond3 = True if self.__states[state].get('leave') is True else False
- color = "gray" if cond1 and cond2 and not cond3 else "olivedrab" if cond3 else "indianred2"
+ color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2"
self.__nodes['{}_leave'.format(state)] = pydotplus.Node('{}_leave'.format(state),
style="filled", fillcolor=color, shape="diamond",
label='leave', pos=position)
@@ -605,6 +580,39 @@ def drawgraph(self, filename):
self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_leave'.format(state)],
self.__nodes['{}_actions_leave'.format(state)], style='bold',
taillabel=" True", tooltip='run leave actions'))
+ previous_conditionset = self.__nodes['{}_leave'.format(state)]
+
+ if len(actions_pass) > 0:
+ new_y -= 1 * self.__scalefactor if j == 0 else 2 * self.__scalefactor
+ position = '{},{}!'.format(0.3, new_y)
+ try:
+ cond2 = i >= list(self.__states.keys()).index(self.__active_state)
+ except Exception:
+ cond2 = True
+ cond3 = True if self.__states[state].get('pass') is True else False
+ color = "gray" if cond2 and not cond3 else "olivedrab" if cond3 else "indianred2"
+ self.__nodes['{}_pass'.format(state)] = pydotplus.Node('{}_pass'.format(state),
+ style="filled", fillcolor=color, shape="diamond",
+ label='pass', pos=position)
+ self.__graph.add_node(self.__nodes['{}_pass'.format(state)])
+ self.__graph.add_edge(pydotplus.Edge(previous_conditionset, self.__nodes['{}_pass'.format(state)],
+ style='bold', color='black', tooltip='check pass'))
+
+ position = '{},{}!'.format(new_x, new_y)
+ xlabel = '1 tooltip' if action_tooltip_count_pass == 1\
+ else '{} tooltips'.format(action_tooltip_count_pass)\
+ if action_tooltip_count_pass > 1 else ''
+ #self._log_debug('action pass: {}', position)
+ self.__nodes['{}_actions_pass'.format(state)] = pydotplus.Node('{}_actions_pass'.format(state),
+ style="filled", fillcolor=color,
+ shape="rectangle", label=actionlist_pass,
+ pos=position, align="center",
+ tooltip=action_tooltip_pass,
+ xlabel=xlabel)
+ self.__graph.add_node(self.__nodes['{}_actions_pass'.format(state)])
+ self.__graph.add_edge(pydotplus.Edge(self.__nodes['{}_pass'.format(state)],
+ self.__nodes['{}_actions_pass'.format(state)], style='bold',
+ taillabel=" True", tooltip='run pass actions'))
previous_state = state
diff --git a/stateengine/__init__.py b/stateengine/__init__.py
index bfa0c5c71..5b5296938 100755
--- a/stateengine/__init__.py
+++ b/stateengine/__init__.py
@@ -47,7 +47,7 @@
class StateEngine(SmartPlugin):
- PLUGIN_VERSION = '2.1.0'
+ PLUGIN_VERSION = '2.2.0'
# Constructor
# noinspection PyUnusedLocal,PyMissingConstructor
diff --git a/stateengine/plugin.yaml b/stateengine/plugin.yaml
index 5897f722a..be3495046 100755
--- a/stateengine/plugin.yaml
+++ b/stateengine/plugin.yaml
@@ -39,7 +39,7 @@ plugin:
state: ready
support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1303071-stateengine-plugin-support
- version: '2.1.0'
+ version: '2.2.0'
sh_minversion: '1.6'
multi_instance: False
classname: StateEngine
@@ -853,6 +853,12 @@ item_structs:
eval: sh..suspendduration(sh..suspendduration(), "Init", "Start")
crontab: init = True
+ conditionset_leaveactions:
+ suspend:
+ type: str
+ initial_value: '.*'
+ cache: True
+
suspendduration:
remark: duration of suspend mode in minutes (gets converted automatically)
type: num
@@ -937,29 +943,52 @@ item_structs:
- 'repeat: True'
- 'order: 5'
+ on_pass:
+ se_action_suspend:
+ - 'function: set'
+ - 'to: False'
+ - 'repeat: False'
+ se_action_suspend_visu:
+ - 'function: set'
+ - 'to: False'
+ - 'repeat: False'
+ se_action_suspend_end:
+ - 'function: set'
+ - 'to: '
+ - 'repeat: False'
+ se_action_suspend_start:
+ - 'function: set'
+ - 'to: '
+ - 'repeat: False'
+
on_leave:
se_action_suspend:
- 'function: set'
- 'to: False'
- 'order: 2'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_visu:
- 'function: set'
- 'to: False'
- 'order: 3'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_end:
- 'function: set'
- 'to: '
- 'order: 4'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_start:
- 'function: set'
- 'to: '
- 'order: 5'
- 'delay: 1'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_retrigger:
- 'function: special'
- 'value: retrigger:..retrigger'
- 'delay: -1'
- 'order: 1'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
enter_manuell:
se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path')
@@ -1049,6 +1078,12 @@ item_structs:
eval: (sh..suspendduration(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration0(sh..suspendduration(), "Init", "Start"), sh..suspendvariant.suspendduration1(sh..suspendvariant.suspendduration1(), "Init", "Start"), sh..suspendvariant.suspendduration2(sh..suspendvariant.suspendduration2(), "Init", "Start"))
crontab: init = True
+ conditionset_leaveactions:
+ suspend:
+ type: str
+ initial_value: '.*'
+ cache: True
+
suspendvariant:
remark: number between 0 and 2 to define which suspendduration should be used
type: num
@@ -1241,33 +1276,57 @@ item_structs:
- 'repeat: True'
- 'order: 5'
+ on_pass:
+ se_action_suspend:
+ - 'function: set'
+ - 'to: False'
+ - 'repeat: False'
+ se_action_suspend_visu:
+ - 'function: set'
+ - 'to: False'
+ - 'repeat: False'
+ se_action_suspend_end:
+ - 'function: set'
+ - 'to: '
+ - 'repeat: False'
+ se_action_suspend_start:
+ - 'function: set'
+ - 'to: '
+ - 'repeat: False'
+
on_leave:
se_action_suspendvariant:
- 'function: set'
- 'to: eval:se_eval.get_relative_itemproperty("..settings.suspendvariant", "last_value")'
- 'order: 1'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend:
- 'function: set'
- 'to: False'
- 'order: 2'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_visu:
- 'function: set'
- 'to: False'
- 'order: 3'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_end:
- 'function: set'
- 'to: '
- 'order: 4'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_suspend_start:
- 'function: set'
- 'to: '
- 'order: 5'
- 'delay: 1'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
se_action_retrigger:
- 'function: special'
- 'value: retrigger:..retrigger'
- 'delay: -1'
- 'order: 1'
+ - 'nextconditionset: item:..settings.conditionset_leaveactions.suspend'
enter_manuell:
se_value_trigger_source: eval:se_eval.get_relative_itemproperty('..manuell', 'path')
@@ -1418,6 +1477,12 @@ item_attribute_prefixes:
de: 'Definiert um welchen Wert sich das Item mindestens geändert haben muss, um neu gesetzt zu werden'
en: 'Definition of a threshold the item has to surpass to be newly set'
+ se_minagedelta_:
+ type: foo
+ description:
+ de: 'Definiert eine Pause zwischen mehreren Aktionsaufrufen.'
+ en: 'Definition of pause interval between multiple runs of actions'
+
se_min_:
type: foo
description:
@@ -1431,6 +1496,7 @@ item_attribute_prefixes:
en: 'Condition: The value of the item has to be lower than defined by se_max'
se_value_:
+ type: foo
description:
de: 'Bedingung: Das Item muss exakt dem durch se_value angegebenem Wert entsprechen'
en: 'Condition: The item has to have the exact same value as defined by se_value'
@@ -1508,11 +1574,13 @@ item_attribute_prefixes:
en: 'Special action like suspend or retrigger'
se_set_:
+ type: foo
description:
de: 'Setzen eines Items auf einen bestimmten Wert (veraltet - Nutze stattdessen se_action)'
en: 'Setting an item to a specific value (deprecated - use se_action instead)'
se_force_:
+ type: foo
description:
de: 'Setzen eines Items auf einen bestimmten Wert, egal ob sich der Wert geändert hat oder nicht (veraltet - Nutze stattdessen se_action)'
en: 'Setting an item to a specific value no matter whether the value has changed or not (deprecated - use se_action instead)'
@@ -1542,17 +1610,19 @@ item_attribute_prefixes:
en: 'Delaying an action (deprecated - use se_action instead)'
se_repeat_:
- type: bool
+ type: foo
description:
de: 'Definiert, ob eine Aktion beim erneuten Eintritt in den gleichen Status wiederholt werden soll oder nicht (veraltet - Nutze stattdessen se_action)'
en: 'Definition wether an action should be repeated or not when reentering the same state (deprecated - use se_action instead)'
se_order_:
+ type: foo
description:
de: 'Definiert die Reihenfolge einer Aktion als Integerzahl (veraltet - Nutze stattdessen se_action)'
en: 'Definition of the running order of an action as integer (deprecated - use se_action instead)'
se_manual_:
+ type: foo
description:
de: 'Diverse Funktion für den Suspendmodus wie include, exclude, invert, logitem.'
en: 'Some functions relevant for the suspend mode like include, exclude, invert, logitem.'
diff --git a/stateengine/user_doc/06_aktionen.rst b/stateengine/user_doc/06_aktionen.rst
index 339427c70..c278d3752 100755
--- a/stateengine/user_doc/06_aktionen.rst
+++ b/stateengine/user_doc/06_aktionen.rst
@@ -18,8 +18,9 @@ definiert und benannt wurde. Die Herangehensweise ähnelt also stark der Deklara
Zusätzlich zu ``se_item_`` lässt sich über den Eintrag
``se_mindelta_`` definieren, um welchen Wert
-sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Im unten
-stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta
+sich ein Item mindestens geändert haben muss, um neu gesetzt zu werden. Diese Konfiguration
+kann für einzelne Aktionen individuell über die Angabe ``mindelta`` überschrieben werden.
+Im unten stehenden Beispiel wird der Lamellenwert abhängig vom Sonnenstand berechnet. Ohne mindelta
würden sich die Lamellen ständig um wenige Grad(bruchteile) ändern. Wird jedoch mindelta
beispielsweise auf den Wert 10 gesetzt, findet eine Änderung erst statt, wenn sich der
errechnete Wert um mindestens 10 Grad vom aktuellen Lamellenwert unterscheidet.
@@ -300,6 +301,7 @@ ursprünglichen Zustands (regen) gesetzt werden soll, kann der Parameter ``insta
.. code-block:: yaml
+ 'repeat: /' --> Ergebnis eines Eval-Ausdrucks oder eines Items
'repeat: [True|False]'
Über das Attribut wird unabhängig vom globalen Setting für das
@@ -393,7 +395,7 @@ regnet hingegen auf den Wert, der in den Settings hinterlegt ist.
.. code-block:: yaml
- previousconditionset: regex:enter_(.*)_test"
+ "previousconditionset: regex:enter_(.*)_test"
Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden
soll, wenn die vorherige Bedingungsgruppe des aktuellen Zustands mit dem angegebenen Ausdruck übereinstimmt.
@@ -403,12 +405,65 @@ Die Abfrage erfolgt dabei nach den gleichen Regeln wie bei ``conditionset`` oben
.. code-block:: yaml
- previousstate_conditionset: regex:enter_(.*)_test"
+ "previousstate_conditionset: regex:enter_(.*)_test"
Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden
soll, wenn die Bedingungsgruppe, mit der der vorherige Zustand eingenommen wurde, mit dem angegebenen Ausdruck übereinstimmt.
Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben.
+**next_conditionset: **
+
+.. code-block:: yaml
+
+ "next_conditionset: regex:enter_(.*)_test"
+
+Über das Attribut wird festgelegt, dass die Aktion nur dann ausgeführt werden
+soll, wenn die Bedingungsgruppe, mit der der zukünftige Zustand eingenommen wird, mit dem angegebenen Ausdruck übereinstimmt.
+Die Abfrage erfolgt dabei ebenfalls nach den gleichen Regeln wie bei ``conditionset`` oben angegeben.
+Diese Angabe ist primär bei leave_actions sinnvoll.
+
+**mindelta: **
+
+Im folgenden Beispiel wird mindelta für eine einzelne Aktion gesetzt. Anstatt also eine minimale Änderung
+für alle Aktionen mit bestimmtem Namen festzulegen, wird eine einzelne Aktion nur dann ausgeführt,
+wenn sich der Wert um mindestens den angegeben Wert geändert hat.
+Wird mindelta beispielsweise auf den Wert 10 gesetzt, findet eine Änderung erst statt, wenn sich der
+errechnete Wert um mindestens 10 Grad vom aktuellen Lamellenwert unterscheidet.
+
+.. code-block:: yaml
+
+ #items/item.yaml
+ raffstore1:
+ automatik:
+ struct: stateengine.general
+ rules:
+ se_item_height: raffstore1.hoehe # Definition des zu ändernden Höhe-Items
+ se_item_lamella: raffstore1.lamelle # Definition des zu ändernden Lamellen-Items
+ se_status_lamella: raffstore1.lamelle.status # Definition des Lamellen Statusitems
+ Daemmerung:
+ <...>
+ se_action_height:
+ - 'function: set'
+ - 'to: value:100'
+ se_action_lamella:
+ - 'function: set'
+ - 'to: value:25'
+ - 'mindelta: 10'
+ <...>
+
+**minagedelta: **
+
+.. code-block:: yaml
+
+ minagedelta: 300
+
+Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird.
+Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens
+5 Minuten zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird.
+Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem
+Betrieb gespeichert und überdauert einen Neustart nicht.
+
+
Templates für Aktionen
----------------------
diff --git a/stateengine/user_doc/10_funktionen_variablen.rst b/stateengine/user_doc/10_funktionen_variablen.rst
index c1ee17a7b..e84e1f0e7 100755
--- a/stateengine/user_doc/10_funktionen_variablen.rst
+++ b/stateengine/user_doc/10_funktionen_variablen.rst
@@ -282,13 +282,10 @@ die Situation deutlich vereinfachen würde.
**current.conditionset_name:**
*Der Name der Bedingungsgruppe, die gerade geprüft wird*
-Beide current.conditionset Variablen können ebenso wie die oben erwähnten current.state_id/name
-nur während der Prüfung der Bedingungen genutzt werden, nicht jedoch für Aktionen.
-
Das Beispiel zeigt einen Windzustand. Dieser übernimmt keine Funktionen,
sondern dient lediglich der Visualisierung (Sicherheitsrelevante Features
sollten unbedingt z.B. über den KNX Bus erfolgen!). Außerdem wird davon
-ausgegangen, dass es einen untergeordneten Zustand names x gibt.
+ausgegangen, dass es einen untergeordneten Zustand namens x gibt.
- enter_normal wird angenommen, sobald das Wind-Item aktiv ist, zuvor aber
nicht der x-Zustand aktiv war.
@@ -364,6 +361,14 @@ Zustand aktiv gewesen ist. Ansonsten gelten alle vorhin beschriebenen Regeln.
**previous.state_conditionset_name:**
*Der Name der Bedingungsgruppe, die beim vorigen Zustand zuletzt aktiv war*
+**next.conditionset_id:**
+*Die Id der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist*
+
+**next.conditionset_name:**
+*Der Name der Bedingungsgruppe, die für einen Zustandswechsel verantwortlich ist*
+
+Beide next.conditionset Variablen können sinnvoll nur für Aktionen genutzt werden.
+
**release.can_be_released_by:**
*Die Definitionen, die den aktuellen Zustand generell auflösen könnten*
diff --git a/stateengine/user_doc/12_aktioneneinzeln.rst b/stateengine/user_doc/12_aktioneneinzeln.rst
index 9363459ae..cb00bf9dc 100755
--- a/stateengine/user_doc/12_aktioneneinzeln.rst
+++ b/stateengine/user_doc/12_aktioneneinzeln.rst
@@ -150,8 +150,50 @@ Es ist möglich, eine Minimumabweichung für
aktuellen Wert des Items und dem ermittelten neuen Wert kleiner
ist als die festgelegte Minimumabweichung wird keine Änderung
vorgenommen. Die Minimumabweichung wird über das Attribut
-``se_mindelta_`` auf der Ebene des Regelwerk-Items
-festgelegt.
+``se_mindelta_`` auf der Ebene des Regelwerk-Items für alle
+Aktionen mit dem entsprechenden Namen festgelegt. Alternativ kann dieses
+Attribut aber auch für einzelne Aktionen festgelegt und/oder überschrieben werden.
+
+Im angeführten Beispiel werden die Lamellen beim Eintritt in einen Zustand und
+beim Verlassen des Zustands nur dann gesetzt, wenn sich der Wert der Lamellen inzwischen
+um mindestens 10 verändert hat. Wird der Zustand erneut eingenommen (stay), wird der
+Wert hingegen mit 5 überschrieben.
+
+.. code-block:: yaml
+
+ #items/item.yaml
+ raffstore1:
+ automatik:
+ struct: stateengine.general
+ rules:
+ se_item_height: raffstore1.hoehe # Definition des zu ändernden Höhe-Items
+ se_item_lamella: raffstore1.lamelle # Definition des zu ändernden Lamellen-Items
+ se_status_lamella: raffstore1.lamelle.status # Definition des Lamellen Statusitems
+ se_mindelta_lamella: 10 # Alle Aktionen mit Namen lamella sind betroffen
+ Daemmerung:
+ on_enter:
+ se_set_height: 100
+ se_set_lamella: 20
+ on_leave:
+ se_set_height: 100
+ se_set_lamella: 15
+ on_stay:
+ se_set_height: 100
+ se_set_lamella: 20
+ se_set_lamella: 20
+ se_mindelta_lamella: 5
+
+**minagedelta: **
+
+.. code-block:: yaml
+
+ se_minagedelta_: 300 (Sekunden)|30m (Minuten)
+
+Über das Attribut wird festgelegt, dass eine Aktion nur in einem vorgegebenen Intervall ausgeführt wird.
+Im angegebenen Beispiel wird die Aktion also nur ausgeführt, wenn der letzte Ausführungszeitpunkt mindestens
+300 Sekunden zurück liegt. So kann verhindert werden, dass die Aktion bei jeder Neuevaluierung des Status ausgeführt wird.
+Ein Neustart des Plugins setzt den Counter wieder zurück, der letzte Ausführungszeitpunkt wird also nur bei laufendem
+Betrieb gespeichert und überdauert einen Neustart nicht.
**delay: **
@@ -167,8 +209,10 @@ Der Timer zur Ausführung der Aktion nach der angegebenen
Verzögerung wird entfernt, wenn eine gleichartige Aktion
ausgeführt werden soll (egal ob verzögert oder nicht). Wenn also
die Verzögerung größer als der ``cycle`` ist, wird die Aktion
-nie durchgeführt werden, es sei denn die Aktion soll nur
-einmalig ausgeführt werden.
+nicht mehr durchgeführt.
+Außerdem ist es möglich, den Timer bewusst abzubrechen, ohne eine Aktion auszuführen,
+indem der Delay auf -1 gesetzt wird. Dies macht insbesondere beim Verlassen von
+Zuständen Sinn, um ungewünschte verzögerte Aktionen vom "alten" Zustand zu verhindern.
**repeat: **
diff --git a/telegram/__init__.py b/telegram/__init__.py
index c3ec5507e..6c0dded7b 100755
--- a/telegram/__init__.py
+++ b/telegram/__init__.py
@@ -2,7 +2,7 @@
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2017 Markus Garscha http://knx-user-forum.de/
-# 2018-2023 Ivan De Filippis
+# 2018-2024 Ivan De Filippis
# 2018-2021 Bernd Meiners Bernd.Meiners@mail.de
#########################################################################
#
@@ -68,7 +68,7 @@
class Telegram(SmartPlugin):
- PLUGIN_VERSION = "2.0.1"
+ PLUGIN_VERSION = '2.0.3'
_items = [] # all items using attribute ``telegram_message``
_items_info = {} # dict used whith the info-command: key = attribute_value, val= item_list telegram_info
@@ -77,6 +77,7 @@ class Telegram(SmartPlugin):
_chat_ids_item = {} # an item with a dict of chat_id and write access
_waitAnswer = None # wait a specific answer Yes/No - or num (change_item)
_queue = None # queue for the messages to be sent
+ _cHandlers = [] # CommandHandler from parse_item
def __init__(self, sh):
"""
@@ -100,8 +101,9 @@ def __init__(self, sh):
self.logger.error(f"{self.get_fullname()}: Unable to import Python package 'python-telegram-bot' [{REQUIRED_PACKAGE_IMPORTED}]")
return
- self._loop = asyncio.new_event_loop() # new_event is required for multi-instance
- asyncio.set_event_loop(self._loop)
+ #self._loop = asyncio.new_event_loop() # new_event is required for multi-instance
+ #self.log_event_loop_details()
+ #asyncio.set_event_loop(self._loop)
self.alive = False
self._name = self.get_parameter_value('name')
@@ -116,27 +118,12 @@ def __init__(self, sh):
self._resend_delay = self.get_parameter_value('resend_delay')
self._resend_attemps = self.get_parameter_value('resend_attemps')
+ self._pause_item = None
+ self._pause_item_path = self.get_parameter_value('pause_item')
+
self._bot = None
self._queue = Queue()
- self._application = Application.builder().token(self._token).build()
-
- if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug("adding command handlers to application")
-
- self._application.add_error_handler(self.eHandler)
- self._application.add_handler(CommandHandler('time', self.cHandler_time))
- self._application.add_handler(CommandHandler('help', self.cHandler_help))
- self._application.add_handler(CommandHandler('hide', self.cHandler_hide))
- self._application.add_handler(CommandHandler('list', self.cHandler_list))
- self._application.add_handler(CommandHandler('info', self.cHandler_info))
- self._application.add_handler(CommandHandler('start', self.cHandler_start))
- self._application.add_handler(CommandHandler('lo', self.cHandler_lo))
- self._application.add_handler(CommandHandler('tr', self.cHandler_tr))
- self._application.add_handler(CommandHandler('control', self.cHandler_control))
- # Filters.text includes also commands, starting with ``/`` so it is needed to exclude them.
- self._application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.mHandler))
-
self.init_webinterface()
if not self.init_webinterface(WebInterface):
self.logger.error("Unable to start Webinterface")
@@ -162,22 +149,56 @@ def run(self):
This is called when the plugins thread is about to run
"""
if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug("Run method called")
+ self.logger.debug(f"Plugin '{self.get_fullname()}': run method called")
- self.logics = Logics.get_instance() # Returns the instance of the Logics class, to be used to access the logics-api
+ if self.alive:
+ return
+ if self._pause_item is not None and bool(self._pause_item()):
+ self.logger.info(f'plugin not startet - pause_item is True')
+ return
+
+ self._loop = asyncio.new_event_loop() # new_event is required for multi-instance
+ asyncio.set_event_loop(self._loop)
+
+ self._application = Application.builder().token(self._token).build()
+
+ if self.logger.isEnabledFor(logging.DEBUG):
+ self.logger.debug("adding command handlers to application")
+
+ self._application.add_error_handler(self.eHandler)
+ self._application.add_handler(CommandHandler('time', self.cHandler_time))
+ self._application.add_handler(CommandHandler('help', self.cHandler_help))
+ self._application.add_handler(CommandHandler('hide', self.cHandler_hide))
+ self._application.add_handler(CommandHandler('list', self.cHandler_list))
+ self._application.add_handler(CommandHandler('info', self.cHandler_info))
+ self._application.add_handler(CommandHandler('start', self.cHandler_start))
+ self._application.add_handler(CommandHandler('lo', self.cHandler_lo))
+ self._application.add_handler(CommandHandler('tr', self.cHandler_tr))
+ self._application.add_handler(CommandHandler('control', self.cHandler_control))
+ # Filters.text includes also commands, starting with ``/`` so it is needed to exclude them.
+ self._application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), self.mHandler))
+ # add CommandHandlers from parse_item
+ for cHandler in self._cHandlers:
+ self._application.add_handler(cHandler)
+
self.alive = True
+ self.logics = Logics.get_instance() # Returns the instance of the Logics class, to be used to access the logics-api
+
self._loop.run_until_complete(self.run_coros())
if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug(f"Run method ended")
+ self.logger.debug(f"Plugin '{self.get_fullname()}': run method finished ")
def stop(self):
"""
This is called when the plugins thread is about to stop
"""
if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug("stop telegram plugin")
+ self.logger.debug(f"Plugin '{self.get_fullname()}': stop method called")
+
+ if self.scheduler_get('telegram_change_item_timeout'):
+ self.scheduler_remove('telegram_change_item_timeout')
try:
if self._bye_msg:
@@ -188,8 +209,18 @@ def stop(self):
except Exception as e:
self.logger.error(f"could not send bye message [{e}]")
- time.sleep(1)
- self.alive = False # Clears the infiniti loop in sendQueue
+ if not self._queue.empty():
+ time.sleep(5)
+ if not self._queue.empty():
+ try:
+ self.alive = False # Clears the infiniti loop in sendQueue
+ self.remove_all_events()
+ self.logger.debug(f"Events removed.")
+ except Exception as e:
+ self.logger.error(f"An error occurred while removing the events: [{e}]")
+
+ self.alive = False
+
try:
asyncio.gather(self._taskConn, self._taskQueue)
self.disconnect()
@@ -204,8 +235,20 @@ def stop(self):
self.logger.error(f"An error occurred while stopping the plugin [{e}]")
if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug("stop telegram plugin finished")
+ self.logger.debug(f"Plugin '{self.get_fullname()}': stop method finished")
+
+ def remove_all_events(self):
+ while not self._queue.empty():
+ self._queue.get_nowait() # Entfernt das Event aus der Queue
+ self._queue.task_done() # Markiert die Aufgabe als erledigt
+ self.logger.debug("all events removed")
+ def _start_loop(self):
+ self._loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self._loop)
+ self.logger.debug("Starting event loop")
+ self._loop.run_forever()
+
async def run_coros(self):
"""
This method run multiple coroutines concurrently using asyncio
@@ -220,37 +263,41 @@ async def connect(self):
"""
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug("connect method called")
- try:
- await self._application.initialize()
- await self._application.start()
- self._updater = self._application.updater
-
- q = await self._updater.start_polling(timeout=self._long_polling_timeout, error_callback=self.error_handler)
+ while self.alive:
+ try:
+ await self._application.initialize()
+ await self._application.start()
+ self._updater = self._application.updater
- if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug(f"started polling the updater, Queue is {q}")
+ q = await self._updater.start_polling(timeout=self._long_polling_timeout, error_callback=self.error_handler)
- self._bot = self._updater.bot
- self.logger.info(f"Telegram bot is listening: {await self._updater.bot.getMe()}")
- if self._welcome_msg:
if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug(f"sent welcome message {self._welcome_msg}")
- cids = [key for key, value in self._chat_ids_item().items() if value == 1]
- self.msg_broadcast(self._welcome_msg, chat_id=cids)
+ self.logger.debug(f"started polling the updater, Queue is {q}")
- except TelegramError as e:
- # catch Unauthorized errors due to an invalid token
- self.logger.error(f"Unable to start up Telegram conversation. Maybe an invalid token? {e}")
- return False
+ self._bot = self._updater.bot
+ self.logger.info(f"Telegram bot is listening: {await self._updater.bot.getMe()}")
+ if self._welcome_msg:
+ if self.logger.isEnabledFor(logging.DEBUG):
+ self.logger.debug(f"sent welcome message {self._welcome_msg}")
+ cids = [key for key, value in self._chat_ids_item().items() if value == 1]
+ self.msg_broadcast(self._welcome_msg, chat_id=cids)
+
+ # If no exception occurred, break the loop and exit the function
+ break
+
+ except TelegramError as e:
+ self.logger.error(f"Unable to start up Telegram conversation. {e}")
+ await asyncio.sleep(60) # Wait for 60 seconds before retrying - while
+
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug("connect method end")
- def error_handler(self, update, context):
+ def error_handler(self, error):
"""
Just logs an error in case of a problem
"""
try:
- self.logger.warning(f'Update {update} caused error {context.error}')
+ self.logger.error(f'Update caused error {error}')
except Exception:
pass
@@ -330,6 +377,14 @@ def parse_item(self, item):
Default plugin parse_item method. Is called when the plugin is initialized.
:param item: The item to process.
"""
+
+ # 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
+
if self.has_iattr(item.conf, ITEM_ATTR_CHAT_IDS):
if self._chat_ids_item:
self.logger.warning(f"Item: {item.property.path} declares chat_id for telegram plugin which are already defined, aborting!")
@@ -360,7 +415,8 @@ def parse_item(self, item):
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"Register new command '{key}', add item '{item}' and register a handler")
# add a handler for each info-attribute
- self._application.add_handler(CommandHandler(key, self.cHandler_info_attr))
+ self._cHandlers.append(CommandHandler(key, self.cHandler_info_attr))
+ #self._application.add_handler(CommandHandler(key, self.cHandler_info_attr))
return self.update_item
else:
self.logger.error(f"Command '{key}' chosen for item '{item}' is invalid for telegram botfather")
@@ -411,7 +467,8 @@ def parse_item(self, item):
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"Register new command '{key}', add item '{item}' and register a handler")
# add a handler for each control-attribute
- self._application.add_handler(CommandHandler(key, self.cHandler_control_attr))
+ #self._application.add_handler(CommandHandler(key, self.cHandler_control_attr))
+ self._cHandlers.append(CommandHandler(key, self.cHandler_control_attr))
return self.update_item
return None
@@ -433,6 +490,21 @@ def update_item(self, item, caller=None, source=None, dest=None):
"""
Called each time an item changed in SmartHomeNG
"""
+
+ # 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 not self.alive:
+ self.logger.info('Plugin is not alive, data will not be written')
+ return
+
if caller != self.get_fullname():
self.logger.info(f"update item: {item.property.path}")
@@ -527,7 +599,7 @@ async def async_msg_broadcast(self, msg, chat_id=None, reply_markup=None, parse_
for cid in self.get_chat_id_list(chat_id):
try:
- response = await self._bot.send_message(chat_id=cid, text=msg, reply_markup=reply_markup, parse_mode=parse_mode)
+ response = await self._bot.sendMessage(chat_id=cid, text=msg, reply_markup=reply_markup, parse_mode=parse_mode)
if response:
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"Message sent:[{msg}] to Chat_ID:[{cid}] Bot:[{self._bot.bot}] response:[{response}]")
@@ -630,7 +702,7 @@ def has_access_right(self, user_id):
if user_id in self._chat_ids_item():
return True
else:
- self._bot.send_message(chat_id=user_id, text=self._no_access_msg)
+ self._bot.sendMessage(chat_id=user_id, text=self._no_access_msg)
return False
@@ -642,7 +714,7 @@ def has_write_access_right(self, user_id):
if user_id in self._chat_ids_item():
return self._chat_ids_item()[user_id]
else:
- self._bot.send_message(chat_id=user_id, text=self._no_write_access_msg)
+ self._bot.sendMessage(chat_id=user_id, text=self._no_write_access_msg)
return False
@@ -685,7 +757,7 @@ def has_write_access_right(self, user_id):
it contains the following objects:
args
- bot context.bot is the target for send_message() function
+ bot context.bot is the target for sendMessage() function
bot_data
chat_data
dispatcher
@@ -707,7 +779,7 @@ async def eHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) ->
Just logs an error in case of a problem
"""
try:
- self.logger.warning(f'Update {update} caused error {context.error}')
+ self.logger.error(f'Update {update} caused error {context.error}')
except Exception:
pass
@@ -740,26 +812,26 @@ async def mHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) ->
if dicCtl['type'] == 'onoff':
item = dicCtl['item']
msg = f"{dicCtl['name']} \n change to:On(True)"
- self._bot.sendMessage(chat_id=update.message.chat.id, text=msg)
+ await self._bot.sendMessage(chat_id=update.message.chat.id, text=msg)
item(True)
self._waitAnswer = None
- self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
+ await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
elif text == 'Off':
if dicCtl['type'] == 'onoff':
item = dicCtl['item']
msg = f"{dicCtl['name']} \n change to:Off(False)"
- self._bot.sendMessage(chat_id=update.message.chat.id, text=msg)
+ await self._bot.sendMessage(chat_id=update.message.chat.id, text=msg)
item(False)
self._waitAnswer = None
- self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
+ await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
elif text == 'Yes':
if self.scheduler_get('telegram_change_item_timeout'):
self.scheduler_remove('telegram_change_item_timeout')
dicCtlCopy = dicCtl.copy()
dicCtlCopy['question'] = ''
- self.change_item(update, context, dicCtlCopy['name'], dicCtlCopy)
+ await self.change_item(update, context, dicCtlCopy['name'], dicCtlCopy)
self._waitAnswer = None
- self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
+ await self._bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
elif dicCtl['type'] == 'num':
if type(text) == int or float:
if self.logger.isEnabledFor(logging.DEBUG):
@@ -787,7 +859,7 @@ async def mHandler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) ->
msg = f"{dicCtl['name']} \n out off range"
await context.bot.sendMessage(chat_id=update.message.chat.id, text=msg)
else:
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()})
self._waitAnswer = None
except Exception as e:
if self.logger.isEnabledFor(logging.DEBUG):
@@ -800,7 +872,7 @@ async def cHandler_time(self, update: Update, context: ContextTypes.DEFAULT_TYPE
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"/time: return server time for update={update}, chat_id={update.message.chat.id} and context={dir(context)}")
if self.has_access_right(update.message.chat.id):
- await context.bot.send_message(chat_id=update.message.chat.id, text=str(datetime.datetime.now()))
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=str(datetime.datetime.now()))
async def cHandler_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -809,7 +881,7 @@ async def cHandler_help(self, update: Update, context: ContextTypes.DEFAULT_TYPE
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"/help: show available commands as keyboard for update={update}, chat_id={update.message.chat.id} and context={dir(context)}")
if self.has_access_right(update.message.chat.id):
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("choose"), reply_markup={"keyboard": [["/hide","/start"], ["/time","/list"], ["/lo","/info"], ["/control", "/tr "]]})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("choose"), reply_markup={"keyboard": [["/hide","/start"], ["/time","/list"], ["/lo","/info"], ["/control", "/tr "]]})
async def cHandler_hide(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -819,7 +891,7 @@ async def cHandler_hide(self, update: Update, context: ContextTypes.DEFAULT_TYPE
self.logger.debug(f"/hide: hide keyboard for bot={context.bot} and chat_id={update.message.chat.id}")
if self.has_access_right(update.message.chat.id):
hide_keyboard = {'hide_keyboard': True}
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("I'll hide the keyboard"), reply_markup=hide_keyboard)
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("I'll hide the keyboard"), reply_markup=hide_keyboard)
async def cHandler_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -828,8 +900,7 @@ async def cHandler_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"/list: show registered items and value for chat_id={update.message.chat.id}")
if self.has_access_right(update.message.chat.id):
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.list_items())
- #self.list_items(update.message.chat.id)
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items())
async def cHandler_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -839,9 +910,9 @@ async def cHandler_info(self, update: Update, context: ContextTypes.DEFAULT_TYPE
self.logger.debug(f"/info: show item-menu with registered items with specific attribute for chat_id={update.message.chat.id}")
if self.has_access_right(update.message.chat.id):
if len(self._items_info) > 0:
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Infos from the items:"), reply_markup={"keyboard": self.create_info_reply_markup()})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Infos from the items:"), reply_markup={"keyboard": self.create_info_reply_markup()})
else:
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_info!"), reply_markup={"keyboard": self.create_info_reply_markup()})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_info!"), reply_markup={"keyboard": self.create_info_reply_markup()})
async def cHandler_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -866,7 +937,7 @@ async def cHandler_start(self, update: Update, context: ContextTypes.DEFAULT_TYP
else:
self.logger.warning('No chat_ids defined')
- await context.bot.send_message(chat_id=update.message.chat.id, text=text)
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=text)
async def cHandler_info_attr(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -882,7 +953,6 @@ async def cHandler_info_attr(self, update: Update, context: ContextTypes.DEFAULT
if c_key in self._items_info:
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug(f"info-command: {c_key}")
- #self.list_items_info(update.message.chat.id, c_key)
await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items_info(c_key))
else:
await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("unknown command %s") % c_key)
@@ -901,7 +971,6 @@ async def cHandler_lo(self, update: Update, context: ContextTypes.DEFAULT_TYPE)
for logic in sorted(self.logics.return_defined_logics()): # list with the names of all logics that are currently loaded
data = []
info = self.logics.get_logic_info(logic)
- # self.logger.debug(f"logic_info: {info}")
if len(info) == 0 or not info['enabled']:
data.append("disabled")
if 'next_exec' in info:
@@ -936,11 +1005,10 @@ async def cHandler_control(self, update: Update, context: ContextTypes.DEFAULT_T
self.logger.debug(f"/control: show item-menu with registered items with specific attribute for chat_id={update.message.chat.id}")
if self.has_write_access_right(update.message.chat.id):
if len(self._items_control) > 0:
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.list_items_control())
- #self.list_items_control(update.message.chat.id)
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard":self.create_control_reply_markup()})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.list_items_control())
else:
- await context.bot.send_message(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_control!"), reply_markup={"keyboard": self.create_control_reply_markup()})
+ await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("no items have attribute telegram_control!"), reply_markup={"keyboard": self.create_control_reply_markup()})
async def cHandler_control_attr(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""
@@ -1035,6 +1103,7 @@ def list_items_control(self):
"""
Show registered items and value with specific attribute ITEM_ATTR_CONTROL
"""
+ text = ""
for key, value in sorted(self._items_control.items()): # {'type':type,'item':item}
item = value['item']
if item.type():
@@ -1135,16 +1204,14 @@ async def change_item(self, update, context, name, dicCtl):
text = self.translate("no items found with the attribute %s") % ITEM_ATTR_CONTROL
await context.bot.sendMessage(chat_id=chat_id, text=text)
- async def telegram_change_item_timeout(self, **kwargs):
+ def telegram_change_item_timeout(self, **kwargs):
update = None
- context = None
if 'update' in kwargs:
update = kwargs['update']
- if 'context' in kwargs:
- context = kwargs['context']
- if self.logger.isEnabledFor(logging.DEBUG):
- self.logger.debug(f"Answer control_item timeout update:{update} context:{context}")
- if self._waitAnswer is not None:
- self._waitAnswer = None
- # self._bot.send_message(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()})
- await context.bot.sendMessage(chat_id=update.message.chat.id, text=self.translate("Control/Change item-values:"), reply_markup={"keyboard": self.create_control_reply_markup()})
+ if self.logger.isEnabledFor(logging.DEBUG):
+ self.logger.debug(f"Answer control_item timeout update:{update}")
+ if self._waitAnswer is not None:
+ self._waitAnswer = None
+ self.msg_broadcast(msg=self.translate("Control/Change item-values:"), chat_id=update.message.chat.id, reply_markup={"keyboard": self.create_control_reply_markup()} )
+ elif self.logger.isEnabledFor(logging.DEBUG):
+ self.logger.debug(f"telegram_change_item_timeout: update argument missing")
diff --git a/telegram/plugin.yaml b/telegram/plugin.yaml
index 09d9dfc1a..5bf572ecc 100755
--- a/telegram/plugin.yaml
+++ b/telegram/plugin.yaml
@@ -11,10 +11,10 @@ plugin:
keywords: telegram chat messenger photo
support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1548691-support-thread-für-das-telegram-plugin
- version: 2.0.1 # Plugin version
- sh_minversion: '1.9.5' # minimum shNG version to use this plugin
+ version: 2.0.3 # 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 # minimum Python version to use for this plugin
+ py_minversion: '3.6' # minimum Python version to use for this plugin
# py_maxversion: # maximum Python version to use for this plugin (leave empty if latest)
multi_instance: True # plugin supports multi instance
restartable: True
@@ -92,6 +92,14 @@ parameters:
de: 'Telegram Threads zur leichteren Unterscheidung in der Thread Liste umbenennen'
en: 'Rename Telegram threads for easier distinction in thread list'
+ pause_item:
+ type: str
+ default: ''
+ description:
+ de: 'Item, um die Ausführung des Plugins zu steuern'
+ en: 'item for controlling plugin execution'
+
+
item_attributes:
# Definition of item attributes defined by this plugin
telegram_message:
diff --git a/telegram/user_doc.rst b/telegram/user_doc.rst
index e1b8890a8..25dfdd892 100755
--- a/telegram/user_doc.rst
+++ b/telegram/user_doc.rst
@@ -599,6 +599,12 @@ dargestellt und die entsprechenden Aktionen ausgeführt.
if msg != '':
telegram_plugin.msg_broadcast(msg, message_chat_id, reply_markup, parse_mode)
+Changelog
+---------
+V2.0.3 Plugin mit stop/run/pause_item steuerbar
+V2.0.2 Fehler beim Kommando ``/control`` behoben
+V2.0.0 Umbau auf neues Telegram Paket (V20.2+) mit async
+
Web Interface
=============
diff --git a/uzsu/plugin.yaml b/uzsu/plugin.yaml
index 12c7eb150..f03cf0f8a 100755
--- a/uzsu/plugin.yaml
+++ b/uzsu/plugin.yaml
@@ -149,6 +149,7 @@ item_structs:
visu_acl: ro
eval: sh...planned()
eval_trigger: ..
+ initial_value: "{'value': '-', 'next': '-'}"
value:
remark: The next value
@@ -156,7 +157,6 @@ item_structs:
visu_acl: ro
eval: sh...property.value['value']
eval_trigger: ..
- initial_value: '-'
time:
remark: The next time
@@ -164,7 +164,6 @@ item_structs:
visu_acl: ro
eval: sh...property.value['next']
eval_trigger: ..
- initial_value: '-'
logic_parameters: NONE
# Definition of logic parameters defined by this plugin
diff --git a/visu_smartvisu/__init__.py b/visu_smartvisu/__init__.py
index 504d35b83..feaaa1d9f 100755
--- a/visu_smartvisu/__init__.py
+++ b/visu_smartvisu/__init__.py
@@ -41,10 +41,9 @@
#########################################################################
class SmartVisu(SmartPlugin):
- PLUGIN_VERSION="1.3.4"
+ PLUGIN_VERSION = "1.3.4"
ALLOW_MULTIINSTANCE = True
-
def my_to_bool(self, value, attr='', default=False):
try:
result = self.to_bool(value)
@@ -53,15 +52,14 @@ def my_to_bool(self, value, attr='', default=False):
self.logger.error("smartVISU: Invalid value '"+str(value)+"' configured for attribute "+attr+" in plugin.conf, using '"+str(result)+"' instead")
return result
-
- def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite_templates='Yes', visu_style = 'std', handle_widgets='True' ):
+ def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite_templates=True, visu_style='std', handle_widgets='True'):
self.logger = logging.getLogger(__name__)
self._sh = smarthome
self.smartvisu_dir = str(smartvisu_dir)
self._generate_pages = self.my_to_bool(generate_pages, 'generate_pages', True)
self.overwrite_templates = self.my_to_bool(overwrite_templates, 'overwrite_templates', True)
- if visu_style.lower() in ['std','blk']:
+ if visu_style.lower() in ['std', 'blk']:
self.visu_style = visu_style.lower()
else:
self.visu_style = 'std'
@@ -74,7 +72,6 @@ def __init__(self, smarthome, smartvisu_dir='', generate_pages='True', overwrite
# else:
# self.logger.log(logging.WARNING, "Handling for smartVISU v{} in directory {}".format(self.smartvisu_version, self.smartvisu_dir))
-
def run(self):
self.alive = True
if self.smartvisu_dir != '':
@@ -97,11 +94,9 @@ def run(self):
self.logger.exception("SmartVisuGenerator: Exception: {}".format(e))
self.logger.info("Finished smartVISU v{} handling".format(self.smartvisu_version))
-
def stop(self):
self.alive = False
-
def parse_item(self, item):
# Relative path support (release 1.3 and up)
item.expand_relativepathes('sv_widget', "'", "'")
@@ -109,15 +104,12 @@ def parse_item(self, item):
item.expand_relativepathes('sv_nav_aside', "'", "'")
item.expand_relativepathes('sv_nav_aside2', "'", "'")
-
def parse_logic(self, logic):
pass
-
def update_item(self, item, caller=None, source=None, dest=None):
pass
-
def get_smartvisu_version(self):
"""
Determine which smartVISU version is installed in 'smartvisu_dir'
@@ -147,7 +139,7 @@ def get_smartvisu_version(self):
class SmartVisuGenerator:
- def __init__(self, smarthome, smartvisu_dir='', overwrite_templates='Yes', visu_style='std', smartvisu_version=''):
+ def __init__(self, smarthome, smartvisu_dir='', overwrite_templates=True, visu_style='std', smartvisu_version=''):
self.logger = logging.getLogger(__name__)
self._sh = smarthome
self.items = Items.get_instance()
@@ -156,9 +148,9 @@ def __init__(self, smarthome, smartvisu_dir='', overwrite_templates='Yes', visu_
self.smartvisu_version = smartvisu_version
self.overwrite_templates = overwrite_templates
self.visu_style = visu_style.lower()
- if not self.visu_style in ['std','blk']:
+ if self.visu_style not in ['std', 'blk']:
self.visu_style = 'std'
- self.logger.warning("SmartVisuGenerator: visu_style '{}' unknown, using visu_style '{1}'".format(visu_style, self.visu_style))
+ self.logger.warning("SmartVisuGenerator: visu_style '{0}' unknown, using visu_style '{1}'".format(visu_style, self.visu_style))
self.logger.info("Generating pages for smartVISU v{}".format(self.smartvisu_version))
@@ -174,7 +166,6 @@ def __init__(self, smarthome, smartvisu_dir='', overwrite_templates='Yes', visu_
self.pages()
self.logger.info("Generating pages for smartVISU v{} End".format(self.smartvisu_version))
-
def handle_heading_attributes(self, room):
if 'sv_heading_right' in room.conf:
heading_right = room.conf['sv_heading_right']
@@ -194,7 +185,6 @@ def handle_heading_attributes(self, room):
heading = ''
return heading
-
def get_widgetblocksize(self, item):
"""
Returns the blocksize for the block in which the item is to be displayed.
@@ -206,13 +196,12 @@ def get_widgetblocksize(self, item):
"""
if 'sv_blocksize' in item.conf:
blocksize = item.conf['sv_blocksize']
- if not blocksize in ['1','2','3']:
+ if blocksize not in ['1', '2', '3']:
blocksize = '2'
else:
blocksize = '2'
return blocksize
-
def get_attribute(self, attr, item):
if attr in item.conf:
attrvalue = item.conf[attr]
@@ -220,14 +209,12 @@ def get_attribute(self, attr, item):
attrvalue = ''
return attrvalue
-
def room(self, room):
"""
Interpretation of the room-specific item-attributes.
This routine is called once per 'sv_page'.
:param room: Items (with room configuration)
- :param tpldir: Directory where the template files are stored (within smartVISU)
:return: html code to be included in the visu file for the room
:rtype: str
@@ -292,7 +279,6 @@ def room(self, room):
r = self.parse_tpl('roomlite.html', [('{{ visu_name }}', str(room)), ('{{ visu_widgets }}', widgets), ('{{ visu_img }}', rimg), ('{{ visu_heading }}', heading)])
return r
-
def pages(self):
if not self.remove_oldpages():
return
@@ -341,7 +327,6 @@ def pages(self):
lite_lis += self.parse_tpl('navi.html', [('{{ visu_page }}', item.property.path), ('{{ visu_name }}', str(item)), ('{{ visu_img }}', img), ('{{ visu_aside }}', nav_aside), ('{{ visu_aside2 }}', nav_aside2), ('item.name', str(item)), ("'item", "'" + item.property.path)])
self.write_parseresult(item.property.path+'.html', r)
-
nav = self.parse_tpl('navigation.html', [('{{ visu_navis }}', nav_lis)])
self.write_parseresult('room_nav.html', nav)
@@ -351,7 +336,6 @@ def pages(self):
nav = self.parse_tpl('navigation.html', [('{{ visu_navis }}', lite_lis)])
self.write_parseresult('roomlite_nav.html', nav)
-
self.copy_tpl('rooms.html')
self.copy_tpl('roomslite.html')
self.copy_tpl('category.html')
@@ -373,7 +357,6 @@ def parse_tpl(self, template, replace):
tpl = tpl.replace(s, r)
return tpl
-
def write_parseresult(self, htmlfile, parseresult):
try:
with open(self.outdir + '/' + htmlfile, 'w') as f:
@@ -381,14 +364,13 @@ def write_parseresult(self, htmlfile, parseresult):
except Exception as e:
self.logger.warning("Could not write to {0}/{1}: {2}".format(self.outdir, htmlfile, e))
-
def copy_tpl(self, tplname, destname=''):
if destname == '':
destname = tplname
try:
shutil.copy(self.tpldir + '/' + tplname, self.outdir + '/' + destname)
except Exception as e:
- self.logger.error("Could not copy {0} from {1} to {2}".format(tplname, tpldir, destdir))
+ self.logger.error("Could not copy {0} from {1} to {2}".format(tplname, self.tpldir, self.outdir))
#########################################################################
@@ -435,9 +417,9 @@ def copy_templates(self):
if self.smartvisu_version == '2.9':
for fn in os.listdir(srcdir):
- if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn)) ):
+ if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn))):
self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{}".format(fn, self.smartvisu_version))
- shutil.copy2( os.path.join(srcdir, fn), self.tpldir )
+ shutil.copy2(os.path.join(srcdir, fn), self.tpldir)
else:
# create output directory
@@ -450,10 +432,10 @@ def copy_templates(self):
# Open file for twig import statements (for root.html)
for fn in os.listdir(srcdir):
- if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn)) ):
+ if (self.overwrite_templates) or (not os.path.isfile(os.path.join(self.tpldir, fn))):
self.logger.debug("copy_templates: Copying template '{}' from plugin to smartVISU v{}".format(fn, self.smartvisu_version))
try:
- shutil.copy2( os.path.join(srcdir, fn), self.tpldir )
+ shutil.copy2(os.path.join(srcdir, fn), self.tpldir)
except Exception as e:
self.logger.error("Could not copy {0} from {1} to {2}".format(fn, srcdir, self.tpldir))
return
@@ -487,17 +469,16 @@ def __init__(self, smarthome, smartvisu_dir='', smartvisu_version=''):
self.install_widgets(self._sh)
-
def install_widgets(self, smarthome):
if not self.remove_oldfiles():
return
if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8':
# make a backup copy of root.html if it doesn't exist (for full integeration)
- if not os.path.isfile( self.pgbdir + '/root_master.html' ):
- self.logger.warning( "install_widgets: Creating a copy of root.html" )
+ if not os.path.isfile(self.pgbdir + '/root_master.html'):
+ self.logger.warning("install_widgets: Creating a copy of root.html")
try:
- shutil.copy2( self.pgbdir + '/root.html', self.pgbdir + '/root_master.html' )
+ shutil.copy2(self.pgbdir + '/root.html', self.pgbdir + '/root_master.html')
except Exception as e:
self.logger.error("Could not copy {} from {} to {}".format('root.html', self.pgbdir, self.pgbdir + '/root_master.html'))
return
@@ -506,17 +487,17 @@ def install_widgets(self, smarthome):
f_root = open(self.pgbdir + '/root_master.html', "r")
root_contents = f_root.readlines()
f_root.close()
- self.logger.debug( "root_contents: {0}".format(root_contents) )
+ self.logger.debug("root_contents: {0}".format(root_contents))
# find insert points in original root.html
- iln_html = self.findinsertline( root_contents, '{% import "plot.html" as plot %}' )
- iln_js = self.findinsertline( root_contents, "{% if isfile('pages/'~config_pages~'/visu.js') %}" )
- iln_css = self.findinsertline( root_contents, "{% if isfile('pages/'~config_pages~'/visu.css') %}" )
+ iln_html = self.findinsertline(root_contents, '{% import "plot.html" as plot %}')
+ iln_js = self.findinsertline(root_contents, "{% if isfile('pages/'~config_pages~'/visu.js') %}")
+ iln_css = self.findinsertline(root_contents, "{% if isfile('pages/'~config_pages~'/visu.css') %}")
# copy widgets from plugin directories of configured plugins
# read plungin.conf
_conf = lib.config.parse(smarthome._plugin_conf)
- self.logger.debug( "install_widgets: _conf = {}".format(str(_conf)) )
+ self.logger.debug("install_widgets: _conf = {}".format(str(_conf)))
mypluginlist = []
for plugin in _conf:
self.logger.debug("install_widgets: Plugin section '{}', class_path = '{}', plugin_name = '{}'".format(plugin, str(_conf[plugin].get('class_path', '')), str(_conf[plugin].get('plugin_name', ''))))
@@ -525,15 +506,15 @@ def install_widgets(self, smarthome):
plgdir = 'plugins.' + _conf[plugin].get('plugin_name', '')
if plgdir not in mypluginlist:
# process each plugin only once
- mypluginlist.append( plgdir )
+ mypluginlist.append(plgdir)
if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8':
- self.copy_widgets( plgdir.replace('.', '/'), root_contents, iln_html, iln_js, iln_css )
+ self.copy_widgets(plgdir.replace('.', '/'), root_contents, iln_html, iln_js, iln_css)
else:
- self.copy_widgets( plgdir.replace('.', '/') )
+ self.copy_widgets(plgdir.replace('.', '/'))
if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8':
# write root.html with additions for widgets
- self.logger.info( "Adding import statements to root.html" )
+ self.logger.info("Adding import statements to root.html")
f_root = open(self.pgbdir + '/root.html', "w")
root_contents = "".join(root_contents)
f_root.write(root_contents)
@@ -542,16 +523,15 @@ def install_widgets(self, smarthome):
#########################################################################
- def findinsertline(self, root_contents, searchstring ):
+ def findinsertline(self, root_contents, searchstring):
# look for insert point in root.html: find and return line that contains the searchstring
iln = ''
for ln in root_contents:
- if ln.find( searchstring ) != -1:
+ if ln.find(searchstring) != -1:
iln = ln
if iln == '':
self.logger.warning("findinsertline: No insert point for pattern {0}".format(searchstring))
- return( iln )
-
+ return(iln)
def copy_widgets(self, plgdir, root_contents='', iln_html='', iln_js='', iln_css=''):
wdgdir = 'sv_widgets'
@@ -569,47 +549,45 @@ def copy_widgets(self, plgdir, root_contents='', iln_html='', iln_js='', iln_css
if self.smartvisu_version == '2.9':
if os.path.splitext(fn)[1] == '.png':
# copy icons to the icons directory
- shutil.copy2( os.path.join(srcdir, fn), self.icndir )
+ shutil.copy2(os.path.join(srcdir, fn), self.icndir)
else:
# the rest to the widgets directory & strip 'widgets_' from name
if fn.startswith('widget_'):
dn = fn[len('widget_'):]
- shutil.copy2( os.path.join(srcdir, fn), os.path.join(self.outdir, dn) )
+ shutil.copy2(os.path.join(srcdir, fn), os.path.join(self.outdir, dn))
else:
- shutil.copy2( os.path.join(srcdir, fn), self.outdir )
+ shutil.copy2(os.path.join(srcdir, fn), self.outdir)
else:
# v2.7 & v2.8
- shutil.copy2( srcdir + '/' + fn, self.outdir )
+ shutil.copy2(srcdir + '/' + fn, self.outdir)
if self.smartvisu_version == '2.7' or self.smartvisu_version == '2.8':
if (fn[0:7] == "widget_") and (fn[-5:] == ".html"):
self.logger.info("- Installing for SV v{} from '{}': {}".format(self.smartvisu_version, plgdir, '\t' + fn))
if iln_html != '':
- self.create_htmlinclude(fn, fn[7:-5] , root_contents, iln_html)
+ self.create_htmlinclude(fn, fn[7:-5], root_contents, iln_html)
if (fn[0:7] == "widget_") and (fn[-3:] == ".js"):
if iln_js != '':
- self.create_jsinclude(fn, fn[7:-3] , root_contents, iln_js)
+ self.create_jsinclude(fn, fn[7:-3], root_contents, iln_js)
if (fn[0:7] == "widget_") and (fn[-4:] == ".css"):
if iln_css != '':
- self.create_cssinclude(fn, fn[7:-4] , root_contents, iln_css)
+ self.create_cssinclude(fn, fn[7:-4], root_contents, iln_css)
return
-
def create_htmlinclude(self, filename, classname, root_contents, iln_html):
insertln = root_contents.index(iln_html) +1
# Insert widget statements to root_contents
if insertln != 0:
- self.logger.debug( "create_htmlinclude: Inserting in root.html at line {0} after '{1}'".format(insertln, iln_html) )
+ self.logger.debug("create_htmlinclude: Inserting in root.html at line {0} after '{1}'".format(insertln, iln_html))
twig_statement = '\t{% import "' + self.shwdgdir + '/' + filename + '" as ' + classname + ' %}'
root_contents.insert(insertln, twig_statement+'\n')
-
def create_jsinclude(self, filename, classname, root_contents, iln_js):
insertln = root_contents.index(iln_js)
# Insert widget statements to root_contents
if insertln > -1:
- self.logger.debug( "create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_js) )
+ self.logger.debug("create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_js))
twig_statement1 = "\t{% if isfile('widgets/sh_widgets/" + filename + "') %}"
twig_statement2 = '\t\t{% endif %}'
self.logger.debug('create_jsinclude: {0}'.format(twig_statement1))
@@ -617,12 +595,11 @@ def create_jsinclude(self, filename, classname, root_contents, iln_js):
root_contents.insert(insertln, twig_statement2+'\n')
root_contents.insert(insertln, twig_statement1+'\n')
-
def create_cssinclude(self, filename, classname, root_contents, iln_css):
insertln = root_contents.index(iln_css)
# Insert widget statements to root_contents
if insertln > -1:
- self.logger.debug( "create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_css) )
+ self.logger.debug("create_jsinclude: Inserting in root.html at line {0} before '{1}'".format(insertln, iln_css))
twig_statement1 = "\t{% if isfile('widgets/sh_widgets/" + filename + "') %}"
twig_statement2 = '\t\t{% endif %}'
self.logger.debug('create_cssinclude: {0}'.format(twig_statement1))
@@ -630,7 +607,6 @@ def create_cssinclude(self, filename, classname, root_contents, iln_css):
root_contents.insert(insertln, twig_statement2+'\n')
root_contents.insert(insertln, twig_statement1+'\n')
-
def remove_oldfiles(self):
# clear temp directory
if not os.path.isdir(self.tmpdir):
@@ -668,4 +644,3 @@ def remove_oldfiles(self):
self.logger.warning("Could not delete file {0}: {1}".format(fp, e))
return True
-
diff --git a/withings_health/user_doc.rst b/withings_health/user_doc.rst
index 5cada8c16..98395b516 100644
--- a/withings_health/user_doc.rst
+++ b/withings_health/user_doc.rst
@@ -23,7 +23,8 @@ Vorbereitung
Dieses Plugin benötigt die withings-api.
Sie müssen sich unter `Withings Account `_ registrieren und im Dashboard
-eine Applikation anlegen. Der Name ist frei wählbar, die (lokale) Callback-URL wird über die Weboberfläche des Plugins angezeigt: http://:/plugin/withings_health.
+eine Applikation anlegen. Der Name ist frei wählbar, die (lokale) Callback-URL wird über die Weboberfläche des Plugins
+angezeigt: http://:/plugin/withings_health (Die IP sollte die von SmartHomeNG sein!).
Wenn Sie sich bei der `Withings App `_ einloggen, kann die achtstellige Zahl
in der URL ausgelesen und in der Pluginkonfiguration als user_id angegeben werden.
diff --git a/yamahayxc/__init__.py b/yamahayxc/__init__.py
index a2ec97899..35975be9f 100755
--- a/yamahayxc/__init__.py
+++ b/yamahayxc/__init__.py
@@ -29,7 +29,6 @@
# - parse zone().range_step -> read range/step for vol / eq
-import logging
import requests
import socket
import json
@@ -53,20 +52,20 @@ class YamahaYXC(SmartPlugin):
"""
This is the main plugin class YamahaYXC to control YXC-compatible devices.
"""
- PLUGIN_VERSION = "1.0.6"
+ PLUGIN_VERSION = "1.0.7"
ALLOW_MULTIINSTANCE = False
#
# public functions
#
- def __init__(self, smarthome):
+ def __init__(self, smarthome, **kwargs):
"""
Default init function
"""
- self.logger = logging.getLogger(__name__)
- self.logger.info("Init YamahaYXC")
+ super().__init__(**kwargs)
self._sh = smarthome
+ self.logger.info("Init YamahaYXC")
# valid commands for use in item configuration 'yamahayxc_cmd = ...'
self._yamaha_cmds = ['state', 'power', 'input', 'playback', 'preset',
@@ -79,7 +78,7 @@ def __init__(self, smarthome):
self._yamaha_ignore_cmds_upd = ['state', 'preset', 'alarm_on', 'alarm_time', 'alarm_beep']
# store items in 2D-array:
- # _yamaha_dev [host] [cmd] = item
+ # _yamaha_dev [host] [cmd] = item
# also see parse_item()...
self._yamaha_dev = {}
# store host addresses of devices
@@ -106,13 +105,13 @@ def run(self):
data, addr = self.sock.recvfrom(self.srv_buffer)
try:
host, port = addr
- except:
+ except Exception as e:
self.logger.warn(
- "Error receiving data - host/port not readable")
+ f"Error receiving data - host/port not readable: {e}")
return
if host not in list(self._yamaha_dev.keys()):
self.logger.debug(
- "Received notify from unknown host {}".format(host))
+ f"Received notify from unknown host {host}")
else:
# connected device sends updates every second for
# about 10 minutes without further interaction
@@ -128,11 +127,11 @@ def run(self):
data_flat = {}
try:
data_flat.update(data['main'])
- except:
+ except Exception:
pass
try:
data_flat.update(data['netusb'])
- except:
+ except Exception:
pass
# try all known command words...
@@ -145,7 +144,7 @@ def run(self):
notify_val = self._convert_value_yxc_to_plugin(data_flat[cmd], cmd, host)
item = self._yamaha_dev[host][cmd]
item(notify_val, "YamahaYXC")
- except:
+ except Exception:
pass
# device told us new info is available?
@@ -171,7 +170,7 @@ def stop(self):
self.alive = False
try:
self.sock.shutdown(socket.SHUT_RDWR)
- except:
+ except Exception:
pass
def parse_item(self, item):
@@ -352,7 +351,7 @@ def _update_alarm_state(self, yamaha_host, update_items=True):
alarm = state["alarm"]
except KeyError:
return
-
+
alarm_on = alarm["alarm_on"]
alarm_time = alarm["oneday"]["time"]
alarm_beep = alarm["oneday"]["beep"]
diff --git a/yamahayxc/plugin.yaml b/yamahayxc/plugin.yaml
index 9da7b64bf..57eff09f8 100755
--- a/yamahayxc/plugin.yaml
+++ b/yamahayxc/plugin.yaml
@@ -64,7 +64,7 @@ plugin:
# documentation: https://github.com/smarthomeNG/smarthome/wiki/CLI-Plugin # url of documentation (wiki) page
support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1174064-plugin-yamaha-musiccast-geräte-neuere-generation
- version: 1.0.6 # Plugin version
+ version: 1.0.7 # Plugin version
sh_minversion: '1.3' # 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