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
2 changes: 1 addition & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v5
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
124 changes: 65 additions & 59 deletions appdaemon/adapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,99 +587,115 @@ async def get_pin_thread(self) -> int:
# Namespace
#

def set_namespace(self, namespace: str, writeback: str = "safe", persist: bool = True) -> None:
"""Set the current namespace of the app
def set_namespace(
self,
namespace: str,
writeback: Literal["safe", "hybrid"] | utils.ADWritebackType = "safe",
persist: bool = True,
) -> None:
"""Set the current namespace of the app.

This will create a new namespace if it doesn't already exist. By default, this will be a persistent namespace
with ``safe`` writeback, which means that all state changes will be stored to disk as they happen.

See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
See the :py:ref:`app_namespaces` for more information.

Args:
namespace (str): Name of the new namespace
writeback (str, optional): The writeback to be used if a new namespace gets created. Will be ``safe`` by
default.
persist (bool, optional): Whether to make the namespace persistent if a new one is created. Defaults to
``True``.
`True`.

Returns:
None.

Examples:
>>> self.set_namespace("hass1")
Create a namespace that buffers state changes in memory and periodically writes them to disk.

>>> self.set_namespace("on_disk", writeback="hybrid", persist=True)

Create an in-memory namespace that won't survive AppDaemon restarts.

>>> self.set_namespace("in_memory", persist=False)
"""
# Keeping namespace get/set functions for legacy compatibility
if not self.namespace_exists(namespace):
self.add_namespace(namespace=namespace, writeback=writeback, persist=persist)
self.add_namespace(
namespace=namespace,
writeback=utils.ADWritebackType(writeback),
persist=persist
)
self.namespace = namespace

def get_namespace(self) -> str:
"""Get the app's current namespace.

See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
See :py:ref:`app_namespaces` for more information.
"""
# Keeping namespace get/set functions for legacy compatibility
return self.namespace

@utils.sync_decorator
async def namespace_exists(self, namespace: str) -> bool:
"""Check the existence of a namespace in AppDaemon.
"""Check for the existence of a namespace.

See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
See :py:ref:`app_namespaces` for more information.

Args:
namespace (str): The namespace to be checked.
namespace (str): The namespace to check for.

Returns:
bool: ``True`` if the namespace exists, otherwise ``False``.

Examples:
Check if the namespace ``storage`` exists within AD

>>> if self.namespace_exists("storage"):
>>> #do something like create it

bool: `True` if the namespace exists, otherwise `False`.
"""
return self.AD.state.namespace_exists(namespace)

@utils.sync_decorator
async def add_namespace(self, namespace: str, writeback: str = "safe", persist: bool = True) -> str | None:
"""Add a user-defined namespace, which has a database file associated with it.

When AppDaemon restarts these entities will be loaded into the namespace with all their previous states. This
can be used as a basic form of non-volatile storage of entity data. Depending on the configuration of the
namespace, this function can be setup to constantly be running automatically
or only when AD shutdown.
async def add_namespace(
self,
namespace: str,
writeback: Literal["safe", "hybrid"] | utils.ADWritebackType = "safe",
persist: bool = True,
) -> str | None:
"""Add a user-defined namespace.

See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
See the :py:ref:`app_namespaces` for more information.

Args:
namespace (str): The name of the new namespace to create
writeback (optional): The writeback to be used. Will be ``safe`` by default
writeback (optional): The writeback to be used. Defaults to ``safe``, which writes every state change to
disk. This can be problematic for namespaces that have a lot of state changes. `Safe` in this case
refers data loss, rather than performance. The other option is ``hybrid``, which buffers state changes.
persist (bool, optional): Whether to make the namespace persistent. Persistent namespaces are stored in a
database file and are reloaded when AppDaemon restarts. Defaults to ``True``
database file and are reloaded when AppDaemon restarts. Defaults to `True`.

Returns:
The file path to the newly created namespace. Will be ``None`` if not persistent
The file path to the newly created namespace. Will be ``None`` if not persistent.

Examples:
Add a new namespace called `storage`.
Create a namespace that buffers state changes in memory and periodically writes them to disk.

>>> self.add_namespace("on_disk", writeback="hybrid", persist=True)

>>> self.add_namespace("storage")
Create an in-memory namespace that won't survive AppDaemon restarts.

>>> self.add_namespace("in_memory", persist=False)
"""
new_namespace = await self.AD.state.add_namespace(namespace, writeback, persist, self.name)
match new_namespace:
case Path() | str():
new_namespace = str(new_namespace)
self.AD.state.app_added_namespaces.add(new_namespace)
return new_namespace
case _:
self.logger.warning("Namespace %s already exists or was not created", namespace)
match await self.AD.state.add_namespace(
namespace,
utils.ADWritebackType(writeback),
persist,
self.name
):
case Path() as ns_path:
return str(ns_path)
case False | None:
return None

@utils.sync_decorator
async def remove_namespace(self, namespace: str) -> dict[str, Any] | None:
"""Remove a user-defined namespace, which has a database file associated with it.

See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
See :py:ref:`app_namespaces` for more information.

Args:
namespace (str): The namespace to be removed, which must not be the current namespace.
Expand Down Expand Up @@ -709,32 +725,22 @@ async def list_namespaces(self) -> list[str]:
return self.AD.state.list_namespaces()

@utils.sync_decorator
async def save_namespace(self, namespace: str | None = None) -> None:
"""Saves entities created in user-defined namespaces into a file.
async def save_namespace(self, namespace: str | None = None) -> bool:
"""Saves the given state namespace to its corresponding file.

This way, when AD restarts these entities will be reloaded into AD with its
previous states within the namespace. This can be used as a basic form of
non-volatile storage of entity data. Depending on the configuration of the
namespace, this function can be setup to constantly be running automatically
or only when AD shutdown. This function also allows for users to manually
execute the command as when needed.
This is only relevant for persistent namespaces, which if not set to ``safe`` buffers changes in memory and only
periodically writes them to disk. This function manually forces a write of all the changes since the last save
to disk. See the :py:ref:`app_namespaces` docs section for more information.

Args:
namespace (str, optional): Namespace to use for the call. See the section on
`namespaces <APPGUIDE.html#namespaces>`__ for a detailed description.
In most cases it is safe to ignore this parameter.
namespace (str, optional): Namespace to save. If not specified, the current app namespace will be used.

Returns:
None.

Examples:
Save all entities of the default namespace.

>>> self.save_namespace()
bool: `True` if the namespace was saved successfully, `False` otherwise.

"""
namespace = namespace if namespace is not None else self.namespace
await self.AD.state.save_namespace(namespace)
return await self.AD.state.save_namespace(namespace)

#
# Utility
Expand Down
1 change: 1 addition & 0 deletions appdaemon/appdaemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ def start(self) -> None:
self.thread_async.start()
self.sched.start()
self.utility.start()
self.state.start()

if self.apps_enabled:
self.app_management.start()
Expand Down
15 changes: 10 additions & 5 deletions appdaemon/models/config/misc.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import json
from datetime import datetime
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, Field, model_validator

from appdaemon.utils import ADWritebackType

from .common import ParsedTimedelta

LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]


Expand All @@ -31,23 +35,24 @@ class FilterConfig(BaseModel):


class NamespaceConfig(BaseModel):
writeback: Literal["safe", "hybrid"] | None = None
writeback: ADWritebackType | None = None
persist: bool = Field(default=False, alias="persistent")
save_interval: ParsedTimedelta = Field(default=timedelta(seconds=1))

@model_validator(mode="before")
@classmethod
def validate_persistence(cls, values: Any):
"""Sets persistence to True if writeback is set to safe or hybrid."""
match values:
case {"writeback": wb} if wb is not None:
values["persistent"] = True
values["persist"] = True
case _ if getattr(values, "writeback", None) is not None:
values.persistent = True
values.persist = True
return values

@model_validator(mode="after")
def validate_writeback(self):
"""Makes the writeback safe by default if persist is set to True."""
if self.persist and self.writeback is None:
self.writeback = "safe"
self.writeback = ADWritebackType.safe
return self
Loading
Loading