class BaseWatch:
    EVENT: typing.Union[typing.Type[ItemNoUpdateEvent],
                        typing.Type[ItemNoChangeEvent]]

    def __init__(self, name: str, secs: typing.Union[int, float]):
        self.fut = PendingFuture(self._post_event, secs)
        self.name: str = name

    async def _post_event(self):
        HABApp.core.EventBus.post_event(self.name,
                                        self.EVENT(self.name, self.fut.secs))

    async def __cancel_watch(self):
        self.fut.cancel()
        log.debug(
            f'Canceled {self.__class__.__name__} ({self.fut.secs}s) for {self.name}'
        )

    def cancel(self):
        """Cancel the item watch"""
        asyncio.run_coroutine_threadsafe(self.__cancel_watch(), loop)

    def listen_event(
        self, callback: typing.Callable[[typing.Any], typing.Any]
    ) -> 'HABApp.core.EventBusListener':
        """Listen to (only) the event that is emitted by this watcher"""
        rule = HABApp.rule.get_parent_rule()
        cb = HABApp.core.WrappedFunction(callback,
                                         name=rule._get_cb_name(callback))
        listener = EventFilter(self.EVENT,
                               seconds=self.fut.secs).create_event_listener(
                                   self.name, cb)
        return rule._add_event_listener(listener)
Esempio n. 2
0
    def __init__(self):
        super().__init__()
        self.created_items: Dict[str, Set[str]] = {}
        self.do_cleanup = PendingFuture(self.clean_items, 120)

        self.watcher: Optional[AggregatingAsyncEventHandler] = None

        self.cache_ts: float = 0.0
        self.cache_cfg: dict = {}
Esempio n. 3
0
class BaseWatch:
    EVENT: typing.Union[typing.Type[ItemNoUpdateEvent],
                        typing.Type[ItemNoChangeEvent]]

    def __init__(self, name: str, secs: typing.Union[int, float]):
        self._fut = PendingFuture(self._post_event, secs)
        self._name: str = name

    async def _post_event(self):
        HABApp.core.EventBus.post_event(self._name,
                                        self.EVENT(self._name, self._fut.secs))

    async def __cancel_watch(self):
        self._fut.cancel()

    def cancel(self):
        """Cancel the item watch"""
        asyncio.run_coroutine_threadsafe(self.__cancel_watch(), loop)
Esempio n. 4
0
async def test_pending_future_cancel():
    exception = None

    async def b():
        nonlocal exception
        try:
            await asyncio.sleep(200)
        except Exception as e:
            exception = e

    p = PendingFuture(b, 0)
    p.reset()
    await asyncio.sleep(0.01)
    p.reset()
    await asyncio.sleep(0.01)
    if sys.version_info[:2] != (3, 8):
        assert isinstance(exception, asyncio.CancelledError)
    p.cancel()
Esempio n. 5
0
async def test_pending_future_cancel():
    exception = None

    async def b():
        nonlocal exception
        try:
            await asyncio.sleep(200)
        except BaseException as e:
            exception = e

    p = PendingFuture(b, 0)
    p.reset()
    await asyncio.sleep(0.05)
    p.reset()
    await asyncio.sleep(0.05)
    p.cancel()

    assert exception is not None
    assert isinstance(exception, asyncio.CancelledError)
Esempio n. 6
0
async def test_pending_future():
    a = 0

    async def b():
        nonlocal a
        a += 1

    p = PendingFuture(b, 0.01)
    for i in range(10):
        p.reset()
        await asyncio.sleep(0.05)
        assert i + 1 == a

    # let it run down once
    a = 0
    p = PendingFuture(b, 0.05)
    for i in range(10):
        p.reset()
        await asyncio.sleep(0.001)

    assert a == 0
    await asyncio.sleep(0.05)
    assert a == 1
Esempio n. 7
0
 def __init__(self, name: str, secs: typing.Union[int, float]):
     self._fut = PendingFuture(self._post_event, secs)
     self._name: str = name
Esempio n. 8
0
 def __init__(self):
     super().__init__()
     self.created_items: Dict[str, Set[str]] = {}
     self.do_cleanup = PendingFuture(self.clean_items, 120)
Esempio n. 9
0
class ManualThingConfig(OnConnectPlugin):

    def __init__(self):
        super().__init__()
        self.created_items: Dict[str, Set[str]] = {}
        self.do_cleanup = PendingFuture(self.clean_items, 120)

    async def on_connect_function(self):
        try:
            await asyncio.sleep(0.3)

            files = list(HABApp.core.lib.list_files(HABApp.CONFIG.directories.config, '.yml'))
            if not files:
                log.debug(f'No manual configuration files found in {HABApp.CONFIG.directories.config}')
                return None

            # if oh is not ready we will get None, but we will trigger again on reconnect
            data = await async_get_things()
            if data is None:
                return None

            for f in files:
                await self.update_thing_config(f, data)
        except asyncio.CancelledError:
            pass

    @HABApp.core.wrapper.ignore_exception
    async def clean_items(self):
        items = set()
        for s in self.created_items.values():
            items.update(s)
        await cleanup_items(items)

    @HABApp.core.wrapper.ignore_exception
    async def update_thing_config(self, path: Path, data=None):
        # we have to check the naming structure because we get file events for the whole folder
        _name = path.name.lower()
        if not _name.startswith('thing_') or not _name.endswith('.yml'):
            return None

        # only load if we don't supply the data
        if data is None:
            data = await async_get_things()

        # remove created items
        self.created_items.pop(path.name, None)
        created_items = self.created_items.setdefault(path.name, set())

        # shedule cleanup
        self.do_cleanup.reset()

        # output file
        output_file = path.with_suffix('.items')
        if output_file.is_file():
            output_file.unlink()

        # we also get events when the file gets deleted
        if not path.is_file():
            log.debug(f'File {path} does not exist -> skipping Thing configuration!')
            return None
        log.debug(f'Loading {path}!')

        # load the config file
        yml = HABApp.parameters.parameter_files._yml_setup
        with path.open(mode='r', encoding='utf-8') as file:
            try:
                cfg = yml.load(file)
            except Exception as e:
                HABAppError(log).add_exception(e).dump()
                return None

        # validate configuration
        cfg = validate_cfg(cfg, path.name)
        if cfg is None:
            return None

        # if one entry has test set we show an overview of all the things
        if any(map(lambda x: x.test, cfg)):
            log_overview(data, THING_ALIAS, 'Thing overview')

        # process each thing part in the cfg
        for cfg_entry in cfg:
            test: bool = cfg_entry.test
            things = list(apply_filters(cfg_entry.filter, data, test))

            # show a warning we found no Things
            if not things:
                log_warning(log, f'No things matched for {cfg_entry.filter}')
                continue

            # update thing configuration
            if cfg_entry.thing_config:
                await update_thing_cfg(cfg_entry.thing_config, things, test)

            try:
                # item creation for every thing
                create_items = {}
                shown_types = set()
                for thing in things:
                    thing_context = {k: thing.get(alias, '') for k, alias in THING_ALIAS.items()}

                    # create items without channel
                    for item_cfg in cfg_entry.get_items(thing_context):
                        name = item_cfg.name
                        if name in create_items:
                            raise ValueError(f'Duplicate item: {name}')
                        create_items[name] = item_cfg

                    # Channel overview, only if we have something configured
                    if test and cfg_entry.channels:
                        __thing_type = thing_context['thing_type']
                        if __thing_type not in shown_types:
                            shown_types.add(__thing_type)
                            log_overview(thing['channels'], CHANNEL_ALIAS, heading=f'Channels for {__thing_type}')

                    # do channel things
                    for channel_cfg in cfg_entry.channels:
                        channels = apply_filters(channel_cfg.filter, thing['channels'], test)
                        for channel in channels:
                            channel_context = {k: channel.get(alias, '') for k, alias in CHANNEL_ALIAS.items()}
                            channel_context.update(thing_context)

                            for item_cfg in channel_cfg.get_items(channel_context):
                                item_cfg.link = channel['uid']
                                name = item_cfg.name

                                if name in create_items:
                                    raise ValueError(f'Duplicate item: {name}')
                                create_items[name] = item_cfg

                    # newline only if we create logs
                    if test and (cfg_entry.create_items or cfg_entry.channels):
                        log.info('')
            except InvalidItemNameError as e:
                HABAppError(log).add_exception(e).dump()
                continue

            # Create all items
            for item_cfg in create_items.values():
                created = await create_item(item_cfg, test)
                if created:
                    created_items.add(item_cfg.name)

            self.do_cleanup.reset()

            create_items_file(output_file, create_items)
Esempio n. 10
0
        item._last_update.tasks.append(t)
    for t in data.change:
        item._last_change.tasks.append(t)


async def clean_tmp_data():
    now = datetime.now()
    diff_max = timedelta(seconds=CLEANUP.secs)

    to_del = []
    for name, obj in TMP_DATA.items():
        diff = now - obj.ts
        if diff < diff_max:
            continue

        to_del.append(name)

        # show a warning because otherwise it's not clear what is happening
        w = HABApp.core.logger.HABAppWarning(logging.getLogger('HABApp.Item'))
        w.add(
            f'Item {name} has been deleted {diff.total_seconds():.1f}s ago even though it has item watchers. '
            f'If it will be added again the watchers have to be created again, too!'
        )
        w.dump()

    for name in to_del:
        TMP_DATA.pop(name)


CLEANUP = PendingFuture(clean_tmp_data, 15)
Esempio n. 11
0
class ManualThingConfig(OnConnectPlugin):
    def __init__(self):
        super().__init__()
        self.created_items: Dict[str, Set[str]] = {}
        self.do_cleanup = PendingFuture(self.clean_items, 120)

        self.watcher: Optional[AggregatingAsyncEventHandler] = None

        self.cache_ts: float = 0.0
        self.cache_cfg: dict = {}

    def setup(self):
        path = HABApp.CONFIG.directories.config
        if not path.is_dir():
            log.info(
                'Config folder does not exist - textual thing config disabled!'
            )
            return None

        class HABAppThingConfigFile(HABAppFile):
            LOGGER = log
            LOAD_FUNC = self.file_load
            UNLOAD_FUNC = self.file_unload

        folder = add_habapp_folder('config/', path, 50)
        folder.add_file_type(HABAppThingConfigFile)
        self.watcher = folder.add_watch('.yml')

    async def file_unload(self, prefix: str, path: Path):
        return None

    async def on_connect_function(self):
        if self.watcher is None:
            return None

        try:
            await asyncio.sleep(0.3)

            self.cache_cfg = await async_get_things()
            self.cache_ts = time.time()

            await self.watcher.trigger_all()
        except asyncio.CancelledError:
            pass

    @HABApp.core.wrapper.ignore_exception
    async def clean_items(self):
        items = set()
        for s in self.created_items.values():
            items.update(s)
        await cleanup_items(items)

    async def file_load(self, name: str, path: Path):
        # we have to check the naming structure because we get file events for the whole folder
        _name = path.name.lower()
        if not _name.startswith('thing_') or not _name.endswith('.yml'):
            log.warning(
                f'Name for "{name}" does not start with "thing_" -> skip!')
            return None

        # only load if we don't supply the data
        if time.time() - self.cache_ts > 20 or not self.cache_cfg:
            self.cache_cfg = await async_get_things()
            self.cache_ts = time.time()

        data = self.cache_cfg

        # remove created items
        self.created_items.pop(path.name, None)
        created_items = self.created_items.setdefault(path.name, set())

        # shedule cleanup
        self.do_cleanup.reset()

        # output file
        output_file = path.with_suffix('.items')
        if output_file.is_file():
            output_file.unlink()

        # we also get events when the file gets deleted
        if not path.is_file():
            log.debug(
                f'File {path} does not exist -> skipping Thing configuration!')
            return None
        log.debug(f'Loading {name}!')

        # load the config file
        with path.open(mode='r', encoding='utf-8') as file:
            try:
                cfg = HABApp.core.const.yml.load(file)
            except Exception as e:
                HABAppError(log).add_exception(e).dump()
                return None

        # validate configuration
        cfg = validate_cfg(cfg, path.name)
        if cfg is None:
            return None

        # if one entry has test set we show an overview of all the things
        if any(map(lambda x: x.test, cfg)):
            log_overview(data, THING_ALIAS, 'Thing overview')

        # process each thing part in the cfg
        for cfg_entry in cfg:
            test: bool = cfg_entry.test
            things = list(apply_filters(cfg_entry.filter, data, test))

            # show a warning we found no Things
            if not things:
                log_warning(log, f'No things matched for {cfg_entry.filter}')
                continue

            # update thing configuration
            if cfg_entry.thing_config:
                await update_thing_cfg(cfg_entry.thing_config, things, test)

            try:
                # item creation for every thing
                create_items = {}
                shown_types = set()
                for thing in things:
                    thing_context = {
                        k: thing.get(alias, '')
                        for k, alias in THING_ALIAS.items()
                    }

                    # create items without channel
                    for item_cfg in cfg_entry.get_items(thing_context):
                        name = item_cfg.name
                        if name in create_items:
                            raise DuplicateItemError(f'Duplicate item: {name}')
                        create_items[name] = item_cfg

                    # Channel overview, only if we have something configured
                    if test and cfg_entry.channels:
                        __thing_type = thing_context['thing_type']
                        if __thing_type not in shown_types:
                            shown_types.add(__thing_type)
                            log_overview(
                                thing['channels'],
                                CHANNEL_ALIAS,
                                heading=f'Channels for {__thing_type}')

                    # do channel things
                    for channel_cfg in cfg_entry.channels:
                        channels = apply_filters(channel_cfg.filter,
                                                 thing['channels'], test)
                        for channel in channels:
                            channel_context = {
                                k: channel.get(alias, '')
                                for k, alias in CHANNEL_ALIAS.items()
                            }
                            channel_context.update(thing_context)

                            for item_cfg in channel_cfg.get_items(
                                    channel_context):
                                item_cfg.link = channel['uid']
                                name = item_cfg.name

                                if name in create_items:
                                    raise DuplicateItemError(
                                        f'Duplicate item: {name}')
                                create_items[name] = item_cfg

                    # newline only if we create logs
                    if test and (cfg_entry.create_items or cfg_entry.channels):
                        log.info('')
            except InvalidItemNameError as e:
                HABAppError(log).add_exception(e).dump()
                continue
            except DuplicateItemError as e:
                # Duplicates should never happen, the user clearly made a mistake, that's why we exit here
                HABAppError(log).add_exception(e).dump()
                return None

            # Create all items
            for item_cfg in create_items.values():
                created = await create_item(item_cfg, test)
                if created:
                    created_items.add(item_cfg.name)

            self.do_cleanup.reset()

            create_items_file(output_file, create_items)

            self.cache_cfg = {}