diff --git a/readme.md b/readme.md index ddcaf195..bcfba3ba 100644 --- a/readme.md +++ b/readme.md @@ -127,7 +127,11 @@ MyOpenhabRule() ``` # Changelog -#### 23.09.0 (2023-XX-XX) +#### 23.09.1 (2023-09-18) +- Log a warning for broken links between items and things +- Fix CI + +#### 23.09.0 (2023-09-12) - Switched version number scheme to CalVer (Calendar Versioning): ``YEAR.MONTH.COUNTER`` - Fail fast when a value instead of a callback is passed to the event listener / scheduler - Completely removed types and type hints from traceback diff --git a/src/HABApp/__version__.py b/src/HABApp/__version__.py index 460e109e..545ab9b8 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.0' +__version__ = '23.09.1' diff --git a/src/HABApp/mqtt/connection/handler.py b/src/HABApp/mqtt/connection/handler.py index 5c53b9a2..4259ac3a 100644 --- a/src/HABApp/mqtt/connection/handler.py +++ b/src/HABApp/mqtt/connection/handler.py @@ -73,6 +73,9 @@ 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/openhab/connection/connection.py b/src/HABApp/openhab/connection/connection.py index 2973911f..1067fee5 100644 --- a/src/HABApp/openhab/connection/connection.py +++ b/src/HABApp/openhab/connection/connection.py @@ -44,7 +44,7 @@ def setup(): from HABApp.openhab.connection.plugins import (WaitForStartlevelPlugin, LoadOpenhabItemsPlugin, SseEventListenerPlugin, OUTGOING_PLUGIN, LoadTransformationsPlugin, WaitForPersistenceRestore, PingPlugin, ThingOverviewPlugin, - TextualThingConfigPlugin) + TextualThingConfigPlugin, BrokenLinksPlugin) connection = Connections.add(OpenhabConnection()) connection.register_plugin(CONNECTION_HANDLER) @@ -59,6 +59,7 @@ def setup(): connection.register_plugin(WaitForPersistenceRestore(), 110) connection.register_plugin(TextualThingConfigPlugin(), 120) connection.register_plugin(ThingOverviewPlugin(), 500_000) + connection.register_plugin(BrokenLinksPlugin(), 500_001) connection.register_plugin(ConnectionStateToEventBusPlugin()) connection.register_plugin(AutoReconnectPlugin()) diff --git a/src/HABApp/openhab/connection/plugins/__init__.py b/src/HABApp/openhab/connection/plugins/__init__.py index 2273b80b..dd374f2f 100644 --- a/src/HABApp/openhab/connection/plugins/__init__.py +++ b/src/HABApp/openhab/connection/plugins/__init__.py @@ -7,3 +7,4 @@ from .wait_for_restore import WaitForPersistenceRestore from .overview_things import ThingOverviewPlugin from .plugin_things import TextualThingConfigPlugin +from .overview_broken_links import BrokenLinksPlugin diff --git a/src/HABApp/openhab/connection/plugins/overview_broken_links.py b/src/HABApp/openhab/connection/plugins/overview_broken_links.py new file mode 100644 index 00000000..e2f68a3f --- /dev/null +++ b/src/HABApp/openhab/connection/plugins/overview_broken_links.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import logging +from typing import Final + +from HABApp.config import CONFIG +from HABApp.core.connections import BaseConnectionPlugin +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 + +PING_CONFIG: Final = CONFIG.openhab.ping + +Items = uses_item_registry() + + +class BrokenLinksPlugin(BaseConnectionPlugin[OpenhabConnection]): + + def __init__(self, name: str | None = None): + super().__init__(name) + self.do_run = True + + async def on_online(self): + if not self.do_run: + return None + self.do_run = False + + log = logging.getLogger('HABApp.openhab.links') + + things = await async_get_things() + links = await async_get_links() + + available_things = {t.uid for t in things} + available_channels = {c.uid for t in things for c in t.channels} + + for link in sorted(links, key=lambda x: x.channel): + if not Items.item_exists(link.item): + log_warning(log, f'Item "{link.item}" does not exist! ' + f'(link between item "{link.item:s}" and channel "{link.channel:s}")') + continue + + if link.channel not in available_channels: + # check if the thing exists + thing_uid, channel_id = link.channel.rsplit(':', maxsplit=1) + if thing_uid in available_things: + log_warning(log, f'Channel "{channel_id}" on thing "{thing_uid:s}" does not exist! ' + f'(link between item "{link.item:s}" and channel "{link.channel:s}")') + else: + log_warning(log, f'Thing "{thing_uid:s}" does not exist! ' + f'(link between item "{link.item:s}" and channel "{link.channel:s}")') diff --git a/tests/test_openhab/test_plugins/test_broken_links.py b/tests/test_openhab/test_plugins/test_broken_links.py new file mode 100644 index 00000000..bfe9b79d --- /dev/null +++ b/tests/test_openhab/test_plugins/test_broken_links.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import logging +from functools import partial + +import HABApp.openhab.connection.plugins.overview_broken_links as plugin_module +from HABApp.core.internals import ItemRegistry +from HABApp.core.items import Item +from HABApp.openhab.definitions.rest import ThingResp, ItemChannelLinkResp +from HABApp.openhab.definitions.rest.things import ThingStatusResp, ChannelResp + + +async def _mock_things() -> list[ThingResp]: + return [ + ThingResp( + uid='thing_type:uid', thing_type='thing_type', + status=ThingStatusResp(status='ONLINE', detail='ONLINE'), + editable=False, + channels=[ + ChannelResp(uid='thing_type:uid:channel1', id='channel1', channel_type='channel1_type', + item_type='String', kind='STATE', linked_items=[]), + ChannelResp(uid='thing_type:uid:channel2', id='channel2', channel_type='channel2_type', + item_type='String', kind='STATE', linked_items=[]) + ] + ) + ] + + +async def _mock_links() -> list[ItemChannelLinkResp]: + return [ + ItemChannelLinkResp(item='item1', channel='thing_type:uid:channel1', editable=True), # okay + ItemChannelLinkResp(item='item2', channel='thing_type:uid:channel1', editable=True), # item does not exist + ItemChannelLinkResp(item='item1', channel='thing_type:uid:channel3', editable=True), # channel does not exist + ItemChannelLinkResp(item='item1', channel='other_thing:uid:channel1', editable=True), # thing does not exist + ] + + +async def test_link_warning(monkeypatch, ir: ItemRegistry, test_logs): + monkeypatch.setattr(plugin_module, 'async_get_things', _mock_things) + monkeypatch.setattr(plugin_module, 'async_get_links', _mock_links) + + ir.add_item(Item('item1')) + + p = plugin_module.BrokenLinksPlugin() + await p.on_online() + + add = partial(test_logs.add_expected, 'HABApp.openhab.links', logging.WARNING) + + add('Item "item2" does not exist! (link between item "item2" and channel "thing_type:uid:channel1")') + add('Channel "channel3" on thing "thing_type:uid" does not exist! ' + '(link between item "item1" and channel "thing_type:uid:channel3")') + add('Thing "other_thing:uid" does not exist! (link between item "item1" and channel "other_thing:uid:channel1")') + + # ensure that it runs only once + async def do_raise(): + raise ValueError() + + monkeypatch.setattr(plugin_module, 'async_get_things', do_raise) + monkeypatch.setattr(plugin_module, 'async_get_links', do_raise) + await p.on_online()