Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
315 changes: 253 additions & 62 deletions appdaemon/adapi.py

Large diffs are not rendered by default.

7 changes: 2 additions & 5 deletions appdaemon/adbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from appdaemon import adapi
from appdaemon import utils
from appdaemon.models.config.app import AppConfig, AllAppConfig

from appdaemon import adapi, utils
from appdaemon.models.config.app import AllAppConfig, AppConfig

# Check if the module is being imported using the legacy method
if __name__ == Path(__file__).name:
Expand Down Expand Up @@ -79,7 +77,6 @@ class ADBase:
args: dict
"""Dictionary of the app configuration
"""

_namespace: str

logger: Logger
Expand Down
2 changes: 2 additions & 0 deletions appdaemon/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@


class Admin:
name: str = "_admin"

def __init__(self, config_dir, logger, ad: "AppDaemon", **kwargs):
#
# Set Defaults
Expand Down
85 changes: 44 additions & 41 deletions appdaemon/appdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import RLock
from typing import TYPE_CHECKING, Optional

from typing import TYPE_CHECKING, Any

from appdaemon.admin_loop import AdminLoop
from appdaemon.app_management import AppManagement
Expand All @@ -30,7 +29,8 @@


class AppDaemon(metaclass=Singleton):
"""Top-level container for the subsystem objects. This gets passed to the subsystem objects and stored in them as the ``self.AD`` attribute.
"""Top-level container for the subsystem objects. This gets passed to the subsystem objects and stored in them as
the ``self.AD`` attribute.

Asyncio:

Expand Down Expand Up @@ -81,36 +81,22 @@ class AppDaemon(metaclass=Singleton):
"""

# subsystems
app_management: AppManagement
callbacks: Callbacks = None
events: Events = None
futures: Futures
app_management: "AppManagement"
callbacks: "Callbacks"
events: "Events"
futures: "Futures"
logging: "Logging"
plugins: PluginManagement
scheduler: Scheduler
services: Services
sequences: Sequences
state: State
threading: Threading
thread_async: ThreadAsync = None
utility: Utility

# settings
app_dir: Path
"""Defined in the main YAML config under ``appdaemon.app_dir``. Defaults to ``./apps``
"""
config_dir: Path
"""Path to the AppDaemon configuration files. Defaults to the first folder that has ``./apps``

- ``~/.homeassistant``
- ``/etc/appdaemon``
"""
apps: bool
"""Flag for whether ``disable_apps`` was set in the AppDaemon config
"""

admin_loop: AdminLoop | None = None
http: Optional["HTTP"] = None
plugins: "PluginManagement"
scheduler: "Scheduler"
services: "Services"
sequences: "Sequences"
state: "State"
threading: "Threading"
thread_async: "ThreadAsync | None" = None
utility: "Utility"

admin_loop: "AdminLoop | None" = None
http: "HTTP | None" = None
global_lock: RLock = RLock()

# shut down flag
Expand All @@ -122,13 +108,13 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
self.config = ad_config_model
self.booted = "booting"
self.logger = logging.get_logger()
self.logging.register_ad(self) # needs to go last to reference the config object
self.logging.register_ad(self) # needs to go last to reference the config object

self.global_vars = {}
self.global_vars: Any = {}
self.main_thread_id = threading.current_thread().ident

if not self.apps:
self.logging.log("INFO", "Apps are disabled")
self.logger.info("Apps are disabled")

# Initialize subsystems
self.callbacks = Callbacks(self)
Expand All @@ -143,7 +129,8 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
assert self.config_dir is not None, "Config_dir not set. This is a development problem"
assert self.config_dir.exists(), f"{self.config_dir} does not exist"
assert os.access(
self.config_dir, os.R_OK | os.X_OK
self.config_dir,
os.R_OK | os.X_OK,
), f"{self.config_dir} does not have the right permissions"

# this will always be None because it never gets set in ad_kwargs in __main__.py
Expand All @@ -152,7 +139,8 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
if not self.app_dir.exists():
self.app_dir.mkdir()
assert os.access(
self.app_dir, os.R_OK | os.W_OK | os.X_OK
self.app_dir,
os.R_OK | os.W_OK | os.X_OK,
), f"{self.app_dir} does not have the right permissions"

self.logger.info(f"Using {self.app_dir} as app_dir")
Expand All @@ -179,19 +167,25 @@ def __init__(self, logging: "Logging", loop: BaseEventLoop, ad_config_model: App
# Property definitions
#
@property
def admin_delay(self):
def admin_delay(self) -> int:
return self.config.admin_delay

@property
def api_port(self):
def api_port(self) -> int | None:
return self.config.api_port

@property
def app_dir(self):
def app_dir(self) -> Path:
"""Defined in the main YAML config under ``appdaemon.app_dir``. Defaults to ``./apps``"""
return self.config.app_dir

@app_dir.setter
def app_dir(self, path: os.PathLike) -> None:
self.config.app_dir = Path(path)

@property
def apps(self):
"""Flag for whether ``disable_apps`` was set in the AppDaemon config"""
return not self.config.disable_apps

@property
Expand All @@ -204,8 +198,17 @@ def check_app_updates_profile(self):

@property
def config_dir(self):
"""Path to the AppDaemon configuration files. Defaults to the first folder that has ``./apps``

- ``~/.homeassistant``
- ``/etc/appdaemon``
"""
return self.config.config_dir

@config_dir.setter
def config_dir(self, path: os.PathLike) -> None:
self.config.config_dir = Path(path)

@property
def config_file(self):
return self.config.config_file
Expand Down Expand Up @@ -366,7 +369,7 @@ def terminate(self):
def register_http(self, http: "HTTP"):
"""Sets the ``self.http`` attribute with a :class:`~.http.HTTP` object and starts the admin loop."""

self.http: "HTTP" = http
self.http = http
# Create admin loop

if http.old_admin is not None or http.admin is not None:
Expand Down
20 changes: 11 additions & 9 deletions appdaemon/events.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from collections.abc import Iterable
import datetime
import json
import traceback
import uuid
from collections.abc import Callable, Iterable
from copy import deepcopy
from logging import Logger
from typing import TYPE_CHECKING, Any, Protocol
from collections.abc import Callable

import appdaemon.utils as utils

from .plugin_management import PluginBase

if TYPE_CHECKING:
from appdaemon.appdaemon import AppDaemon

Expand Down Expand Up @@ -190,13 +191,14 @@ async def fire_event(self, namespace: str, event: str, **kwargs):

self.logger.debug("fire_plugin_event() %s %s %s", namespace, event, kwargs)
plugin = self.AD.plugins.get_plugin_object(namespace)

if hasattr(plugin, "fire_plugin_event"):
# We assume that the event will come back to us via the plugin
return await plugin.fire_plugin_event(event, namespace, **kwargs)
else:
# Just fire the event locally
await self.AD.events.process_event(namespace, {"event_type": event, "data": kwargs})
match plugin:
case PluginBase() as plugin:
if hasattr(plugin, "fire_plugin_event"):
# We assume that the event will come back to us via the plugin
return await plugin.fire_plugin_event(event, namespace, **kwargs)
else:
# Just fire the event locally
await self.AD.events.process_event(namespace, {"event_type": event, "data": kwargs})

async def process_event(self, namespace: str, data: dict[str, Any]):
"""Processes an event that has been received either locally or from a plugin.
Expand Down
12 changes: 9 additions & 3 deletions appdaemon/plugin_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import importlib
import sys
import traceback
from collections.abc import Generator, Iterable
from collections.abc import Generator, Iterable, Mapping
from logging import Logger
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Type
Expand Down Expand Up @@ -138,7 +138,13 @@ async def remove_entity(self, namespace: str, entity: str) -> None:
# pass

# @abc.abstractmethod
async def fire_plugin_event(self):
async def fire_plugin_event(
self,
event: str,
namespace: str,
timeout: str | int | float | datetime.timedelta | None = None,
**kwargs: Any,
) -> dict[str, Any] | None: # fmt: skip
raise NotImplementedError

@utils.warning_decorator(error_text="Unexpected error during notify_plugin_started()")
Expand Down Expand Up @@ -233,7 +239,7 @@ class PluginManagement:
"""Flag for if PluginManagement should be shutting down
"""

def __init__(self, ad: "AppDaemon", config: dict[str, PluginConfig]):
def __init__(self, ad: "AppDaemon", config: Mapping[str, PluginConfig]):
self.AD = ad
self.config = config
self.stopping = False
Expand Down
8 changes: 7 additions & 1 deletion appdaemon/plugins/hass/hassplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,13 @@ async def call_plugin_service(
#

@hass_check
async def fire_plugin_event(self, event, namespace, timeout: float | None = None, **kwargs) -> dict | None:
async def fire_plugin_event(
self,
event: str,
namespace: str,
timeout: str | int | float | datetime.timedelta | None = None,
**kwargs: Any,
) -> dict[str, Any] | None: # fmt: skip
# if we get a request for not our namespace something has gone very wrong
assert namespace == self.namespace

Expand Down
1 change: 1 addition & 0 deletions appdaemon/plugins/mqtt/mqttplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class MqttPlugin(PluginBase):
mqtt_wildcards: list[str]
mqtt_binary_topics: list[str]
mqtt_lock: Lock
name: str = "_mqtt"

def __init__(self, ad: "AppDaemon", name: str, config: MQTTConfig):
"""Initialize MQTT Plugin."""
Expand Down
1 change: 1 addition & 0 deletions appdaemon/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Scheduler:

schedule: dict[str, dict[str, Any]]

name: str = "_scheduler"
active: bool = False
realtime: bool = True
stopping: bool = False
Expand Down
13 changes: 7 additions & 6 deletions appdaemon/state.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from datetime import timedelta
import threading
import traceback
import uuid
from copy import copy, deepcopy
from datetime import timedelta
from logging import Logger
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Set, Union, overload
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Set, overload

from . import exceptions as ade
from . import utils
Expand Down Expand Up @@ -74,8 +74,8 @@ async def add_namespace(
namespace: str,
writeback: str,
persist: bool,
name: str = None
) -> Union[bool, Path]:
name: str | None = None,
) -> Path | bool | None: # fmt: skip
"""Used to Add Namespaces from Apps"""

if self.namespace_exists(namespace):
Expand Down Expand Up @@ -533,11 +533,12 @@ async def add_entity(
self,
namespace: str,
entity: str,
state: str | dict,
state: str | dict[str, Any],
attributes: Optional[dict] = None
):
) -> None:
"""Adds an entity to the internal state registry and fires the ``__AD_ENTITY_ADDED`` event"""
if self.entity_exists(namespace, entity):
self.logger.warning("%s already exists, will not be adding it", entity)
return

state = {
Expand Down
4 changes: 4 additions & 0 deletions appdaemon/stream/adstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@


class ADStream:
name: str = "_adstream"

def __init__(self, ad: AppDaemon, app, transport):
self.AD = ad
self.logger = ad.logging.get_child("_stream")
Expand Down Expand Up @@ -80,6 +82,8 @@ async def process_event(self, data): # noqa: C901
## directly. Only Create public methods here if you wish to make them
## stream commands.
class RequestHandler:
name: str = "_request_handler"

def __init__(self, ad: AppDaemon, adstream, handle, request):
self.AD = ad
self.handle = handle
Expand Down
5 changes: 4 additions & 1 deletion appdaemon/stream/sockjs_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import traceback
import json
import traceback

import sockjs

from appdaemon import utils as utils
Expand Down Expand Up @@ -49,6 +50,8 @@ def makeStream(self, ad, request, **kwargs):


class SockJSStream:
name: str = "_sockjsstream"

def __init__(self, ad, session, **kwargs):
self.AD = ad
self.session = session
Expand Down
Loading
Loading