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
22 changes: 14 additions & 8 deletions appdaemon/adapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,13 @@ class ADAPI:
"""Pydantic model of the app configuration
"""
config: dict[str, Any]
"""Dictionary of the AppDaemon configuration
"""Dict of the AppDaemon configuration. This meant to be read-only, and modifying it won't affect any behavior.
"""
app_config: dict[str, dict[str, Any]]
"""Dict of the full config for all apps. This meant to be read-only, and modifying it won't affect any behavior.
"""
args: dict[str, Any]
"""Dictionary of this app's configuration
"""Dict of this app's configuration. This meant to be read-only, and modifying it won't affect any behavior.
"""
logger: Logger
err: Logger
Expand All @@ -75,6 +78,7 @@ def __init__(self, ad: AppDaemon, config_model: "AppConfig"):
self.config_model = config_model

self.config = self.AD.config.model_dump(by_alias=True, exclude_unset=True)
self.app_config = self.AD.app_management.app_config.model_dump(by_alias=True, exclude_unset=True)
self.args = config_model.model_dump(by_alias=True, exclude_unset=True)

self.dashboard_dir = None
Expand Down Expand Up @@ -123,10 +127,6 @@ def _get_namespace(self, **kwargs):
#
# Properties
#
@property
def app_config(self) -> dict[str, dict[str, Any]]:
"""Dict of the full dump of all the config for all apps"""
return self.AD.app_management.app_config.model_dump(by_alias=True, exclude_unset=True)

@property
def app_dir(self) -> Path:
Expand All @@ -144,8 +144,14 @@ def config_dir(self) -> Path:
return self.AD.config_dir

@property
def global_vars(self) -> dict:
return self.AD.global_vars
def global_vars(self) -> Any:
with self.AD.global_lock:
return self.AD.global_vars

@global_vars.setter
def global_vars(self, value: Any) -> None:
with self.AD.global_lock:
self.AD.global_vars = Any

@property
def _logging(self) -> Logging:
Expand Down
223 changes: 156 additions & 67 deletions docs/APPGUIDE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -466,74 +466,178 @@ In the App, the app_users can be accessed like every other argument the App can
App Dependencies
----------------

It is possible for apps to be dependent upon other apps. Some
examples where this might be the case are:
Apps can interact without any explicit references to each other by using because the calling app only needs
to know the service name ``<domain>/<service>`` to be able to use :py:meth:`~appdaemon.adapi.ADAPI.call_service`. It
doesn't need to reference or know anything about the app that provides the service. See
`service registration <#service-registration>`__ for more details on how to register services.

- A global App that defines constants for use in other apps
- An App that provides a service for other modules, e.g., a TTS App
Sometimes in development it's useful to intentionally create a dependency so that apps get reloaded together as files
change. This can be done with the ``dependencies`` direction in the app configuration.

In these cases, when changes are made to one of these apps, we also
want the apps that depend upon them to be reloaded. Furthermore, we
also want to guarantee that they are loaded in order so that the apps
depended upon by other modules are loaded first.
.. code-block:: yaml
:emphasize-lines: 9

AppDaemon fully supports this through the use of the dependency
directive in the App configuration. Using this directive, each App
identifies other apps that it depends upon. The dependency directive
will identify the name of the App it cares about, and AppDaemon
will see to it that the dependency is loaded before the App depending
on it, and that the dependent App will be reloaded if it changes.
# conf/apps/apps.yaml
my_provider:
module: provider
class: Provider

For example, an App ``Consumer``, uses another App ``Sound`` to play
sound files. ``Sound`` in turn uses ``Global`` to store some global
values. We can represent these dependencies as follows:
my_consumer:
module: consumer
class: Consumer
dependencies:
- my_provider

.. code:: yaml
In this example, both apps would get reloaded if anything in `provider.py` changes, and the ``my_provider`` app is
guaranteed to be loaded after the ``my_provider`` app.

Global:
module: global
class: Global
Imports
~~~~~~~

Sound
module: sound
class: Sound
dependencies: Global
Apps in AppDaemon can import from other python files in the apps directory, and it's a common pattern to have a single
file containing global data that gets imported by multiple other apps.

Consumer:
module: sound
class: Sound
dependencies: Sound
This shows a complete example of defining some things in a single file `globals.py` that are used by both apps defined
in `app_a.py` and `app_b.py`.

It is also possible to have multiple dependencies, added as a yaml list
.. code-block:: text
:caption: Example App Directory Structure with Globals

.. code:: yaml
conf/apps
├── apps.yaml
├── globals.py
└── my_apps
├── app_a.py
└── app_b.py

.. code-block:: yaml
:caption: Example App Configuration File

Consumer:
module: sound
class: Sound
# conf/apps/apps.yaml
AppA:
module: app_a
class: AppA
dependencies:
- Sound
- Global
- AppB # This is only set to demonstrate forcing it to load after AppB

In TOML this would be:
AppB:
module: app_b
class: AppB

.. code:: toml
.. code-block:: python
:caption: Example Global File

[Consumer]
module = "sound"
class = "Sound"
dependencies = [ "Sound", "Global" ]
# conf/apps/globals.py
from enum import Enum


AppDaemon will write errors to the log if a dependency is missing and it
will also detect circular dependencies.
GLOBAL_VAR = "Hello, World!"

Dependencies can also be set using the ``register_dependency()`` api call.

App Loading Priority
--------------------
class ModeSelect(Enum):
MODE_A = 'mode_a'
MODE_B = 'mode_b'
MODE_C = 'mode_c'

GLOBAL_MODE = ModeSelect.MODE_B

.. code-block:: python

# conf/apps/app_a.py
from appdaemon.adapi import ADAPI
from globals import GLOBAL_MODE, GLOBAL_VAR


class AppA(ADAPI):
def initialize(self) -> None:
self.log(GLOBAL_VAR)
self.log(f'Global mode is set to: {GLOBAL_MODE.value}')

def terminate(self) -> None: ...

.. code-block:: python

# conf/apps/app_b.py
from appdaemon.adapi import ADAPI
from globals import GLOBAL_MODE, GLOBAL_VAR


class AppB(ADAPI):
def initialize(self) -> None:
self.log(GLOBAL_VAR)
self.log(f'Global mode is set to: {GLOBAL_MODE.value}')

def terminate(self) -> None: ...

AppDaemon understands that both `app_a.py` and `app_b.py` depend on `globals.py` because of the import statement, so any
changes to `globals.py` will effectively trigger a reload of both ``AppA`` and ``AppB``. Just for the example, ``AppA``
was given a dependency on ``AppB``, which will cause it to always stopped before ``AppB`` and always started after
``AppB``.

For example, if ``GLOBAL_MODE`` is set to ``ModeSelect.MODE_C`` in `globals.py`, the log output would look like this:

It is possible to influence the loading order of Apps using the dependency system. To add a loading priority to an App, simply add a ``priority`` entry to its parameters. e.g.:
.. code-block:: text

INFO AppDaemon: Calling initialize() for AppB
INFO AppB: Hello, World!
INFO AppB: Global mode is set to: mode_b
INFO AppDaemon: Calling initialize() for AppA
INFO AppA: Hello, World!
INFO AppA: Global mode is set to: mode_b
...
INFO AppDaemon: Calling terminate() for 'AppA'
INFO AppDaemon: Calling terminate() for 'AppB'
INFO AppDaemon: Calling initialize() for AppB
INFO AppB: AppB Initialized
INFO AppB: Hello, World!
INFO AppB: Global mode is set to: mode_c
INFO AppDaemon: Calling initialize() for AppA
INFO AppA: AppA Initialized
INFO AppA: Hello, World!
INFO AppA: Global mode is set to: mode_c

Globals
~~~~~~~

.. admonition:: Global Modules
:class: warning

Global modules are deprecated and will be removed in a future release. AppDaemon now automatically tracks and
resolves dependencies by parsing files using the :py:mod:`ast <ast>` package from the standard library.

This is a legacy feature, but apps still have the ability to access a variable that's shared globally across all apps in
their ``self.global_vars`` attribute. Accessing this variable is wrapped with a the global lock, so it is safe to read
and write between threads, although it's advised to lock entire methods with the ``global_lock`` decorator.

In this example, the ``global_vars`` would remain locked throughout the duration of the ``do_something`` method.

.. code-block:: python

# conf/apps/simple.py
from appdaemon import adbase as ad
from appdaemon.adapi import ADAPI

class SimpleApp(ADAPI):
def initialize(self) -> None:
self.do_something()

@ad.global_lock
def do_something(self):
vars = self.global_vars
... # do some operations
self.global_vars = vars

App Priorities
~~~~~~~~~~~~~~

The priority system is complementary to the dependency system, but they are trying to solve different problems.
Dependencies should be used when an app literally depends upon another, for instance, it is using variables stored in it
with the ``get_app()`` call. Priorities should be used when an app does some setup for other apps but doesn't provide
variables or code for the dependent app. An example of this might be an app that sets up some sensors in Home Assistant,
or sets some switch or input_slider to a specific value. It may be necessary for that setup to be performed before other
apps are started, but there is no requirement to reload those apps if the first app changes.

To add a priority to an app, simply add a ``priority`` entry to its configuration. e.g.:

.. code:: yaml

Expand All @@ -544,16 +648,10 @@ It is possible to influence the loading order of Apps using the dependency syste
light: light.downstairs_hall
priority: 10


Priorities can be any number you like, and can be float values if required, the lower the number, the higher the priority. AppDaemon will load any modules with a priority in the order specified.

For modules with no priority specified, the priority is assumed to be ``50``. It is, therefore, possible to cause modules to be loaded before and after modules with no priority.

The priority system is complementary to the dependency system, although they are trying to solve different problems. Dependencies should be used when an App literally depends upon another, for instance, it is using variables stored in it with the ``get_app()`` call. Priorities should be used when an App does some setup for other apps but doesn't provide variables or code for the dependent App. An example of this might be an App that sets up some sensors in Home Assistant, or sets some switch or input_slider to a specific value. It may be necessary for that setup to be performed before other apps are started, but there is no requirement to reload those apps if the first App changes.

To accommodate both systems, dependency trees are assigned priorities in the range 50 - 51, again allowing apps to set priorities such that they will be loaded before or after specific sets of dependent apps.

Note that apps that are dependent upon other apps, and apps that are depended upon by other apps will ignore any priority setting in their configuration.
Priorities can be any floating point number, and the lower the value, the higher the priority. All apps are guaranteed
to load and start before apps that have a higher priority number. However, explicitly declared dependencies will always
take precedence over priorities. By default all apps have a priority of ``50``. It's therefore possible to cause modules
to be loaded before or after modules without a priority explicitly set.

App Log
-------
Expand All @@ -572,15 +670,6 @@ Starting from AD 4.0, it is now possible to determine which log as declared by t

By declaring the above, each time the function ``self.log()`` is used within the App, the log entry is sent to the user defined ``lights_log``. It is also possible to write to another log, within the same App if need be. This is done using the function ``self.log(text, log='main_log')``. Without using any of the aforementioned log capabilities, all logs from apps by default will be sent to the ``main_log``.

Global Module Dependencies
--------------------------

.. admonition:: Deprecation warning
:class: warning

Global modules are deprecated and will be removed in a future release. AppDaemon now automatically tracks and
resolves dependencies using the :py:mod:`ast <ast>` package from the standard library.

AppDir Structure
----------------

Expand Down
Loading