diff --git a/.github/workflows/run-tox.yml b/.github/workflows/run-tox.yml index e13878f2..17106d8b 100644 --- a/.github/workflows/run-tox.yml +++ b/.github/workflows/run-tox.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: pre-commit: - name: + name: pre-commit runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67fcdb42..048861d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,13 +34,6 @@ repos: # - pep8-naming==0.13.3 - - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 - hooks: - - id: pyupgrade - args: ["--py38-plus"] - - - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..29f7cd3b --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,69 @@ + +line-length = 120 +indent-width = 4 + +target-version = "py38" +src = ["src", "test"] + +# https://docs.astral.sh/ruff/settings/#ignore-init-module-imports +ignore-init-module-imports = true + +extend-exclude = ["__init__.py"] + +select = [ + "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "I", # https://docs.astral.sh/ruff/rules/#isort-i + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + + "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async + "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + "FIX", # https://docs.astral.sh/ruff/rules/#flake8-fixme-fix + "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + "ISC", # https://docs.astral.sh/ruff/rules/#flake8-implicit-str-concat-isc + "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "SLOT", # https://docs.astral.sh/ruff/rules/#flake8-slots-slot + "T10", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 + "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "TD", # https://docs.astral.sh/ruff/rules/#flake8-todos-td + + "TRY", # https://docs.astral.sh/ruff/rules/#tryceratops-try + "FLY", # https://docs.astral.sh/ruff/rules/#flynt-fly + "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf + "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + + # "PL", # https://docs.astral.sh/ruff/rules/#pylint-pl + # "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb +] + +ignore = [ + "RET501", # https://docs.astral.sh/ruff/rules/unnecessary-return-none/#unnecessary-return-none-ret501 + "TRY400", # https://docs.astral.sh/ruff/rules/error-instead-of-exception/ + + "A003", # https://docs.astral.sh/ruff/rules/builtin-attribute-shadowing/ +] + + +[format] +# Use single quotes for non-triple-quoted strings. +quote-style = "single" + + + +[lint.flake8-builtins] +builtins-ignorelist = ["id", "input"] + + +[lint.per-file-ignores] +"docs/conf.py" = ["INP001", "A001"] +"setup.py" = ["PTH123"] + + +[lint.isort] +# https://docs.astral.sh/ruff/settings/#isort-lines-after-imports +lines-after-imports = 2 diff --git a/docs/conf.py b/docs/conf.py index a9847ee9..c456e0b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,15 +11,15 @@ import os import re import sys +from pathlib import Path import sphinx -from docutils.nodes import Text, Node +from docutils.nodes import Node, Text from sphinx.addnodes import desc_signature IS_RTD_BUILD = os.environ.get('READTHEDOCS', '-').lower() == 'true' IS_CI = os.environ.get('CI', '-') == 'true' - # https://www.sphinx-doc.org/en/master/extdev/logging.html sphinx_logger = sphinx.util.logging.getLogger('post') logger_lvl = logging.DEBUG if IS_RTD_BUILD or IS_CI else logging.INFO # set level to DEBUG for CI @@ -35,7 +35,11 @@ def log(msg: str): # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -sys.path.insert(0, os.path.join(os.path.abspath('..'), 'src')) +src_folder = Path(__file__).parent.with_name('src') +assert src_folder.is_dir() + +# required for autodoc +sys.path.insert(0, str(src_folder)) # -- Project information ----------------------------------------------------- @@ -49,6 +53,7 @@ def log(msg: str): release = 'beta' try: from HABApp import __version__ + version = __version__ print(f'Building docs for {version}') except Exception as e: @@ -229,7 +234,6 @@ def log(msg: str): (re.compile(r'py:class'), re.compile(r'(?:datetime|pendulum|aiohttp|pathlib)\..+')) ] - # -- Extension configuration ------------------------------------------------- exec_code_working_dir = '../src' exec_code_source_folders = ['../src', '../tests'] @@ -237,7 +241,6 @@ def log(msg: str): autodoc_member_order = 'bysource' autoclass_content = 'class' - # No config on member autodoc_pydantic_model_show_config_member = False autodoc_pydantic_model_show_config_summary = False @@ -255,9 +258,6 @@ def log(msg: str): autodoc_pydantic_field_list_validators = False autodoc_pydantic_field_swap_name_and_alias = True - - - # ---------------------------------------------------------------------------------------------------------------------- # Post processing of default value @@ -339,7 +339,6 @@ def setup(app): 'python': ('https://docs.python.org/3', None) } - # Don't show warnings for missing python references since these are created via intersphinx during the RTD build if not IS_RTD_BUILD: nitpick_ignore_regex.append( diff --git a/docs/interface_habapp.rst b/docs/interface_habapp.rst index ba308d96..27c00785 100644 --- a/docs/interface_habapp.rst +++ b/docs/interface_habapp.rst @@ -122,7 +122,7 @@ And since it is just like a normal item triggering on changes etc. is possible, my_agg.aggregation_period(2 * 3600) # Use max as an aggregation function - my_agg.aggregation_func = max + my_agg.aggregation_func(max) The value of ``my_agg`` in the example will now always be the maximum of ``MyInputItem`` in the last two hours. diff --git a/readme.md b/readme.md index 0fc6168a..0662d034 100644 --- a/readme.md +++ b/readme.md @@ -127,6 +127,11 @@ MyOpenhabRule() ``` # Changelog +#### 23.11.1 (2023-11-23) +- Fix for very small float values (#425) +- Fix for writing to persistence (#424) +- Updated dependencies + #### 23.09.2 (2023-09-24) - Made channel type on a ``Thing`` optional (#416) - Fixed an issue with mqtt publish and reconnect diff --git a/requirements.txt b/requirements.txt index 65b8fb17..9bcc4000 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,8 @@ # ----------------------------------------------------------------------------- # Packages for source formatting # ----------------------------------------------------------------------------- -pre-commit >= 3.3, < 3.4 +pre-commit >= 3.5, < 3.6 +ruff >= 0.1.6, < 0.2 # ----------------------------------------------------------------------------- # Packages for other developement tasks diff --git a/requirements_setup.txt b/requirements_setup.txt index cd8247b8..9251d495 100644 --- a/requirements_setup.txt +++ b/requirements_setup.txt @@ -1,21 +1,21 @@ -aiohttp == 3.8.5 -pydantic == 2.3.0 -msgspec == 0.18.2 +aiohttp == 3.9.0 +pydantic == 2.5.2 +msgspec == 0.18.4 pendulum == 2.1.2 bidict == 0.22.1 watchdog == 3.0.0 ujson == 5.8.0 -aiomqtt == 1.2.0 +aiomqtt == 1.2.1 immutables == 0.20 eascheduler == 0.1.11 -easyconfig == 0.3.0 -stack_data == 0.6.2 +easyconfig == 0.3.1 +stack_data == 0.6.3 colorama == 0.4.6 -voluptuous == 0.13.1 +voluptuous == 0.14.1 -typing-extensions == 4.7.1 +typing-extensions == 4.8.0 aiohttp-sse-client == 0.2.1 diff --git a/requirements_tests.txt b/requirements_tests.txt index 04e181d7..34746f45 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -6,6 +6,6 @@ # ----------------------------------------------------------------------------- # Packages to run source tests # ----------------------------------------------------------------------------- -packaging == 23.1 -pytest == 7.4.2 +packaging == 23.2 +pytest == 7.4.3 pytest-asyncio == 0.21.1 diff --git a/run/conf_testing/rules/openhab/test_items.py b/run/conf_testing/rules/openhab/test_items.py index 709a20b7..a152d811 100644 --- a/run/conf_testing/rules/openhab/test_items.py +++ b/run/conf_testing/rules/openhab/test_items.py @@ -6,7 +6,7 @@ from HABApp.core.events import ValueUpdateEventFilter from HABApp.core.types import HSB, RGB from HABApp.openhab.interface_async import async_get_items -from HABApp.openhab.items import GroupItem, StringItem, ColorItem +from HABApp.openhab.items import GroupItem, StringItem, ColorItem, NumberItem from HABAppTests import OpenhabTmpItem, TestBaseRule, ItemWaiter, EventWaiter @@ -21,6 +21,7 @@ def __init__(self): self.add_test('TestExisting', self.test_existing) self.add_test('TestColor', self.test_color) self.add_test('TestGroupFunction', self.test_group_func) + self.add_test('TestSmallValues', self.test_small_float_values) self.item_number = OpenhabTmpItem('Number') self.item_switch = OpenhabTmpItem('Switch') @@ -66,6 +67,18 @@ def test_api(self): self.openhab.get_item(self.item_group.name) asyncio.run_coroutine_threadsafe(async_get_items(), loop).result() + @OpenhabTmpItem.create('Number', arg_name='tmp_item') + def test_small_float_values(self, tmp_item: OpenhabTmpItem): + # https://github.com/spacemanspiff2007/HABApp/issues/425 + item = NumberItem.get_item(tmp_item.name) + assert item.value is None + + for i in range(3, 16, 3): + with ItemWaiter(item) as waiter: + value = 1 / 10 ** i + item.oh_post_update(value) + waiter.wait_for_state(value) + @OpenhabTmpItem.use('String', arg_name='oh_item') def test_tags(self, oh_item: OpenhabTmpItem): oh_item.create_item(tags=['tag1', 'tag2']) diff --git a/run/conf_testing/rules/openhab/test_persistence.py b/run/conf_testing/rules/openhab/test_persistence.py index be5827c6..983d204f 100644 --- a/run/conf_testing/rules/openhab/test_persistence.py +++ b/run/conf_testing/rules/openhab/test_persistence.py @@ -1,47 +1,66 @@ +from __future__ import annotations + from datetime import datetime, timedelta +from typing import Final, Any +from HABApp.openhab.definitions.helpers import OpenhabPersistenceData from HABApp.openhab.items import NumberItem from HABAppTests import TestBaseRule -class TestPersistence(TestBaseRule): - - def __init__(self): +class TestPersistenceBase(TestBaseRule): + def __init__(self, service_name: str, item_name: str): super().__init__() - self.item = 'RRD4J_Item' - self.add_test('RRD4J configured', self.test_configured) - self.add_test('RRD4J get', self.test_get) + self.config.skip_on_failure = True + self.item_name: Final = item_name + self.service_name: Final = service_name + + self.add_test(f'Persistence {service_name} available', self.test_service_available) def set_up(self): - i = NumberItem.get_item(self.item) - i.oh_post_update(i.value + 1 if i.value < 10 else 0) + i = NumberItem.get_item(self.item_name) + i.oh_post_update(int(i.value) + 1 if i.value < 10 else 0) - def test_configured(self): + def test_service_available(self): for cfg in self.oh.get_persistence_services(): - if cfg.id == 'rrd4j': + if cfg.id == self.service_name: break else: - raise ValueError('rrd4j not found!') + raise ValueError(f'Persistence service "{self.service_name}" not found!') + + def set_persistence_data(self, time: datetime, state: Any): + return self.openhab.set_persistence_data(self.item_name, self.service_name, time, state) + + def get_persistence_data(self, start_time: datetime | None, end_time: datetime | None) -> OpenhabPersistenceData: + return self.openhab.get_persistence_data(self.item_name, self.service_name, start_time, end_time) + + +class TestRRD4j(TestPersistenceBase): + + def __init__(self): + super().__init__('rrd4j', 'RRD4J_Item') + self.add_test('RRD4J get', self.test_get) def test_get(self): now = datetime.now() - d = self.openhab.get_persistence_data(self.item, 'rrd4j', now - timedelta(seconds=60), now) + d = self.get_persistence_data(now - timedelta(seconds=60), now) assert d.get_data() - # def test_set(self): - # now = datetime.now() - # d = self.openhab.get_persistence_data(self.item, 'mapdb', now - timedelta(seconds=5), now) - # was = d.get_data() - # - # assert list(was.values()) == [1] - # - # self.openhab.set_persistence_data(self.item, 'mapdb', now, 2) - # - # d = self.openhab.get_persistence_data(self.item, 'mapdb', now - timedelta(seconds=5), - # now + timedelta(seconds=5)) - # ist = d.get_data() - # assert list(ist.values()) == [2], ist - - -TestPersistence() + +TestRRD4j() + + +class TestMapDB(TestPersistenceBase): + + def __init__(self): + super().__init__('mapdb', 'RRD4J_Item') + self.add_test('MapDB get', self.test_get) + + def test_get(self): + now = datetime.now() + d = self.get_persistence_data(now - timedelta(seconds=60), now) + assert d.get_data() + + +TestMapDB() diff --git a/setup.py b/setup.py index 08cfd28e..3b5f6501 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def load_req() -> typing.List[str]: "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Topic :: Home Automation" ], diff --git a/src/HABApp/__check_dependency_packages__.py b/src/HABApp/__check_dependency_packages__.py index 083c4ed7..dad27f87 100644 --- a/src/HABApp/__check_dependency_packages__.py +++ b/src/HABApp/__check_dependency_packages__.py @@ -1,11 +1,12 @@ +from __future__ import annotations + import importlib import sys -from typing import List, Dict from HABApp.__debug_info__ import print_debug_info -def get_dependencies() -> List[str]: +def get_dependencies() -> list[str]: return [ 'aiohttp-sse-client', 'aiohttp', @@ -31,7 +32,7 @@ def get_dependencies() -> List[str]: def check_dependency_packages(): """Imports all dependencies and reports failures""" - missing: Dict[str, ModuleNotFoundError] = {} + missing: dict[str, ModuleNotFoundError] = {} # Package aliases (if the import name differs from the package name) alias = { @@ -43,7 +44,7 @@ def check_dependency_packages(): for name in get_dependencies(): try: importlib.import_module(alias.get(name, name)) - except ModuleNotFoundError as e: + except ModuleNotFoundError as e: # noqa: PERF203 missing[name] = e if not missing: diff --git a/src/HABApp/__cmd_args__.py b/src/HABApp/__cmd_args__.py index 7e668717..9a79db7a 100644 --- a/src/HABApp/__cmd_args__.py +++ b/src/HABApp/__cmd_args__.py @@ -23,7 +23,8 @@ def get_uptime() -> float: lib.GetTickCount64.restype = ctypes.c_uint64 return lib.GetTickCount64() / 1000 - raise NotImplementedError(f'Not supported on {sys.platform}') + msg = f'Not supported on {sys.platform}' + raise NotImplementedError(msg) def parse_args(passed_args=None) -> argparse.Namespace: @@ -79,10 +80,10 @@ def find_config_folder(arg_config_path: typing.Optional[Path]) -> Path: # Nothing is specified, we try to find the config automatically check_path = [] try: - working_dir = Path(os.getcwd()) - check_path.append( working_dir / 'HABApp') - check_path.append( working_dir.with_name('HABApp')) - check_path.append( working_dir.parent.with_name('HABApp')) + working_dir = Path.cwd() + check_path.append(working_dir / 'HABApp') + check_path.append(working_dir.with_name('HABApp')) + check_path.append(working_dir.parent.with_name('HABApp')) except ValueError: # the ValueError gets raised if the working_dir or its parent is empty (e.g. c:\ or /) pass diff --git a/src/HABApp/__debug_info__.py b/src/HABApp/__debug_info__.py index 29234062..e39d69f1 100644 --- a/src/HABApp/__debug_info__.py +++ b/src/HABApp/__debug_info__.py @@ -1,10 +1,10 @@ import platform import sys + from HABApp.__version__ import __version__ def get_debug_info() -> str: - info = { 'HABApp': __version__, 'Platform': platform.platform(), @@ -12,7 +12,7 @@ def get_debug_info() -> str: 'Python': sys.version, } - indent = max(map(lambda x: len(x), info)) + indent = max(len(x) for x in info) ret = '\n'.join('{:{indent}s}: {:s}'.format(k, str(v).replace('\n', ''), indent=indent) for k, v in info.items()) try: @@ -26,7 +26,7 @@ def get_debug_info() -> str: ret += f'\n\nInstalled Packages\n{"-" * 80}\n{table}' except Exception as e: - ret += f'\n\nCould not get installed Packages!\nError: {str(e)}' + ret += f'\n\nCould not get installed Packages!\nError: {e!s}' return ret diff --git a/src/HABApp/__version__.py b/src/HABApp/__version__.py index f87e0f46..3bd0717c 100644 --- a/src/HABApp/__version__.py +++ b/src/HABApp/__version__.py @@ -10,4 +10,4 @@ # Development versions contain the DEV-COUNTER postfix: # - 23.09.0.DEV-1 -__version__ = '23.09.2' +__version__ = '23.11.0' diff --git a/src/HABApp/core/asyncio.py b/src/HABApp/core/asyncio.py index a3ed5442..78ad13ee 100644 --- a/src/HABApp/core/asyncio.py +++ b/src/HABApp/core/asyncio.py @@ -1,15 +1,20 @@ +from __future__ import annotations + from asyncio import Future as _Future +from asyncio import Task as _Task from asyncio import run_coroutine_threadsafe as _run_coroutine_threadsafe -from contextvars import ContextVar as _ContextVar, Token -from typing import Any as _Any, Callable, Final, Optional +from contextvars import ContextVar as _ContextVar +from contextvars import Token as _Token +from typing import Any as _Any +from typing import Callable, Final from typing import Callable as _Callable from typing import Coroutine as _Coroutine -from typing import Optional as _Optional from typing import TypeVar as _TypeVar from HABApp.core.const import loop from HABApp.core.const.const import PYTHON_310 + if PYTHON_310: from typing import ParamSpec as _ParamSpec else: @@ -22,8 +27,8 @@ class AsyncContext: def __init__(self, value: str): self.value: Final = value - self.token: Optional[Token[str]] = None - self.parent: Optional[AsyncContext] = None + self.token: _Token[str] | None = None + self.parent: AsyncContext | None = None def __enter__(self): assert self.token is None, self @@ -52,22 +57,31 @@ def __str__(self): _tasks = set() -def create_task(coro: _Coroutine, name: _Optional[str] = None) -> _Future: +_T = _TypeVar('_T') + + +def create_task(coro: _Coroutine[_Any, _Any, _T], name: str | None = None) -> _Future[_T]: # https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task if async_context.get(None) is None: f = _run_coroutine_threadsafe(coro, loop) + _tasks.add(f) f.add_done_callback(_tasks.discard) return f - else: - t = loop.create_task(coro, name=name) - t.add_done_callback(_tasks.discard) - return t + + t = loop.create_task(coro, name=name) + _tasks.add(t) + t.add_done_callback(_tasks.discard) + return t -_CORO_RET = _TypeVar('_CORO_RET') +def create_task_from_async(coro: _Coroutine[_Any, _Any, _T], name: str | None = None) -> _Task[_T]: + t = loop.create_task(coro, name=name) + _tasks.add(t) + t.add_done_callback(_tasks.discard) + return t -def run_coro_from_thread(coro: _Coroutine[_Any, _Any, _CORO_RET], calling: _Callable) -> _CORO_RET: +def run_coro_from_thread(coro: _Coroutine[_Any, _Any, _T], calling: _Callable) -> _T: # This function call is blocking, so it can't be called in the async context if async_context.get(None) is not None: raise AsyncContextError(calling) @@ -76,11 +90,10 @@ def run_coro_from_thread(coro: _Coroutine[_Any, _Any, _CORO_RET], calling: _Call return fut.result() -P = _ParamSpec('P') -T = _TypeVar('T') +_P = _ParamSpec('_P') -def run_func_from_async(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: +def run_func_from_async(func: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: # we already have an async context if async_context.get(None) is not None: return func(*args, **kwargs) @@ -92,5 +105,5 @@ def run_func_from_async(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) # return future.result() -async def _run_func_from_async_helper(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: +async def _run_func_from_async_helper(func: Callable[_P, _T], *args: _P.args, **kwargs: _P.kwargs) -> _T: return func(*args, **kwargs) diff --git a/src/HABApp/core/const/const.py b/src/HABApp/core/const/const.py index 1d90db0d..be57b4e2 100644 --- a/src/HABApp/core/const/const.py +++ b/src/HABApp/core/const/const.py @@ -20,6 +20,7 @@ def __repr__(self): PYTHON_310: Final = sys.version_info >= (3, 10) PYTHON_311: Final = sys.version_info >= (3, 11) PYTHON_312: Final = sys.version_info >= (3, 12) +PYTHON_313: Final = sys.version_info >= (3, 13) # In python 3.11 there were changes to MyEnum(str, Enum), so we have to use the StrEnum diff --git a/src/HABApp/core/const/hints.py b/src/HABApp/core/const/hints.py index 4b6e9c0e..9fe059e1 100644 --- a/src/HABApp/core/const/hints.py +++ b/src/HABApp/core/const/hints.py @@ -5,12 +5,14 @@ from .const import PYTHON_310 as __IS_GE_PYTHON_310 + if __IS_GE_PYTHON_310: from typing import TypeAlias else: from typing import Final as TypeAlias -HINT_ANY_CLASS: TypeAlias = __Type[object] -HINT_FUNC_ASYNC: TypeAlias = __Callable[..., __Awaitable[__Any]] -HINT_EVENT_CALLBACK: TypeAlias = __Callable[[__Any], __Any] +TYPE_ANY_CLASS_TYPE: TypeAlias = __Type[object] +TYPE_FUNC_ASYNC: TypeAlias = __Callable[..., __Awaitable[__Any]] + +TYPE_EVENT_CALLBACK: TypeAlias = __Callable[[__Any], __Any] diff --git a/src/HABApp/core/const/loop.py b/src/HABApp/core/const/loop.py index 97eaaed2..08d660eb 100644 --- a/src/HABApp/core/const/loop.py +++ b/src/HABApp/core/const/loop.py @@ -2,7 +2,6 @@ import os import sys - # we can have subprocesses (https://docs.python.org/3/library/asyncio-platforms.html#subprocess-support-on-windows) # or mqtt support (https://github.com/sbtinstruments/aiomqtt#note-for-windows-users) # but not both. For testing, it makes sense to use mqtt support as a default diff --git a/src/HABApp/core/events/filter/event.py b/src/HABApp/core/events/filter/event.py index b013ff58..756d5b3e 100644 --- a/src/HABApp/core/events/filter/event.py +++ b/src/HABApp/core/events/filter/event.py @@ -1,16 +1,16 @@ -from typing import Optional, Final -from typing import get_type_hints as _get_type_hints from inspect import isclass +from typing import Final, Optional +from typing import get_type_hints as typing_get_type_hints from HABApp.core.const import MISSING -from HABApp.core.const.hints import HINT_ANY_CLASS +from HABApp.core.const.hints import TYPE_ANY_CLASS_TYPE from HABApp.core.internals import EventFilterBase class EventFilter(EventFilterBase): """Triggers on event types and optionally on their values, too""" - def __init__(self, event_class: HINT_ANY_CLASS, **kwargs): + def __init__(self, event_class: TYPE_ANY_CLASS_TYPE, **kwargs): assert len(kwargs) < 3, 'EventFilter only allows up to two args that will be used to filter' assert isclass(event_class), f'Class for event required! Passed {event_class} ({type(event_class)})' @@ -22,14 +22,15 @@ def __init__(self, event_class: HINT_ANY_CLASS, **kwargs): self.attr_name2: Optional[str] = None self.attr_value2 = None - type_hints = _get_type_hints(event_class) + type_hints = typing_get_type_hints(event_class) for arg, value in kwargs.items(): if value is MISSING: continue if arg not in type_hints: - raise AttributeError(f'Filter attribute "{arg}" does not exist for "{event_class.__name__}"') + msg = f'Filter attribute "{arg}" does not exist for "{event_class.__name__}"' + raise AttributeError(msg) if self.attr_name1 is None: self.attr_name1 = arg @@ -38,7 +39,8 @@ def __init__(self, event_class: HINT_ANY_CLASS, **kwargs): self.attr_name2 = arg self.attr_value2 = value else: - raise ValueError('Not implemented for more than 2 values!') + msg = 'Not implemented for more than 2 values!' + raise ValueError(msg) def trigger(self, event) -> bool: if not isinstance(event, self.event_class): diff --git a/src/HABApp/core/events/filter/groups.py b/src/HABApp/core/events/filter/groups.py index bc5f5b25..9e567489 100644 --- a/src/HABApp/core/events/filter/groups.py +++ b/src/HABApp/core/events/filter/groups.py @@ -18,10 +18,7 @@ class OrFilterGroup(EventFilterBaseGroup): """Only one child filter has to match""" def trigger(self, event: Any) -> bool: - for f in self.filters: - if f.trigger(event): - return True - return False + return any(f.trigger(event) for f in self.filters) def describe(self) -> str: objs = [f.describe() for f in self.filters] @@ -32,10 +29,7 @@ class AndFilterGroup(EventFilterBaseGroup): """All child filters have to match""" def trigger(self, event: Any) -> bool: - for f in self.filters: - if not f.trigger(event): - return False - return True + return all(f.trigger(event) for f in self.filters) def describe(self) -> str: objs = [f.describe() for f in self.filters] diff --git a/src/HABApp/core/internals/item_registry/item_registry.py b/src/HABApp/core/internals/item_registry/item_registry.py index 88082886..9fa72f54 100644 --- a/src/HABApp/core/internals/item_registry/item_registry.py +++ b/src/HABApp/core/internals/item_registry/item_registry.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import logging import threading -from typing import Dict -from typing import Tuple, Union, TypeVar +from typing import TypeVar -from HABApp.core.errors import ItemNotFoundException, ItemAlreadyExistsError +from HABApp.core.errors import ItemAlreadyExistsError, ItemNotFoundException from HABApp.core.internals.item_registry import ItemRegistryItem @@ -15,9 +16,9 @@ class ItemRegistry: def __init__(self): self._lock = threading.Lock() - self._items: Dict[str, ItemRegistryItem] = {} + self._items: dict[str, ItemRegistryItem] = {} - def item_exists(self, name: Union[str, ItemRegistryItem]) -> bool: + def item_exists(self, name: str | ItemRegistryItem) -> bool: if not isinstance(name, str): name = name.name return name in self._items @@ -28,10 +29,10 @@ def get_item(self, name: str) -> ItemRegistryItem: except KeyError: raise ItemNotFoundException(name) from None - def get_items(self) -> Tuple[ItemRegistryItem, ...]: + def get_items(self) -> tuple[ItemRegistryItem, ...]: return tuple(self._items.values()) - def get_item_names(self) -> Tuple[str, ...]: + def get_item_names(self) -> tuple[str, ...]: return tuple(self._items.keys()) def add_item(self, item: _HINT_ITEM_OBJ) -> _HINT_ITEM_OBJ: @@ -54,7 +55,7 @@ def add_item(self, item: _HINT_ITEM_OBJ) -> _HINT_ITEM_OBJ: item._on_item_added() return item - def pop_item(self, name: Union[str, _HINT_ITEM_OBJ]) -> _HINT_ITEM_OBJ: + def pop_item(self, name: str | _HINT_ITEM_OBJ) -> _HINT_ITEM_OBJ: if not isinstance(name, str): name = name.name diff --git a/src/HABApp/core/internals/wrapped_function/wrapped_async.py b/src/HABApp/core/internals/wrapped_function/wrapped_async.py index 40438d08..516ee393 100644 --- a/src/HABApp/core/internals/wrapped_function/wrapped_async.py +++ b/src/HABApp/core/internals/wrapped_function/wrapped_async.py @@ -2,14 +2,15 @@ from typing import Optional from HABApp.core.asyncio import async_context, create_task -from HABApp.core.const.hints import HINT_FUNC_ASYNC +from HABApp.core.const.hints import TYPE_FUNC_ASYNC from HABApp.core.internals import Context + from .base import WrappedFunctionBase class WrappedAsyncFunction(WrappedFunctionBase): - def __init__(self, func: HINT_FUNC_ASYNC, + def __init__(self, func: TYPE_FUNC_ASYNC, name: Optional[str] = None, logger: Optional[logging.Logger] = None, context: Optional[Context] = None): diff --git a/src/HABApp/core/internals/wrapped_function/wrapper.py b/src/HABApp/core/internals/wrapped_function/wrapper.py index 295445d0..60edb762 100644 --- a/src/HABApp/core/internals/wrapped_function/wrapper.py +++ b/src/HABApp/core/internals/wrapped_function/wrapper.py @@ -5,13 +5,13 @@ from HABApp.config import CONFIG from HABApp.core.internals import Context from HABApp.core.internals.wrapped_function.base import TYPE_WRAPPED_FUNC_OBJ -from HABApp.core.internals.wrapped_function.wrapped_async import WrappedAsyncFunction, HINT_FUNC_ASYNC +from HABApp.core.internals.wrapped_function.wrapped_async import WrappedAsyncFunction, TYPE_FUNC_ASYNC from HABApp.core.internals.wrapped_function.wrapped_sync import WrappedSyncFunction from HABApp.core.internals.wrapped_function.wrapped_thread import HINT_FUNC_SYNC, WrappedThreadFunction, \ create_thread_pool, stop_thread_pool, run_in_thread_pool -def wrap_func(func: Union[HINT_FUNC_SYNC, HINT_FUNC_ASYNC], +def wrap_func(func: Union[HINT_FUNC_SYNC, TYPE_FUNC_ASYNC], warn_too_long=True, name: Optional[str] = None, logger: Optional[logging.Logger] = None, diff --git a/src/HABApp/core/items/base_item.py b/src/HABApp/core/items/base_item.py index d1f38c10..7a0f4190 100644 --- a/src/HABApp/core/items/base_item.py +++ b/src/HABApp/core/items/base_item.py @@ -1,17 +1,24 @@ -from typing import Type, TypeVar, Optional +from typing import Optional, Type, TypeVar +from eascheduler.const import local_tz from pendulum import UTC, DateTime from pendulum import now as pd_now -from HABApp.core.internals import HINT_EVENT_FILTER_OBJ, HINT_EVENT_BUS_LISTENER -from HABApp.core.internals import uses_get_item, uses_item_registry, get_current_context +from HABApp.core.internals import ( + HINT_EVENT_BUS_LISTENER, + HINT_EVENT_FILTER_OBJ, + get_current_context, + uses_get_item, + uses_item_registry, +) from HABApp.core.internals.item_registry import ItemRegistryItem from HABApp.core.lib.parameters import TH_POSITIVE_TIME_DIFF, get_positive_time_diff -from eascheduler.const import local_tz + +from ..const.hints import TYPE_EVENT_CALLBACK from .base_item_times import ChangedTime, ItemNoChangeWatch, ItemNoUpdateWatch, UpdatedTime from .tmp_data import add_tmp_data as _add_tmp_data from .tmp_data import restore_tmp_data as _restore_tmp_data -from ..const.hints import HINT_EVENT_CALLBACK + get_item = uses_get_item() item_registry = uses_item_registry() @@ -79,7 +86,7 @@ def watch_update(self, secs: TH_POSITIVE_TIME_DIFF) -> ItemNoUpdateWatch: secs = get_positive_time_diff(secs, round_digits=1) return self._last_update.add_watch(secs) - def listen_event(self, callback: HINT_EVENT_CALLBACK, + def listen_event(self, callback: TYPE_EVENT_CALLBACK, event_filter: Optional[HINT_EVENT_FILTER_OBJ] = None) -> HINT_EVENT_BUS_LISTENER: """ Register an event listener which listens to all event that the item receives diff --git a/src/HABApp/core/items/base_item_watch.py b/src/HABApp/core/items/base_item_watch.py index 5503c9e2..ad86e79b 100644 --- a/src/HABApp/core/items/base_item_watch.py +++ b/src/HABApp/core/items/base_item_watch.py @@ -3,11 +3,17 @@ import HABApp from HABApp.core.asyncio import run_func_from_async -from HABApp.core.events import ItemNoChangeEvent, ItemNoUpdateEvent, EventFilter +from HABApp.core.const.hints import TYPE_EVENT_CALLBACK +from HABApp.core.events import EventFilter, ItemNoChangeEvent, ItemNoUpdateEvent +from HABApp.core.internals import ( + AutoContextBoundObj, + ContextBoundEventBusListener, + get_current_context, + uses_post_event, + wrap_func, +) from HABApp.core.lib import PendingFuture -from HABApp.core.const.hints import HINT_EVENT_CALLBACK -from HABApp.core.internals import uses_post_event, get_current_context, AutoContextBoundObj, wrap_func -from HABApp.core.internals import ContextBoundEventBusListener + log = logging.getLogger('HABApp') @@ -34,7 +40,7 @@ def cancel(self): self._ctx_unlink() run_func_from_async(self.__cancel_watch) - def listen_event(self, callback: HINT_EVENT_CALLBACK) -> 'HABApp.core.base.HINT_EVENT_BUS_LISTENER': + def listen_event(self, callback: TYPE_EVENT_CALLBACK) -> 'HABApp.core.base.HINT_EVENT_BUS_LISTENER': """Listen to (only) the event that is emitted by this watcher""" context = get_current_context() return context.add_event_listener( diff --git a/src/HABApp/core/items/item.py b/src/HABApp/core/items/item.py index 6e9da4a2..12045b08 100644 --- a/src/HABApp/core/items/item.py +++ b/src/HABApp/core/items/item.py @@ -1,5 +1,5 @@ from HABApp.core.errors import ItemNotFoundException -from HABApp.core.internals import uses_item_registry, uses_get_item +from HABApp.core.internals import uses_get_item, uses_item_registry from HABApp.core.items import BaseValueItem get_item = uses_get_item() diff --git a/src/HABApp/mqtt/connection/handler.py b/src/HABApp/mqtt/connection/handler.py index 4259ac3a..ebb63b94 100644 --- a/src/HABApp/mqtt/connection/handler.py +++ b/src/HABApp/mqtt/connection/handler.py @@ -61,7 +61,6 @@ async def on_setup(self, connection: MqttConnection): # clean_session=False ) - # noinspection PyProtectedMember async def on_connecting(self, connection: MqttConnection, context: CONTEXT_TYPE): assert context is not None @@ -73,9 +72,6 @@ async def on_disconnected(self, connection: MqttConnection, context: CONTEXT_TYP assert context is not None connection.log.info('Disconnected') - # remove this check when https://github.com/sbtinstruments/aiomqtt/pull/249 gets merged - if not context._lock.locked(): - await context._lock.acquire() await context.__aexit__(None, None, None) diff --git a/src/HABApp/mqtt/connection/subscribe.py b/src/HABApp/mqtt/connection/subscribe.py index 338be40a..9b3c5e78 100644 --- a/src/HABApp/mqtt/connection/subscribe.py +++ b/src/HABApp/mqtt/connection/subscribe.py @@ -1,19 +1,21 @@ from __future__ import annotations -from typing import Any, Iterable +from typing import TYPE_CHECKING, Any, Iterable import HABApp from HABApp.config import CONFIG -from HABApp.config.models.mqtt import QOS from HABApp.core.asyncio import run_func_from_async from HABApp.core.errors import ItemNotFoundException -from HABApp.core.internals import uses_post_event, uses_get_item, uses_item_registry +from HABApp.core.internals import uses_get_item, uses_item_registry, uses_post_event from HABApp.core.lib import SingleTask from HABApp.core.wrapper import process_exception from HABApp.mqtt.connection.connection import MqttPlugin from HABApp.mqtt.events import MqttValueChangeEvent, MqttValueUpdateEvent from HABApp.mqtt.mqtt_payload import get_msg_payload +if TYPE_CHECKING: + from HABApp.config.models.mqtt import QOS + SUBSCRIBE_CFG = CONFIG.mqtt.subscribe diff --git a/src/HABApp/mqtt/items/mqtt_item.py b/src/HABApp/mqtt/items/mqtt_item.py index a38c7cd1..64ba6b4b 100644 --- a/src/HABApp/mqtt/items/mqtt_item.py +++ b/src/HABApp/mqtt/items/mqtt_item.py @@ -1,3 +1,5 @@ +from typing import Optional + from HABApp.core.errors import ItemNotFoundException from HABApp.core.internals import uses_get_item, uses_item_registry from HABApp.core.items import BaseValueItem @@ -33,7 +35,7 @@ def get_create_item(cls, name: str, initial_value=None) -> 'MqttItem': assert isinstance(item, cls), f'{cls} != {type(item)}' return item - def publish(self, payload, qos: int = None, retain: bool = None): + def publish(self, payload, qos: Optional[int] = None, retain: Optional[bool] = None): """ Publish the payload under the topic from the item. diff --git a/src/HABApp/mqtt/items/mqtt_pair_item.py b/src/HABApp/mqtt/items/mqtt_pair_item.py index b7de2760..9880d3fb 100644 --- a/src/HABApp/mqtt/items/mqtt_pair_item.py +++ b/src/HABApp/mqtt/items/mqtt_pair_item.py @@ -3,6 +3,7 @@ from HABApp.core.errors import ItemNotFoundException from HABApp.core.internals import uses_item_registry from HABApp.mqtt.interface_sync import publish + from . import MqttBaseItem Items = uses_item_registry() @@ -14,7 +15,8 @@ def build_write_topic(read_topic: str) -> Optional[str]: parts.insert(-1, 'set') return '/'.join(parts) - raise ValueError(f'Can not build write topic for "{read_topic}"') + msg = f'Can not build write topic for "{read_topic}"' + raise ValueError(msg) class MqttPairItem(MqttBaseItem): @@ -46,11 +48,11 @@ def get_create_item(cls, name: str, write_topic: Optional[str] = None, initial_v assert isinstance(item, cls), f'{cls} != {type(item)}' return item - def __init__(self, name: str, initial_value=None, write_topic: str = None): + def __init__(self, name: str, initial_value=None, write_topic: Optional[str] = None): super().__init__(name, initial_value) self.write_topic: str = write_topic - def publish(self, payload, qos: int = None, retain: bool = None): + def publish(self, payload, qos: Optional[int] = None, retain: Optional[bool] = None): """ Publish the payload under the write topic from the item. diff --git a/src/HABApp/mqtt/mqtt_interface.py b/src/HABApp/mqtt/mqtt_interface.py index c7a40100..9b287cf8 100644 --- a/src/HABApp/mqtt/mqtt_interface.py +++ b/src/HABApp/mqtt/mqtt_interface.py @@ -3,17 +3,20 @@ import paho.mqtt.client as mqtt import HABApp +from HABApp.core.const.json import dump_json from HABApp.mqtt.connection.mqtt_connection import STATUS, log -from ..core.const.json import dump_json def __is_connected() -> bool: if STATUS.connected: return True - raise ConnectionError('Mqtt client not connected') + msg = 'Mqtt client not connected' + raise ConnectionError(msg) -def publish(topic: str, payload: typing.Any, qos: int = None, retain: bool = None) -> int: + +def publish(topic: str, payload: typing.Any, + qos: typing.Optional[int] = None, retain: typing.Optional[bool] = None) -> int: """ Publish a value under a certain topic. If qos and/or retain is not set the value from the configuration file will be used. @@ -51,7 +54,7 @@ def publish(topic: str, payload: typing.Any, qos: int = None, retain: bool = Non return info -def subscribe(topic: str, qos: int = None) -> int: +def subscribe(topic: str, qos: typing.Optional[int] = None) -> int: """ Subscribe to a MQTT topic. Note that subscriptions made this way are volatile and will only remain until the next disconnect. diff --git a/src/HABApp/openhab/connection/connection.py b/src/HABApp/openhab/connection/connection.py index 1067fee5..00a3d825 100644 --- a/src/HABApp/openhab/connection/connection.py +++ b/src/HABApp/openhab/connection/connection.py @@ -1,12 +1,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Optional import aiohttp import HABApp -from HABApp.core.connections import BaseConnection, Connections, ConnectionStateToEventBusPlugin, AutoReconnectPlugin +from HABApp.core.connections import AutoReconnectPlugin, BaseConnection, Connections, ConnectionStateToEventBusPlugin from HABApp.core.const.const import PYTHON_310 from HABApp.core.items.base_valueitem import datetime diff --git a/src/HABApp/openhab/connection/handler/func_async.py b/src/HABApp/openhab/connection/handler/func_async.py index b178383c..6f0dbbc9 100644 --- a/src/HABApp/openhab/connection/handler/func_async.py +++ b/src/HABApp/openhab/connection/handler/func_async.py @@ -142,7 +142,7 @@ async def async_create_item(item_type: str, name: str, if ret.status == 404: raise ItemNotFoundError.from_name(name) - elif ret.status == 405: + if ret.status == 405: raise ItemNotEditableError.from_name(name) return ret.status < 300 @@ -228,10 +228,11 @@ async def async_set_thing_enabled(thing: str | ItemRegistryItem, enabled: bool): if ret.status == 404: raise ThingNotFoundError.from_uid(thing) - elif ret.status == 409: + if ret.status == 409: raise ThingNotEditableError.from_uid(thing) - elif ret.status >= 300: - raise ValueError('Something went wrong') + if ret.status >= 300: + msg = 'Something went wrong' + raise ValueError(msg) return ret.status @@ -289,7 +290,9 @@ async def async_get_link(item: str | ItemRegistryItem, channel: str) -> ItemChan if resp.status == 404: raise LinkNotFoundError.from_names(item, channel) - raise LinkRequestError('Unexpected error') + + msg = 'Unexpected error' + raise LinkRequestError(msg) async def async_create_link( @@ -314,7 +317,9 @@ async def async_create_link( if resp.status == 405: LinkNotEditableError.from_names(item, channel) - raise LinkRequestError('Unexpected error') + + msg = 'Unexpected error' + raise LinkRequestError(msg) async def async_remove_link(item: str | ItemRegistryItem, channel: str): @@ -328,7 +333,9 @@ async def async_remove_link(item: str | ItemRegistryItem, channel: str): raise LinkNotFoundError.from_names(item, channel) if resp.status == 405: LinkNotEditableError.from_names(item, channel) - raise LinkRequestError('Unexpected error') + + msg = 'Unexpected error' + raise LinkRequestError(msg) # ---------------------------------------------------------------------------------------------------------------------- diff --git a/src/HABApp/openhab/connection/handler/handler.py b/src/HABApp/openhab/connection/handler/handler.py index 13d3b2bd..2bba83e7 100644 --- a/src/HABApp/openhab/connection/handler/handler.py +++ b/src/HABApp/openhab/connection/handler/handler.py @@ -94,7 +94,7 @@ async def post(self, url: str, log_404=True, json=None, data=None, **kwargs: Any return None mgr = _RequestContextManager( - self.request(METH_POST, url, data=data, json=json, **kwargs, **self.options, **kwargs) + self.request(METH_POST, url, data=data, json=json, **self.options, **kwargs) ) if data is None: data = json @@ -105,7 +105,7 @@ async def put(self, url: str, log_404=True, json=None, data=None, **kwargs: Any) return None mgr = _RequestContextManager( - self.request(METH_PUT, url, data=data, json=json, **kwargs, **self.options, **kwargs) + self.request(METH_PUT, url, data=data, json=json, **self.options, **kwargs) ) if data is None: data = json @@ -116,7 +116,7 @@ async def delete(self, url: str, log_404=True, json=None, data=None, **kwargs: A return None mgr = _RequestContextManager( - self.request(METH_DELETE, url, data=data, json=json, **kwargs, **self.options, **kwargs) + self.request(METH_DELETE, url, data=data, json=json, **self.options, **kwargs) ) if data is None: data = json @@ -178,7 +178,7 @@ async def on_connecting(self, connection: OpenhabConnection): # during startup we get OpenhabCredentialsInvalidError even though credentials are correct except (OpenhabDisconnectedError, OpenhabCredentialsInvalidError): connection.set_error() - raise AlreadyHandledException() + raise AlreadyHandledException() from None HANDLER = ConnectionHandler() diff --git a/src/HABApp/openhab/connection/handler/helper.py b/src/HABApp/openhab/connection/handler/helper.py index 8f20e472..eccf3fc7 100644 --- a/src/HABApp/openhab/connection/handler/helper.py +++ b/src/HABApp/openhab/connection/handler/helper.py @@ -8,9 +8,17 @@ def convert_to_oh_type(obj: Any) -> str: - if isinstance(obj, (str, int, float, bool)): + if isinstance(obj, (str, int, bool)): return str(obj) + if isinstance(obj, float): + v = str(obj) + if 'e-' not in v: + return v + + v = f'{obj:.{int(v.split("e-", maxsplit=1)[1]) + 6}f}' + return v.rstrip('0') + if isinstance(obj, datetime): # Add timezone (if not yet defined) to string, then remote anything below ms. # 2018-11-19T09:47:38.284000+0100 -> 2018-11-19T09:47:38.284+0100 diff --git a/src/HABApp/openhab/connection/plugins/load_transformations.py b/src/HABApp/openhab/connection/plugins/load_transformations.py index 0b55eeb3..f1a69b28 100644 --- a/src/HABApp/openhab/connection/plugins/load_transformations.py +++ b/src/HABApp/openhab/connection/plugins/load_transformations.py @@ -6,7 +6,7 @@ from HABApp.openhab.connection.connection import OpenhabConnection, OpenhabContext from HABApp.openhab.connection.handler.func_async import async_get_transformations from HABApp.openhab.transformations._map import MAP_REGISTRY -from HABApp.openhab.transformations.base import log, TransformationRegistryBase +from HABApp.openhab.transformations.base import TransformationRegistryBase, log Items = uses_item_registry() diff --git a/src/HABApp/openhab/connection/plugins/overview_broken_links.py b/src/HABApp/openhab/connection/plugins/overview_broken_links.py index e2f68a3f..e2e00e97 100644 --- a/src/HABApp/openhab/connection/plugins/overview_broken_links.py +++ b/src/HABApp/openhab/connection/plugins/overview_broken_links.py @@ -8,7 +8,7 @@ from HABApp.core.internals import uses_item_registry from HABApp.core.logger import log_warning from HABApp.openhab.connection.connection import OpenhabConnection -from HABApp.openhab.connection.handler.func_async import async_get_things, async_get_links +from HABApp.openhab.connection.handler.func_async import async_get_links, async_get_things PING_CONFIG: Final = CONFIG.openhab.ping diff --git a/src/HABApp/openhab/connection/plugins/plugin_things/cfg_validator.py b/src/HABApp/openhab/connection/plugins/plugin_things/cfg_validator.py index 66defc10..51a2a6df 100644 --- a/src/HABApp/openhab/connection/plugins/plugin_things/cfg_validator.py +++ b/src/HABApp/openhab/connection/plugins/plugin_things/cfg_validator.py @@ -82,7 +82,8 @@ def validate_item_type(cls, v): try: return {k.lower(): k for k in ITEM_TYPES}[v.lower()] except KeyError: - raise ValueError(f'Must be one of {", ".join(ITEM_TYPES)}') + msg = f'Must be one of {", ".join(ITEM_TYPES)}' + raise ValueError(msg) from None @field_validator('metadata', mode='before') def make_meta_cfg(cls, v): diff --git a/src/HABApp/openhab/definitions/rest/items.py b/src/HABApp/openhab/definitions/rest/items.py index b9218d58..8a7f3896 100644 --- a/src/HABApp/openhab/definitions/rest/items.py +++ b/src/HABApp/openhab/definitions/rest/items.py @@ -1,4 +1,4 @@ -from typing import Optional, Union, List, Any, Dict +from typing import Any, Dict, List, Optional, Union from msgspec import Struct, field diff --git a/src/HABApp/openhab/definitions/rest/root.py b/src/HABApp/openhab/definitions/rest/root.py index 72e8496b..f235f8dd 100644 --- a/src/HABApp/openhab/definitions/rest/root.py +++ b/src/HABApp/openhab/definitions/rest/root.py @@ -2,7 +2,6 @@ from msgspec import Struct - # https://github.com/openhab/openhab-core/blob/main/bundles/org.openhab.core.io.rest/src/main/java/org/openhab/core/io/rest/internal/resources/beans/RootBean.java diff --git a/src/HABApp/openhab/item_to_reg.py b/src/HABApp/openhab/item_to_reg.py index 58c4ac7b..d1050604 100644 --- a/src/HABApp/openhab/item_to_reg.py +++ b/src/HABApp/openhab/item_to_reg.py @@ -48,6 +48,7 @@ def add_to_registry(item: 'HABApp.openhab.items.OpenhabItem', set_value=False): # Replace existing item with the updated definition Items.pop_item(name) Items.add_item(item) + return None def remove_from_registry(name: str): diff --git a/src/HABApp/openhab/items/contact_item.py b/src/HABApp/openhab/items/contact_item.py index 29fd0dca..be01e1d5 100644 --- a/src/HABApp/openhab/items/contact_item.py +++ b/src/HABApp/openhab/items/contact_item.py @@ -53,8 +53,8 @@ def is_closed(self) -> bool: return self.value == CLOSED def oh_send_command(self, value: Any = MISSING): - raise SendCommandNotSupported(f'{self.__class__.__name__} does not support send command! ' - 'See openHAB documentation for details.') + msg = f'{self.__class__.__name__} does not support send command! See openHAB documentation for details.' + raise SendCommandNotSupported(msg) def open(self): """Post an update to the item with the open value""" @@ -70,9 +70,11 @@ def __str__(self): def __eq__(self, other): if isinstance(other, ContactItem): return self.value == other.value - elif isinstance(other, str): + + if isinstance(other, str): return self.value == other - elif isinstance(other, int): + + if isinstance(other, int): if other and self.is_open(): return True if not other and self.is_closed(): diff --git a/src/HABApp/openhab/map_events.py b/src/HABApp/openhab/map_events.py index c0dd6ec5..e685318b 100644 --- a/src/HABApp/openhab/map_events.py +++ b/src/HABApp/openhab/map_events.py @@ -43,4 +43,5 @@ def get_event(_in_dict: dict) -> OpenhabEvent: try: return _events[event_type].from_dict(topic, payload) except KeyError: - raise ValueError(f'Unknown Event: {event_type:s} for {_in_dict}') + msg = f'Unknown Event: {event_type:s} for {_in_dict}' + raise ValueError(msg) from None diff --git a/src/HABApp/openhab/map_items.py b/src/HABApp/openhab/map_items.py index 9a505811..d08ed681 100644 --- a/src/HABApp/openhab/map_items.py +++ b/src/HABApp/openhab/map_items.py @@ -62,7 +62,8 @@ def map_item(name: str, type: str, value: Optional[str], if cls is not None: return cls.from_oh(name, value, label=label, tags=tags, groups=groups, metadata=meta) - raise ValueError(f'Unknown openHAB type: {type} for {name}') + msg = f'Unknown openHAB type: {type} for {name}' + raise ValueError(msg) except Exception as e: process_exception('map_items', e, logger=log) diff --git a/src/HABApp/openhab/process_events.py b/src/HABApp/openhab/process_events.py index 56ff51de..dbc4532c 100644 --- a/src/HABApp/openhab/process_events.py +++ b/src/HABApp/openhab/process_events.py @@ -1,23 +1,37 @@ import logging -from asyncio import create_task from typing import Union import HABApp import HABApp.core import HABApp.openhab.events +from HABApp.core.asyncio import create_task_from_async from HABApp.core.errors import ItemNotFoundException -from HABApp.core.events import ValueUpdateEvent, ValueChangeEvent -from HABApp.core.internals import uses_post_event, uses_get_item +from HABApp.core.events import ValueChangeEvent, ValueUpdateEvent +from HABApp.core.internals import uses_get_item, uses_post_event from HABApp.core.logger import log_warning from HABApp.core.wrapper import process_exception -from HABApp.openhab.definitions.topics import TOPIC_THINGS, TOPIC_ITEMS -from HABApp.openhab.events import GroupStateChangedEvent, GroupStateUpdatedEvent, \ - ItemAddedEvent, ItemRemovedEvent, ItemUpdatedEvent, \ - ThingStatusInfoEvent, ThingAddedEvent, ThingRemovedEvent, ThingUpdatedEvent, ThingConfigStatusInfoEvent -from HABApp.openhab.item_to_reg import add_to_registry, remove_from_registry, remove_thing_from_registry, \ - add_thing_to_registry +from HABApp.openhab.definitions.topics import TOPIC_ITEMS, TOPIC_THINGS +from HABApp.openhab.events import ( + GroupStateChangedEvent, + GroupStateUpdatedEvent, + ItemAddedEvent, + ItemRemovedEvent, + ItemUpdatedEvent, + ThingAddedEvent, + ThingConfigStatusInfoEvent, + ThingRemovedEvent, + ThingStatusInfoEvent, + ThingUpdatedEvent, +) +from HABApp.openhab.item_to_reg import ( + add_thing_to_registry, + add_to_registry, + remove_from_registry, + remove_thing_from_registry, +) from HABApp.openhab.map_events import get_event + log = logging.getLogger('HABApp.openhab.items') post_event = uses_post_event() @@ -64,7 +78,7 @@ def on_sse_event(event_dict: dict, oh_3: bool): # Events that add items to the item registry # These events require that we query openHAB because of the metadata, so we have to do it in a task if isinstance(event, (ItemAddedEvent, ItemUpdatedEvent)): - create_task(item_event(event)) + create_task_from_async(item_event(event)) return None # Events that remove items from the item registry diff --git a/src/HABApp/parameters/parameter.py b/src/HABApp/parameters/parameter.py index 91c762a5..5879b1a0 100644 --- a/src/HABApp/parameters/parameter.py +++ b/src/HABApp/parameters/parameter.py @@ -1,3 +1,7 @@ +# ruff: noqa: TRY003, EM101 +# EM101 Exception must not use a string literal, assign to variable first +# TRY003 Avoid specifying long messages outside the exception class + import typing from math import ceil, floor diff --git a/src/HABApp/parameters/parameter_files.py b/src/HABApp/parameters/parameter_files.py index e4f38dba..aad6688e 100644 --- a/src/HABApp/parameters/parameter_files.py +++ b/src/HABApp/parameters/parameter_files.py @@ -35,7 +35,8 @@ def save_file(file: str): assert isinstance(file, str), type(file) path = HABApp.CONFIG.directories.param if path is None: - raise ValueError('Parameter files are disabled! Configure a folder to use them!') + msg = 'Parameter files are disabled! Configure a folder to use them!' + raise ValueError(msg) filename = path / (file + '.yml') @@ -60,6 +61,7 @@ async def setup_param_files() -> bool: folder.add_file_type(HABAppParameterFile) watcher = folder.add_watch('.yml') await watcher.trigger_all() + return True def reload_param_file(name: str): diff --git a/src/HABApp/rule/interfaces/rule_subprocess.py b/src/HABApp/rule/interfaces/rule_subprocess.py index 863bd0fc..4d04f5a0 100644 --- a/src/HABApp/rule/interfaces/rule_subprocess.py +++ b/src/HABApp/rule/interfaces/rule_subprocess.py @@ -2,7 +2,7 @@ import logging import os from pathlib import Path -from typing import Optional, Union, Iterable, Any, Tuple, Dict, List, Callable +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union from typing_extensions import TypeAlias @@ -10,6 +10,7 @@ from HABApp.core.logger import HABAppError, HABAppWarning from HABApp.core.wrapper import process_exception + log = logging.getLogger('HABApp.execute') HINT_EXEC_ARGS: TypeAlias = Union[str, Path] @@ -28,10 +29,12 @@ def _ensure_str_objs(objs: Iterable[HINT_EXEC_ARGS], key: str, enforce_abs=False path_val = Path(val) str_val = val else: - raise ValueError(f'{key:s}[{i:d}] is not of type str! "{val}" ({type(val).__name__:s})') + msg = f'{key:s}[{i:d}] is not of type str! "{val}" ({type(val).__name__:s})' + raise TypeError(msg) if enforce_abs and not path_val.is_absolute(): - raise ValueError(f'{key:s}[{i:d}] is not an absolute path: "{val}"') + msg = f'{key:s}[{i:d}] is not an absolute path: "{val}"' + raise ValueError(msg) new_args.append(str_val) @@ -42,20 +45,22 @@ def build_exec_params(*args: HINT_EXEC_ARGS, _capture_output=True, _additional_python_path: HINT_PYTHON_PATH = None, **kwargs: Any) -> Tuple[Iterable[str], Dict[str, Any]]: - # convenience for easy capturing if _capture_output: if 'stdout' in kwargs: - raise ValueError('Parameter "capture_output" can not be used with "stdout" in kwargs!') + msg = 'Parameter "capture_output" can not be used with "stdout" in kwargs!' + raise ValueError(msg) kwargs['stdout'] = asyncio.subprocess.PIPE if 'stderr' in kwargs: - raise ValueError('Parameter "capture_output" can not be used with "stderr" in kwargs!') + msg = 'Parameter "capture_output" can not be used with "stderr" in kwargs!' + raise ValueError(msg) kwargs['stderr'] = asyncio.subprocess.PIPE # convenience for additional libraries if _additional_python_path is not None: if 'env' in kwargs: - raise ValueError('Parameter "additional_python_path" can not be used with "env" in kwargs!') + msg = 'Parameter "additional_python_path" can not be used with "env" in kwargs!' + raise ValueError(msg) ppath = _ensure_str_objs(_additional_python_path, 'additional_python_path', enforce_abs=True) @@ -95,8 +100,8 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, FinishedProcessInfo): return self.returncode == other.returncode and self.stdout == other.stdout and self.stderr == other.stderr - else: - return NotImplementedError + + return NotImplementedError HINT_PROCESS_CB_FULL: TypeAlias = Callable[[FinishedProcessInfo], Any] @@ -104,7 +109,6 @@ def __eq__(self, other): async def async_subprocess_exec(callback, *args, calling_func, raw_info: bool, **kwargs): - call_str = '' try: @@ -113,7 +117,7 @@ async def async_subprocess_exec(callback, *args, calling_func, raw_info: bool, * stdout = None stderr = None - call_str = ' '.join(map(lambda x: f'"{x}"', args)) + call_str = ' '.join(f'"{x}"' for x in args) try: proc = await asyncio.create_subprocess_exec(*args, **kwargs) diff --git a/src/HABApp/rule/rule.py b/src/HABApp/rule/rule.py index 9e9a39c5..6079a593 100644 --- a/src/HABApp/rule/rule.py +++ b/src/HABApp/rule/rule.py @@ -3,7 +3,7 @@ import sys import warnings from pathlib import Path -from typing import Iterable, Union, Any, Optional, Tuple, Pattern, List, overload, Literal, TypeVar, Callable, Final +from typing import Any, Callable, Final, Iterable, List, Literal, Optional, Pattern, Tuple, TypeVar, Union, overload import HABApp import HABApp.core @@ -12,16 +12,29 @@ import HABApp.util from HABApp.core.asyncio import create_task from HABApp.core.const.const import PYTHON_310 -from HABApp.core.const.hints import HINT_EVENT_CALLBACK -from HABApp.core.internals import HINT_EVENT_FILTER_OBJ, HINT_EVENT_BUS_LISTENER, ContextProvidingObj, \ - uses_post_event, EventFilterBase, uses_item_registry, ContextBoundEventBusListener -from HABApp.core.internals import wrap_func -from HABApp.core.items import BaseItem, HINT_ITEM_OBJ, HINT_TYPE_ITEM_OBJ, BaseValueItem +from HABApp.core.const.hints import TYPE_EVENT_CALLBACK +from HABApp.core.internals import ( + HINT_EVENT_BUS_LISTENER, + HINT_EVENT_FILTER_OBJ, + ContextBoundEventBusListener, + ContextProvidingObj, + EventFilterBase, + uses_item_registry, + uses_post_event, + wrap_func, +) +from HABApp.core.items import HINT_ITEM_OBJ, HINT_TYPE_ITEM_OBJ, BaseItem, BaseValueItem from HABApp.rule import interfaces from HABApp.rule.scheduler import HABAppSchedulerView as _HABAppSchedulerView + from .interfaces import async_subprocess_exec -from .interfaces.rule_subprocess import build_exec_params, HINT_PYTHON_PATH, HINT_EXEC_ARGS, HINT_PROCESS_CB_SIMPLE, \ - HINT_PROCESS_CB_FULL +from .interfaces.rule_subprocess import ( + HINT_EXEC_ARGS, + HINT_PROCESS_CB_FULL, + HINT_PROCESS_CB_SIMPLE, + HINT_PYTHON_PATH, + build_exec_params, +) from .rule_hook import get_rule_hook as _get_rule_hook if PYTHON_310: @@ -110,7 +123,7 @@ def post_event(self, name: Union[HINT_ITEM_OBJ, str], event: Any): ) def listen_event(self, name: Union[HINT_ITEM_OBJ, str], - callback: HINT_EVENT_CALLBACK, + callback: TYPE_EVENT_CALLBACK, event_filter: Optional[HINT_EVENT_FILTER_OBJ] = None ) -> HINT_EVENT_BUS_LISTENER: """ @@ -249,11 +262,11 @@ def get_rule(self, rule_name: str) -> 'Union[Rule, List[Rule]]': @staticmethod def get_items(type: Union[Tuple[HINT_TYPE_ITEM_OBJ, ...], HINT_TYPE_ITEM_OBJ] = None, - name: Union[str, Pattern[str]] = None, - tags: Union[str, Iterable[str]] = None, - groups: Union[str, Iterable[str]] = None, - metadata: Union[str, Pattern[str]] = None, - metadata_value: Union[str, Pattern[str]] = None, + name: Union[str, Pattern[str], None] = None, + tags: Union[str, Iterable[str], None] = None, + groups: Union[str, Iterable[str], None] = None, + metadata: Union[str, Pattern[str], None] = None, + metadata_value: Union[str, Pattern[str], None] = None, ) -> Union[List[HINT_ITEM_OBJ], List[BaseItem]]: """Search the HABApp item registry and return the found items. diff --git a/src/HABApp/rule_ctx/rule_ctx.py b/src/HABApp/rule_ctx/rule_ctx.py index 871c8cf4..1558c377 100644 --- a/src/HABApp/rule_ctx/rule_ctx.py +++ b/src/HABApp/rule_ctx/rule_ctx.py @@ -1,10 +1,9 @@ import logging -from typing import Optional, Callable +from typing import Callable, Optional import HABApp from HABApp.core.const.topics import ALL_TOPICS -from HABApp.core.internals import Context, uses_item_registry, HINT_EVENT_BUS_LISTENER -from HABApp.core.internals import uses_event_bus +from HABApp.core.internals import HINT_EVENT_BUS_LISTENER, Context, uses_event_bus, uses_item_registry from HABApp.core.internals.event_bus import EventBusBaseListener event_bus = uses_event_bus() diff --git a/src/HABApp/util/multimode/item.py b/src/HABApp/util/multimode/item.py index 17d0774b..16a98b61 100644 --- a/src/HABApp/util/multimode/item.py +++ b/src/HABApp/util/multimode/item.py @@ -1,8 +1,9 @@ from threading import Lock -from typing import Optional, Dict, Any, Tuple, List +from typing import Any, Dict, List, Optional, Tuple from HABApp.core.const import MISSING from HABApp.core.items import Item + from .mode_base import HINT_BASE_MODE, BaseMode LOCK = Lock() @@ -117,7 +118,8 @@ def get_mode(self, name: str) -> HINT_BASE_MODE: try: return self.__values_by_name[name.lower()] except KeyError: - raise KeyError(f'Unknown mode "{name}"! Available: {", ".join(self.__values_by_name.keys())}') from None + msg = f'Unknown mode "{name}"! Available: {", ".join(self.__values_by_name.keys())}' + raise KeyError(msg) from None def calculate_value(self) -> Any: """Recalculate the value. If the new value is not ``MISSING`` the calculated value will be set as the item @@ -128,7 +130,7 @@ def calculate_value(self) -> Any: # recalculate value new_value = MISSING - for priority, child in self.__values_by_prio.items(): + for child in self.__values_by_prio.values(): new_value = child.calculate_value(new_value) # if nothing is set try the default diff --git a/src/HABApp/util/multimode/mode_switch.py b/src/HABApp/util/multimode/mode_switch.py index 8eb29f68..7e96ee67 100644 --- a/src/HABApp/util/multimode/mode_switch.py +++ b/src/HABApp/util/multimode/mode_switch.py @@ -64,7 +64,8 @@ def __init__(self, name: str, # prevent direct calling def set_enabled(self, value: bool, only_on_change: bool = False): """""" # Empty docstring so this function doesn't show up in Sphinx - raise PermissionError('Enabled is controlled through the switch item!') + msg = 'Enabled is controlled through the switch item!' + raise PermissionError(msg) def __switch_changed(self, event): self.__set_enable(event.value == ('ON' if not self.__invert_switch else 'OFF')) diff --git a/tests/test_core/test_wrapped_func.py b/tests/test_core/test_wrapped_func.py index b65d9754..7c6ba1a2 100644 --- a/tests/test_core/test_wrapped_func.py +++ b/tests/test_core/test_wrapped_func.py @@ -1,15 +1,13 @@ import asyncio from datetime import date -from unittest.mock import AsyncMock -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock import pytest import HABApp from HABApp.core.const.topics import TOPIC_ERRORS as TOPIC_ERRORS from HABApp.core.events import NoEventFilter -from HABApp.core.internals import EventBusListener -from HABApp.core.internals import wrap_func +from HABApp.core.internals import EventBusListener, wrap_func from tests.helpers import TestEventBus @@ -66,7 +64,7 @@ async def async_func_div_error(): 1 / 0 -@pytest.mark.ignore_log_errors +@pytest.mark.ignore_log_errors() @pytest.mark.parametrize( 'func, name', ((func_div_error, 'func_div_error'), (async_func_div_error, 'async_func_div_error'))) async def test_async_error_wrapper(eb: TestEventBus, name, func, sync_worker): diff --git a/tests/test_openhab/test_rest/test_value_convert.py b/tests/test_openhab/test_rest/test_value_convert.py new file mode 100644 index 00000000..da031cc2 --- /dev/null +++ b/tests/test_openhab/test_rest/test_value_convert.py @@ -0,0 +1,11 @@ +from HABApp.openhab.connection.handler import convert_to_oh_type + + +def test_convert_to_oh_type(): + assert convert_to_oh_type(1 / 10 ** 3) == '0.001' + assert convert_to_oh_type(1 / 10 ** 6) == '0.000001' + assert convert_to_oh_type(1 / 10 ** 9) == '0.000000001' + + assert convert_to_oh_type(1.234 / 10 ** 3) == '0.001234' + assert convert_to_oh_type(1.234 / 10 ** 6) == '0.000001234' + assert convert_to_oh_type(1.234 / 10 ** 9) == '0.000000001234' diff --git a/tests/test_rule/test_process.py b/tests/test_rule/test_process.py index 9d4a5845..34bf0c91 100644 --- a/tests/test_rule/test_process.py +++ b/tests/test_rule/test_process.py @@ -43,11 +43,11 @@ def rule(monkeypatch): @pytest.mark.no_internals async def test_run_func_arg_errors(rule): - with pytest.raises(ValueError) as e: + with pytest.raises(TypeError) as e: rule.execute_subprocess(rule.cb, sys.executable, "asfd", 123) assert str(e.value) == 'args[2] is not of type str! "123" (int)' - with pytest.raises(ValueError) as e: + with pytest.raises(TypeError) as e: rule.execute_subprocess( rule.cb, sys.executable, "asfd", additional_python_path=[Path(__file__).parent, 123] ) diff --git a/tox.ini b/tox.ini index c9385b16..4f8e4380 100644 --- a/tox.ini +++ b/tox.ini @@ -5,17 +5,17 @@ envlist = py39 py310 py311 + py312 docs - [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310, flake, docs 3.11: py311 - + 3.12: py312 [testenv]