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
6 changes: 3 additions & 3 deletions qtoggleserver/core/api/funcs/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ async def put_ports(request: core_api.APIRequest, params: GenericJSONList) -> No

try:
# Remove all (local) virtual ports
for port in core_ports.get_all():
for port in list(core_ports.get_all()):
if not isinstance(port, core_vports.VirtualPort):
continue

Expand All @@ -174,10 +174,10 @@ async def put_ports(request: core_api.APIRequest, params: GenericJSONList) -> No
await core_ports.reset()
if settings.slaves.enabled:
await slaves.reset_ports()
for port in core_ports.get_all():
for port in list(core_ports.get_all()):
await port.reset()

add_port_schema: GenericJSONDict = dict(core_api_schema.POST_PORTS)
add_port_schema: GenericJSONDict = core_api_schema.POST_PORTS.copy()
add_port_schema["additionalProperties"] = True

# Restore supplied attributes
Expand Down
8 changes: 7 additions & 1 deletion qtoggleserver/core/api/funcs/various.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import inspect
import traceback

from asyncio import CancelledError

from qtoggleserver import slaves, system
from qtoggleserver.conf import settings
from qtoggleserver.core import api as core_api
Expand Down Expand Up @@ -41,7 +43,11 @@ async def get_listen(request: core_api.APIRequest) -> GenericJSONList:
timeout = 60 # default

session = core_sessions.get(session_id)
events = await session.reset_and_wait(timeout, request.access_level)
try:
events = await session.reset_and_wait(timeout, request.access_level)
except CancelledError:
session.debug("waiting cancelled")
return []

return [await e.to_json() for e in events]

Expand Down
92 changes: 60 additions & 32 deletions qtoggleserver/core/device/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@
viewonly_password_hash: str | None = None

_schema: GenericJSONDict | None = None
_attrdefs: AttributeDefinitions | None = None
_attrdefs_cache: AttributeDefinitions | None = None
_to_json_attrdefs_cache: AttributeDefinitions | None = None
_attrs_watch_task: asyncio.Task | None = None
_attrs_cache: Attributes | None = None


class AttrDefDriver(metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -588,23 +590,23 @@ def attr_set_password(which: str, value: str) -> None:


def load_dynamic_attrdef(name: str, params: dict[str, Any]) -> AttributeDefinition:
params = dict(params)
params = params.copy()
class_path = params.pop("driver")

logger.debug('creating device attribute "%s" with driver "%s"', name, class_path)
try:
peripheral_class = dynload_utils.load_attr(class_path)
attrdef_driver_class = dynload_utils.load_attr(class_path)
except Exception:
raise NoSuchDriver(class_path)

return peripheral_class(**params).to_attrdef()
return attrdef_driver_class(**params).to_attrdef()


def load_dynamic_attrdefs() -> AttributeDefinitions:
attrdefs = {}

for params in settings.core.device_attrs:
params = dict(params)
params = params.copy()
name = params.pop("name")

try:
Expand All @@ -619,22 +621,22 @@ def load_dynamic_attrdefs() -> AttributeDefinitions:


def get_attrdefs() -> AttributeDefinitions:
global _attrdefs
global _attrdefs_cache

if _attrdefs is None:
if _attrdefs_cache is None:
logger.debug("initializing attribute definitions")
_attrdefs = copy.deepcopy(ATTRDEFS) | load_dynamic_attrdefs()
_attrdefs_cache = copy.deepcopy(ATTRDEFS) | load_dynamic_attrdefs()

# Transform some callable values into corresponding results
for n, attrdef in list(_attrdefs.items()):
for n, attrdef in list(_attrdefs_cache.items()):
for k, v in attrdef.items():
if callable(v) and k in ATTRDEF_CALLABLE_FIELDS:
attrdef[k] = v()

if attrdef.pop("enabled", True) is False:
_attrdefs.pop(n)
_attrdefs_cache.pop(n)

return _attrdefs
return _attrdefs_cache


def get_schema(loose: bool = False) -> GenericJSONDict:
Expand All @@ -650,7 +652,7 @@ def get_schema(loose: bool = False) -> GenericJSONDict:
if not attrdef.get("modifiable"):
continue

attr_schema = dict(attrdef)
attr_schema = attrdef.copy()
if attr_schema["type"] == "string":
if "min" in attr_schema:
attr_schema["minLength"] = attr_schema.pop("min")
Expand Down Expand Up @@ -682,6 +684,11 @@ def get_schema(loose: bool = False) -> GenericJSONDict:


async def get_attrs() -> Attributes:
global _attrs_cache

if _attrs_cache is not None:
return _attrs_cache.copy()

attrdefs = get_attrdefs()

# Do a first round to gather all required calls and ensure we only call each function once, caching its result
Expand All @@ -699,7 +706,7 @@ async def get_attrs() -> Attributes:
call_results[call] = result

# Do a second round to prepare attribute values
attrs = {}
_attrs_cache = {}
for n, attrdef in attrdefs.items():
getter = attrdef["getter"]
if not getter:
Expand All @@ -721,13 +728,14 @@ async def get_attrs() -> Attributes:
else:
continue

attrs[n] = value
_attrs_cache[n] = value

return attrs
return _attrs_cache.copy()


async def set_attrs(attrs: Attributes, ignore_extra: bool = False) -> bool:
core_device_attrs = sys.modules[__name__]
invalidate_attrs()

reboot_required = False
attrdefs = get_attrdefs()
Expand Down Expand Up @@ -817,30 +825,49 @@ async def set_attrs(attrs: Attributes, ignore_extra: bool = False) -> bool:
return reboot_required


def invalidate_attrs() -> None:
global _attrs_cache

_attrs_cache = None


def invalidate_attrdefs() -> None:
global _to_json_attrdefs_cache
global _attrdefs_cache

_to_json_attrdefs_cache = None
_attrdefs_cache = None


async def to_json() -> GenericJSONDict:
attrdefs: AttributeDefinitions = copy.deepcopy(get_attrdefs())
filtered_attrdefs: AttributeDefinitions = {}
for attr_name, attrdef in attrdefs.items():
if attrdef.pop("standard", False):
continue
global _to_json_attrdefs_cache

# Remove unwanted fields from attribute definition
for field in ("persisted", "setter", "getter"):
attrdef.pop(field, None)
if _to_json_attrdefs_cache is None:
attrdefs: AttributeDefinitions = copy.deepcopy(get_attrdefs())
filtered_attrdefs: AttributeDefinitions = {}
for attr_name, attrdef in attrdefs.items():
if attrdef.pop("standard", False):
continue

# Remove optional boolean fields that are false
for field in ("integer", "reconnect"):
if not attrdef.get(field):
# Remove unwanted fields from attribute definition
for field in ("persisted", "setter", "getter"):
attrdef.pop(field, None)

for key in list(attrdef):
if key.startswith("_"):
attrdef.pop(key)
# Remove optional boolean fields that are false
for field in ("integer", "reconnect"):
if not attrdef.get(field):
attrdef.pop(field, None)

for key in list(attrdef):
if key.startswith("_"):
attrdef.pop(key)

filtered_attrdefs[attr_name] = attrdef

filtered_attrdefs[attr_name] = attrdef
_to_json_attrdefs_cache = filtered_attrdefs

result: dict[str, Any] = dict(await get_attrs())
result["definitions"] = filtered_attrdefs
result: dict[str, Any] = await get_attrs()
result["definitions"] = _to_json_attrdefs_cache

return result

Expand Down Expand Up @@ -881,6 +908,7 @@ async def _attrs_watch_loop() -> None:
logger.error("network attributes data check failed: %s", e, exc_info=True)

if changed:
invalidate_attrs()
await device_events.trigger_update()

await asyncio.sleep(NETWORK_ATTRS_WATCH_INTERVAL)
Expand Down
2 changes: 1 addition & 1 deletion qtoggleserver/core/expressions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def parse(self_port_id: str | None, sexpression: str, role: Role, pos: int) -> E


class EvalContext:
def __init__(self, port_values: dict[str, NullablePortValue], now_ms: int) -> None:
def __init__(self, port_values: dict[str, NullablePortValue], now_ms: int = 0) -> None:
self.port_values: dict[str, NullablePortValue] = port_values
self.now_ms: int = now_ms

Comment thread
ccrisan marked this conversation as resolved.
Expand Down
4 changes: 2 additions & 2 deletions qtoggleserver/core/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async def sampling_task() -> None:
continue # don't record history unless we've got real date/time

now_ms = int(time.time() * 1000)
for port in core_ports.get_all():
for port in list(core_ports.get_all()):
if not port.is_enabled():
continue

Expand Down Expand Up @@ -93,7 +93,7 @@ async def janitor_task() -> None:
continue

now = int(time.time())
for port in core_ports.get_all():
for port in list(core_ports.get_all()):
history_retention = await port.get_history_retention()
if history_retention <= 0:
continue
Expand Down
12 changes: 7 additions & 5 deletions qtoggleserver/core/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@


async def update() -> None:
from . import ports, sessions
from . import sessions

global _last_time
global _last_minute
Expand Down Expand Up @@ -102,7 +102,8 @@ async def update() -> None:
_last_year = now_dt.year
changed_set.add(DEP_YEAR)

for port in ports.get_all():
all_ports = list(core_ports.get_all())
for port in all_ports:
if not port.is_enabled():
continue

Expand Down Expand Up @@ -139,7 +140,7 @@ async def update() -> None:
changed_set.add(port)
value_pairs[port] = old_value, new_value

await handle_value_changes(changed_set, value_pairs, now_ms)
await handle_value_changes(all_ports, changed_set, value_pairs, now_ms)

sessions.update()

Expand All @@ -159,6 +160,7 @@ async def update_loop() -> None:


async def handle_value_changes(
all_ports: list[core_ports.BasePort],
changed_set: set[core_ports.BasePort | str],
value_pairs: dict[core_ports.BasePort, tuple[NullablePortValue, NullablePortValue]],
now_ms: int,
Expand Down Expand Up @@ -199,8 +201,8 @@ async def handle_value_changes(
if await port.is_persisted():
port.save_asap()

# Reevaluate the expressions depending on changed ports
for port in core_ports.get_all():
# Reevaluate all port expressions depending on changed ports
for port in all_ports:
if not port.is_enabled():
continue

Expand Down
Loading
Loading