From 2453af03704454f7cfed126560bc08c21781f68e Mon Sep 17 00:00:00 2001
From: spacemanspiff2007 <->
Date: Wed, 6 Nov 2019 11:04:02 +0100
Subject: [PATCH] 0.10.1 - Openhab Tests log to own file - Serialized rule
(un-) loading (fixes #80) - MyPy fixes - Added documentation for
set_file_validator - added func get_path for FileEvents - Gracefully shutdown
aiohttp on shutdown of HABApp
---
.gitignore | 1 +
HABApp/__main__.py | 6 +-
HABApp/__version__.py | 2 +-
HABApp/config/config.py | 6 +-
HABApp/core/events/habapp_events.py | 11 +++-
HABApp/mqtt/mqtt_connection.py | 3 +-
HABApp/mqtt/mqtt_interface.py | 3 +-
HABApp/openhab/definitions/values.py | 15 +++--
HABApp/openhab/http_connection.py | 8 +--
HABApp/openhab/items/map_items.py | 3 +-
HABApp/openhab/oh_connection.py | 9 ++-
HABApp/parameters/__init__.py | 3 +-
HABApp/parameters/parameter_files.py | 13 ++--
HABApp/parameters/parameters.py | 21 ++++--
HABApp/rule/interfaces/rule_subprocess.py | 16 +++--
HABApp/rule/rule.py | 2 +
HABApp/rule/scheduler/reoccurring_cb.py | 4 +-
HABApp/rule_manager/rule_file.py | 6 +-
HABApp/rule_manager/rule_manager.py | 65 +++++++++++--------
HABApp/util/multimode_item.py | 12 ++--
_doc/parameters.rst | 39 ++++++++++-
conf_testing/logging.yml | 15 +++++
conf_testing/rules/bench_rule.py | 2 +-
conf_testing/rules/test_openhab_item_funcs.py | 2 +-
setup.py | 21 ++++--
tests/test_rule/test_process.py | 15 +++++
tests/test_utils/test_timeframe.py | 2 +
27 files changed, 209 insertions(+), 96 deletions(-)
diff --git a/.gitignore b/.gitignore
index 0a3c9c29..00293648 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.mypy_cache
.idea
__pycache__
/conf
diff --git a/HABApp/__main__.py b/HABApp/__main__.py
index 7cb67004..47781b8b 100644
--- a/HABApp/__main__.py
+++ b/HABApp/__main__.py
@@ -91,7 +91,6 @@ def main() -> typing.Union[int, str]:
if args.NoMQTTConnectionErrors is True:
HABApp.mqtt.MqttInterface._RAISE_CONNECTION_ERRORS = False
- loop = None
log = logging.getLogger('HABApp')
# if installed we use uvloop because it seems to be much faster (untested)
@@ -113,7 +112,7 @@ def main() -> typing.Union[int, str]:
# https://docs.python.org/3/library/asyncio-subprocess.html#subprocess-and-threads
asyncio.get_child_watcher()
- loop = asyncio.get_event_loop()
+ loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
loop.set_debug(True)
loop.slow_callback_duration = 0.02
@@ -142,6 +141,9 @@ def shutdown_handler(sig, frame):
print(e)
return str(e)
finally:
+ # Sleep to allow underlying connections of aiohttp to close
+ # https://aiohttp.readthedocs.io/en/stable/client_advanced.html#graceful-shutdown
+ loop.run_until_complete(asyncio.sleep(1))
loop.close()
return 0
diff --git a/HABApp/__version__.py b/HABApp/__version__.py
index 22ec42ba..5097cb67 100644
--- a/HABApp/__version__.py
+++ b/HABApp/__version__.py
@@ -1 +1 @@
-__VERSION__ = '0.10.0'
+__VERSION__ = '0.10.1'
diff --git a/HABApp/config/config.py b/HABApp/config/config.py
index 5515b326..d410844a 100644
--- a/HABApp/config/config.py
+++ b/HABApp/config/config.py
@@ -17,10 +17,10 @@
_yaml_param = ruamel.yaml.YAML(typ='safe')
_yaml_param.default_flow_style = False
-_yaml_param.default_style = False
-_yaml_param.width = 1000000
+_yaml_param.default_style = False # type: ignore
+_yaml_param.width = 1000000 # type: ignore
_yaml_param.allow_unicode = True
-_yaml_param.sort_base_mapping_type_on_output = False
+_yaml_param.sort_base_mapping_type_on_output = False # type: ignore
log = logging.getLogger('HABApp.Config')
diff --git a/HABApp/core/events/habapp_events.py b/HABApp/core/events/habapp_events.py
index 63512252..9d193bca 100644
--- a/HABApp/core/events/habapp_events.py
+++ b/HABApp/core/events/habapp_events.py
@@ -11,9 +11,12 @@ class RequestFileLoadEvent:
def from_path(cls, folder: Path, file: Path) -> 'RequestFileLoadEvent':
return cls(str(file.relative_to(folder)))
- def __init__(self, name: str = None):
+ def __init__(self, name: str):
self.filename: str = name
+ def get_path(self, parent_folder: Path) -> Path:
+ return parent_folder / self.filename
+
def __repr__(self):
return f'<{self.__class__.__name__} filename: {self.filename}>'
@@ -28,9 +31,13 @@ class RequestFileUnloadEvent:
def from_path(cls, folder: Path, file: Path) -> 'RequestFileUnloadEvent':
return cls(str(file.relative_to(folder)))
- def __init__(self, name: str = None):
+ def __init__(self, name: str):
self.filename: str = name
+ def get_path(self, parent_folder: Path) -> Path:
+ return parent_folder / self.filename
+
+
def __repr__(self):
return f'<{self.__class__.__name__} filename: {self.filename}>'
diff --git a/HABApp/mqtt/mqtt_connection.py b/HABApp/mqtt/mqtt_connection.py
index 23dcde20..8eb4d7f7 100644
--- a/HABApp/mqtt/mqtt_connection.py
+++ b/HABApp/mqtt/mqtt_connection.py
@@ -1,4 +1,5 @@
import logging
+import typing
import ujson
import paho.mqtt.client as mqtt
@@ -24,7 +25,7 @@ def __init__(self, mqtt_config: MqttConfig, shutdown_helper: ShutdownHelper):
self.client: mqtt.Client = None
- self.subscriptions = []
+ self.subscriptions: typing.List[typing.Tuple[str, int]] = []
# config changes
self.__config = mqtt_config
diff --git a/HABApp/mqtt/mqtt_interface.py b/HABApp/mqtt/mqtt_interface.py
index 38fca901..030b74fa 100644
--- a/HABApp/mqtt/mqtt_interface.py
+++ b/HABApp/mqtt/mqtt_interface.py
@@ -105,8 +105,7 @@ def unsubscribe(self, topic: str) -> int:
return result
-
-MQTT_INTERFACE: MqttInterface = None
+MQTT_INTERFACE: MqttInterface
def get_mqtt_interface(connection=None, config=None) -> MqttInterface:
diff --git a/HABApp/openhab/definitions/values.py b/HABApp/openhab/definitions/values.py
index 33a0b8d1..449d1a1c 100644
--- a/HABApp/openhab/definitions/values.py
+++ b/HABApp/openhab/definitions/values.py
@@ -1,3 +1,4 @@
+import typing
from HABApp.core.events import ComplexEventValue
@@ -16,9 +17,9 @@ def __str__(self):
class PercentValue(ComplexEventValue):
def __init__(self, value: str):
- value = float(value)
- assert 0 <= value <= 100, f'{value} ({type(value)})'
- super().__init__(value)
+ percent = float(value)
+ assert 0 <= percent <= 100, f'{percent} ({type(percent)})'
+ super().__init__(percent)
def __str__(self):
return f'{self.value}%'
@@ -47,14 +48,16 @@ def __str__(self):
class QuantityValue(ComplexEventValue):
def __init__(self, value: str):
- val, unit = value.split(' ')
+ str_val, unit = value.split(' ')
+
try:
- val = int(val)
+ val: typing.Union[int, float] = int(str_val)
except ValueError:
- val = float(val)
+ val = float(str_val)
super().__init__(val)
self.unit = unit
+
def __str__(self):
return f'{self.value} {self.unit}'
diff --git a/HABApp/openhab/http_connection.py b/HABApp/openhab/http_connection.py
index 501ded4d..58ca0cc9 100644
--- a/HABApp/openhab/http_connection.py
+++ b/HABApp/openhab/http_connection.py
@@ -103,8 +103,8 @@ def _is_disconnect_exception(self, e) -> bool:
self.__set_offline(str(e))
return True
- async def _check_http_response(self, future, additional_info="",
- accept_404=False) -> typing.Optional[ ClientResponse]:
+ async def _check_http_response(self, future: aiohttp.client._RequestContextManager, additional_info="",
+ accept_404=False) -> ClientResponse:
try:
resp = await future
except Exception as e:
@@ -112,8 +112,7 @@ async def _check_http_response(self, future, additional_info="",
log.log(logging.WARNING if is_disconnect else logging.ERROR, f'"{e}" ({type(e)})')
if is_disconnect:
raise OpenhabDisconnectedError()
- else:
- return None
+ raise
# Server Errors if openhab is not ready yet
if resp.status >= 500:
@@ -290,6 +289,7 @@ async def async_get_items(self) -> typing.Optional[list]:
if not isinstance(e, (OpenhabDisconnectedError, OpenhabNotReadyYet)):
for l in traceback.format_exc().splitlines():
log.error(l)
+ return None
async def async_get_item(self, item_name: str) -> dict:
fut = self.__session.get(self.__get_openhab_url('rest/items/{:s}', item_name))
diff --git a/HABApp/openhab/items/map_items.py b/HABApp/openhab/items/map_items.py
index a6b095cf..d3bbeded 100644
--- a/HABApp/openhab/items/map_items.py
+++ b/HABApp/openhab/items/map_items.py
@@ -1,4 +1,5 @@
import datetime
+import typing
from HABApp.core.items import Item
from . import SwitchItem, ContactItem, RollershutterItem, DimmerItem, ColorItem, NumberItem
@@ -8,7 +9,7 @@ def map_items(name, openhab_type : str, openhab_value : str):
assert isinstance(openhab_type, str), type(openhab_type)
assert isinstance(openhab_value, str), type(openhab_value)
- value = openhab_value
+ value: typing.Optional[str] = openhab_value
if openhab_value == 'NULL' or openhab_value == 'UNDEF':
value = None
diff --git a/HABApp/openhab/oh_connection.py b/HABApp/openhab/oh_connection.py
index 72d7be1a..6fabcdc8 100644
--- a/HABApp/openhab/oh_connection.py
+++ b/HABApp/openhab/oh_connection.py
@@ -14,7 +14,6 @@
log = logging.getLogger('HABApp.openhab.Connection')
-
class OpenhabConnection(HttpConnectionEventHandler):
def __init__(self, config, shutdown):
@@ -99,11 +98,11 @@ async def async_ping(self):
- def on_sse_event(self, event: dict):
+ def on_sse_event(self, event_dict: dict):
try:
# Lookup corresponding OpenHAB event
- event = get_event(event)
+ event = get_event(event_dict)
# Events which change the ItemRegistry
if isinstance(event, (HABApp.openhab.events.ItemAddedEvent, HABApp.openhab.events.ItemUpdatedEvent)):
@@ -140,7 +139,7 @@ def on_sse_event(self, event: dict):
@PrintException
- async def update_all_items(self) -> int:
+ async def update_all_items(self):
try:
data = await self.connection.async_get_items()
@@ -176,4 +175,4 @@ async def update_all_items(self) -> int:
log.error(e)
for l in traceback.format_exc().splitlines():
log.error(l)
- return 0
+ return None
diff --git a/HABApp/parameters/__init__.py b/HABApp/parameters/__init__.py
index ca89781d..3393e751 100644
--- a/HABApp/parameters/__init__.py
+++ b/HABApp/parameters/__init__.py
@@ -1,2 +1,3 @@
-from .parameter import Parameter
\ No newline at end of file
+from .parameter import Parameter
+from .parameters import set_file_validator
diff --git a/HABApp/parameters/parameter_files.py b/HABApp/parameters/parameter_files.py
index 17803558..2751650f 100644
--- a/HABApp/parameters/parameter_files.py
+++ b/HABApp/parameters/parameter_files.py
@@ -1,6 +1,7 @@
import logging
import threading
import traceback
+import typing
import ruamel.yaml
@@ -11,14 +12,14 @@
_yml_setup = ruamel.yaml.YAML()
_yml_setup.default_flow_style = False
-_yml_setup.default_style = False
-_yml_setup.width = 1000000
+_yml_setup.default_style = False # type: ignore
+_yml_setup.width = 1000000 # type: ignore
_yml_setup.allow_unicode = True
-_yml_setup.sort_base_mapping_type_on_output = False
+_yml_setup.sort_base_mapping_type_on_output = False # type: ignore
LOCK = threading.Lock()
HABAPP_PARAM_TOPIC = 'HABApp.Parameters'
-CONFIG = None
+CONFIG = None # type: typing.Optional[HABApp.config.Config]
def setup_param_files(config, folder_watcher):
@@ -56,7 +57,7 @@ def setup_param_files(config, folder_watcher):
def load_file(event: HABApp.core.events.habapp_events.RequestFileLoadEvent):
- path = CONFIG.directories.param / event.filename
+ path = event.get_path(CONFIG.directories.param)
with LOCK: # serialize to get proper error messages
try:
@@ -75,7 +76,7 @@ def load_file(event: HABApp.core.events.habapp_events.RequestFileLoadEvent):
def unload_file(event: HABApp.core.events.habapp_events.RequestFileUnloadEvent):
- path = CONFIG.directories.param / event.filename
+ path = event.get_path(CONFIG.directories.param)
with LOCK: # serialize to get proper error messages
try:
diff --git a/HABApp/parameters/parameters.py b/HABApp/parameters/parameters.py
index f5a8f3fd..d1ecdcb1 100644
--- a/HABApp/parameters/parameters.py
+++ b/HABApp/parameters/parameters.py
@@ -24,23 +24,31 @@ def get_parameter_file(file: str):
return _PARAMETERS[file]
-def set_file_validator(file: str, validator: typing.Any, allow_extra_keys=True):
+def set_file_validator(filename: str, validator: typing.Any, allow_extra_keys=True):
+ """Add a validator for the parameter file. If the file is already loaded this will reload the file.
+
+ :param filename: filename which shall be validated (without extension)
+ :param validator: Description of file content - see the library
+ `voluptuous `_ for examples.
+ Use `None` to remove validator.
+ :param allow_extra_keys: Allow additional keys in the file structure
+ """
# Remove validator
if validator is None:
- _VALIDATORS.pop(file, None)
+ _VALIDATORS.pop(filename, None)
return
# Set validator
- old_validator = _VALIDATORS.get(file)
- _VALIDATORS[file] = new_validator = voluptuous.Schema(
+ old_validator = _VALIDATORS.get(filename)
+ _VALIDATORS[filename] = new_validator = voluptuous.Schema(
validator, required=True, extra=(voluptuous.ALLOW_EXTRA if allow_extra_keys else voluptuous.PREVENT_EXTRA)
)
# todo: move this to file handling so we get the extension
if old_validator != new_validator:
HABApp.core.EventBus.post_event(
- HABAPP_PARAM_TOPIC, HABApp.core.events.habapp_events.RequestFileLoadEvent(file + '.yml')
+ HABAPP_PARAM_TOPIC, HABApp.core.events.habapp_events.RequestFileLoadEvent(filename + '.yml')
)
@@ -50,7 +58,8 @@ def add_parameter(file: str, *keys, default_value):
if file not in _PARAMETERS:
save = True
- _PARAMETERS[file] = param = {}
+ param: typing.Dict[str, typing.Any] = {}
+ _PARAMETERS[file] = param
else:
param = _PARAMETERS[file]
diff --git a/HABApp/rule/interfaces/rule_subprocess.py b/HABApp/rule/interfaces/rule_subprocess.py
index 65c322d2..75ba0fd5 100644
--- a/HABApp/rule/interfaces/rule_subprocess.py
+++ b/HABApp/rule/interfaces/rule_subprocess.py
@@ -1,16 +1,17 @@
import asyncio
+import typing
class FinishedProcessInfo:
"""Information about the finished process."""
- def __init__(self, returncode: int, stdout: str, stderr: str):
+ def __init__(self, returncode: int, stdout: typing.Optional[str], stderr: typing.Optional[str]):
self.returncode: int = returncode
- self.stdout: str = stdout
- self.stderr: str = stderr
+ self.stdout: typing.Optional[str] = stdout
+ self.stderr: typing.Optional[str] = stderr
def __repr__(self):
- return f''
+ return f''
async def async_subprocess_exec(callback, program: str, *args, capture_output=True):
@@ -29,10 +30,11 @@ async def async_subprocess_exec(callback, program: str, *args, capture_output=Tr
stderr=asyncio.subprocess.PIPE if capture_output else None
)
- stdout, stderr = await proc.communicate()
- stdout = stdout.decode()
- stderr = stderr.decode()
+ b_stdout, b_stderr = await proc.communicate()
ret_code = proc.returncode
+ if capture_output:
+ stdout = b_stdout.decode()
+ stderr = b_stderr.decode()
except asyncio.CancelledError:
if proc is not None:
proc.terminate()
diff --git a/HABApp/rule/rule.py b/HABApp/rule/rule.py
index 1f713b01..66ccd35b 100644
--- a/HABApp/rule/rule.py
+++ b/HABApp/rule/rule.py
@@ -21,11 +21,13 @@
log = logging.getLogger('HABApp.Rule')
+# Func to log deprecation warnings
def send_warnings_to_log(message, category, filename, lineno, file=None, line=None):
log.warning('%s:%s: %s:%s' % (filename, lineno, category.__name__, message))
return
+# Setup deprecation warnings
warnings.simplefilter('default')
warnings.showwarning = send_warnings_to_log
diff --git a/HABApp/rule/scheduler/reoccurring_cb.py b/HABApp/rule/scheduler/reoccurring_cb.py
index a21e7fbf..9ff80299 100644
--- a/HABApp/rule/scheduler/reoccurring_cb.py
+++ b/HABApp/rule/scheduler/reoccurring_cb.py
@@ -17,7 +17,7 @@ def __init__(self, time: TYPING_DATE_TIME, interval: typing.Union[int, datetime.
assert isinstance(interval, datetime.timedelta), type(interval)
self.time_interval = interval
- def check_due(self, now: datetime):
+ def check_due(self, now: datetime.datetime):
super().check_due(now)
if self.is_due:
self.next_call += self.time_interval
@@ -40,7 +40,7 @@ def __init__(self, time: TYPING_DATE_TIME, weekdays: typing.List[int], callback,
while not self.next_call.isoweekday() in self.weekdays:
self.next_call += datetime.timedelta(days=1)
- def check_due(self, now: datetime):
+ def check_due(self, now: datetime.datetime):
super().check_due(now)
if self.is_due:
self.next_call += datetime.timedelta(days=1)
diff --git a/HABApp/rule_manager/rule_file.py b/HABApp/rule_manager/rule_file.py
index e182bf15..80c0cd17 100644
--- a/HABApp/rule_manager/rule_file.py
+++ b/HABApp/rule_manager/rule_file.py
@@ -18,11 +18,11 @@ def __init__(self, rule_manager, path: Path):
assert isinstance(rule_manager, RuleManager)
self.rule_manager = rule_manager
- self.path = path
+ self.path: Path = path
- self.rules = {}
+ self.rules = {} # type: typing.Dict[str, HABApp.Rule]
- self.class_ctr = collections.defaultdict(lambda : 1)
+ self.class_ctr: typing.Dict[str, int] = collections.defaultdict(lambda: 1)
def suggest_rule_name(self, obj) -> str:
diff --git a/HABApp/rule_manager/rule_manager.py b/HABApp/rule_manager/rule_manager.py
index 7c87d57a..5c272dd4 100644
--- a/HABApp/rule_manager/rule_manager.py
+++ b/HABApp/rule_manager/rule_manager.py
@@ -26,8 +26,8 @@ def __init__(self, parent):
self.files: typing.Dict[str, RuleFile] = {}
# serialize loading
- self.__file_load_lock = threading.Lock()
- self.__rulefiles_lock = threading.Lock()
+ self.__load_lock = threading.Lock()
+ self.__files_lock = threading.Lock()
# Processing
self.__process_last_sec = 60
@@ -96,7 +96,7 @@ async def process_scheduled_events(self):
# remember sec
self.__process_last_sec = now.second
- with self.__rulefiles_lock:
+ with self.__files_lock:
for file in self.files.values():
assert isinstance(file, RuleFile), type(file)
for rule in file.iterrules():
@@ -136,33 +136,38 @@ def get_rule(self, rule_name):
@PrintException
- def request_file_unload(self, event: HABApp.core.events.habapp_events.RequestFileUnloadEvent):
-
- path = self.runtime.config.directories.rules / event.filename
+ def request_file_unload(self, event: HABApp.core.events.habapp_events.RequestFileUnloadEvent, request_lock=True):
+ path = event.get_path(self.runtime.config.directories.rules)
path_str = str(path)
- # Only unload already loaded files
- if path_str not in self.files:
- log.warning(f'Rule file {path} is not yet loaded and therefore can not be unloaded')
- return None
-
try:
- with self.__file_load_lock:
- log.debug(f'Removing file: {path}')
- if path_str in self.files:
- with self.__rulefiles_lock:
- rule = self.files.pop(path_str)
- rule.unload()
+ if request_lock:
+ self.__load_lock.acquire()
+
+ # Only unload already loaded files
+ with self.__files_lock:
+ already_loaded = path_str in self.files
+ if not already_loaded:
+ log.warning(f'Rule file {path} is not yet loaded and therefore can not be unloaded')
+ return None
+
+ log.debug(f'Removing file: {path}')
+ with self.__files_lock:
+ rule = self.files.pop(path_str)
+ rule.unload()
except Exception:
log.error(f"Could not remove {path}!")
for l in traceback.format_exc().splitlines():
log.error(l)
return None
+ finally:
+ if request_lock:
+ self.__load_lock.release()
@PrintException
def request_file_load(self, event: HABApp.core.events.habapp_events.RequestFileLoadEvent):
- path = self.runtime.config.directories.rules / event.filename
+ path = event.get_path(self.runtime.config.directories.rules)
path_str = str(path)
# Only load existing files
@@ -170,23 +175,27 @@ def request_file_load(self, event: HABApp.core.events.habapp_events.RequestFileL
log.warning(f'Rule file {path} does not exist and can not be loaded!')
return None
- # Unload if we have already loaded
- if path_str in self.files:
- self.request_file_unload(event)
-
try:
# serialize loading
- with self.__file_load_lock:
-
- log.debug(f'Loading file: {path}')
- with self.__rulefiles_lock:
- self.files[path_str] = file = RuleFile(self, path)
- file.load()
+ self.__load_lock.acquire()
+
+ # Unload if we have already loaded
+ with self.__files_lock:
+ already_loaded = path_str in self.files
+ if already_loaded:
+ self.request_file_unload(event, request_lock=False)
+
+ log.debug(f'Loading file: {path}')
+ with self.__files_lock:
+ self.files[path_str] = file = RuleFile(self, path)
+ file.load()
except Exception:
log.error(f"Could not (fully) load {path}!")
for l in traceback.format_exc().splitlines():
log.error(l)
return None
+ finally:
+ self.__load_lock.release()
log.debug(f'File {path_str} successfully loaded!')
diff --git a/HABApp/util/multimode_item.py b/HABApp/util/multimode_item.py
index 1aedb5dc..91fb19b5 100644
--- a/HABApp/util/multimode_item.py
+++ b/HABApp/util/multimode_item.py
@@ -21,7 +21,7 @@ class MultiModeValue:
"""
DISABLE_OPERATORS = {
'>': operator.gt, '<': operator.lt, '>=': operator.ge, '<=': operator.le,
- '==': operator.eq, '!=': operator.ne, None: None
+ '==': operator.eq, '!=': operator.ne,
}
def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None, auto_disable_after=None,
@@ -45,9 +45,9 @@ def __init__(self, parent, name: str, initial_value=None, auto_disable_on=None,
assert isinstance(auto_disable_after, datetime.timedelta) or auto_disable_after is None, \
type(auto_disable_after)
- assert auto_disable_on in MultiModeValue.DISABLE_OPERATORS, auto_disable_on
+ assert auto_disable_on is None or auto_disable_on in MultiModeValue.DISABLE_OPERATORS, auto_disable_on
self.auto_disable_after: typing.Optional[datetime.timedelta] = auto_disable_after
- self.auto_disable_on: str = auto_disable_on
+ self.auto_disable_on: typing.Optional[str] = auto_disable_on
self.calc_value_func: typing.Optional[typing.Callable[[typing.Any, typing.Any], typing.Any]] = calc_value_func
@@ -151,7 +151,7 @@ class MultiModeItem(Item):
"""
@classmethod
- def get_create_item(cls, name: str, logger: logging.getLoggerClass() = None):
+ def get_create_item(cls, name: str, logger: logging.Logger = None):
item = super().get_create_item(name, None)
item.logger = logger
return item
@@ -164,7 +164,7 @@ def __init__(self, name: str, initial_value=None):
self.__lock = Lock()
- self.logger: typing.Optional[logging._loggerClass] = None
+ self.logger: typing.Optional[logging.Logger] = None
def log(self, level, text, *args, **kwargs):
if self.logger is not None:
@@ -204,7 +204,7 @@ def create_mode(
# make the lower priority known to the mode
low = None
- for priority, child in sorted(self.__values_by_prio.items()): # type: int, MultiModeValue
+ for _, child in sorted(self.__values_by_prio.items()): # type: int, MultiModeValue
child._set_lower_priority_mode(low)
low = child
diff --git a/_doc/parameters.rst b/_doc/parameters.rst
index 03270e32..5c554f8b 100644
--- a/_doc/parameters.rst
+++ b/_doc/parameters.rst
@@ -74,4 +74,41 @@ Changes in the file will be automatically picked up through :class:`~HABApp.para
.. autoclass:: HABApp.parameters.Parameter
:members:
- .. automethod:: __init__
\ No newline at end of file
+ .. automethod:: __init__
+
+Validation
+------------------------------
+Since parameters used to provide flexible configuration for automation classes they can get quite complex and
+error prone. Thus it is possible to provide a validator for a file which will check the files for constraints,
+missing keys etc. when the file is loaded.
+
+.. autofunction:: HABApp.parameters.set_file_validator
+
+Example
+
+.. execute_code::
+ :hide_output:
+
+ # hide
+ from HABApp.parameters.parameters import _PARAMETERS
+ _PARAMETERS['param_file_testrule'] = {'min_value': 10, 'Rule A': {'subkey1': {'subkey2': ['a', 'b', 'c']}}}
+ # hide
+
+ import HABApp
+ import voluptuous
+
+ # Validator can even and should be specified before loading rules
+
+ # allows a dict e.g. { 'key1': {'key2': '5}}
+ HABApp.parameters.set_file_validator('file1', {str: {str: int}})
+
+ # More complex example with an optional key:
+ validator = {
+ 'Test': int,
+ 'Key': {
+ 'mandatory': str,
+ voluptuous.Optional('optional'): int
+ }
+ }
+ HABApp.parameters.set_file_validator('file1', validator)
+
diff --git a/conf_testing/logging.yml b/conf_testing/logging.yml
index f8312a7f..11b5b78b 100644
--- a/conf_testing/logging.yml
+++ b/conf_testing/logging.yml
@@ -25,6 +25,15 @@ handlers:
formatter: HABApp_format
level: DEBUG
+ HABApp_test_file:
+ class: logging.handlers.RotatingFileHandler
+ filename: 'tests.log'
+ maxBytes: 10_485_760
+ backupCount: 3
+
+ formatter: HABApp_format
+ level: DEBUG
+
BufferEventFile:
class: logging.handlers.MemoryHandler
capacity: 0
@@ -57,3 +66,9 @@ loggers:
handlers:
- BufferEventFile
propagate: False
+
+ HABApp.Tests:
+ level: DEBUG
+ handlers:
+ - HABApp_test_file
+ propagate: False
diff --git a/conf_testing/rules/bench_rule.py b/conf_testing/rules/bench_rule.py
index ed9a6cfb..bab626af 100644
--- a/conf_testing/rules/bench_rule.py
+++ b/conf_testing/rules/bench_rule.py
@@ -41,7 +41,7 @@ def bench_stop(self, event):
ts_start = time.time()
while True:
for k in self.item_list:
- if self.get_item_state(k) != self.__b_val:
+ if HABApp.core.items.Item.get_item(k).value != self.__b_val:
break
else:
break
diff --git a/conf_testing/rules/test_openhab_item_funcs.py b/conf_testing/rules/test_openhab_item_funcs.py
index 8d8b8e8c..3d27255d 100644
--- a/conf_testing/rules/test_openhab_item_funcs.py
+++ b/conf_testing/rules/test_openhab_item_funcs.py
@@ -6,7 +6,7 @@
from HABApp.openhab.items import SwitchItem, RollershutterItem, DimmerItem, ColorItem
from HABAppTests import TestBaseRule, ItemWaiter, OpenhabTmpItem
-log = logging.getLogger('HABApp.Test')
+log = logging.getLogger('HABApp.Tests')
@dataclasses.dataclass
diff --git a/setup.py b/setup.py
index 74a67a6e..b24824ba 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,20 @@
+import typing
from pathlib import Path
+
import setuptools
-# Load version number
-version = {}
-with open("HABApp/__version__.py") as fp:
- exec(fp.read(), version)
-assert version
-assert version['__VERSION__']
-__VERSION__ = version['__VERSION__']
+
+# Load version number without importing HABApp
+def load_version() -> str:
+ version: typing.Dict[str, str] = {}
+ with open("HABApp/__version__.py") as fp:
+ exec(fp.read(), version)
+ assert version['__VERSION__'], version
+ return version['__VERSION__']
+
+
+__VERSION__ = load_version()
+
print(f'Version: {__VERSION__}')
print('')
diff --git a/tests/test_rule/test_process.py b/tests/test_rule/test_process.py
index fb02e997..9eb69172 100644
--- a/tests/test_rule/test_process.py
+++ b/tests/test_rule/test_process.py
@@ -46,6 +46,21 @@ def test_run_func(self):
self.assertEqual(self.ret.returncode, 0)
self.assertTrue(self.ret.stdout.startswith('20'))
+ def test_run_func_no_cap(self):
+ self.rule.execute_subprocess(
+ self.set_ret, sys.executable, '-c', 'import datetime; print(datetime.datetime.now())', capture_output=False
+ )
+
+ # Test this call from __main__ to create thread save process watchers
+ if sys.platform != "win32":
+ asyncio.get_child_watcher()
+
+ asyncio.get_event_loop().run_until_complete(asyncio.gather(asyncio.sleep(0.5)))
+ self.assertEqual(self.ret.returncode, 0)
+ self.assertEqual(self.ret.stdout, None)
+ self.assertEqual(self.ret.stderr, None)
+
+
def test_exception(self):
self.rule.execute_subprocess(self.set_ret, 'asdfasdf', capture_output=False)
diff --git a/tests/test_utils/test_timeframe.py b/tests/test_utils/test_timeframe.py
index 494ab486..e73f68ed 100644
--- a/tests/test_utils/test_timeframe.py
+++ b/tests/test_utils/test_timeframe.py
@@ -1,3 +1,5 @@
+# mypy: ignore-errors
+
import datetime
import unittest