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()
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)
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
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)
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 = {}