Repository: AppDaemon/appdaemon

Issue:
Avoid copying states in get_state() if read-only access is needed only
Hi,

I think that deep-copying the whole HA state adds considerable overhead to ``get_state()``, especially when called for a whole domain or even complete HA state.

It turns out that in most cases, you just need read-only access to the states. I see the reason why they're deep-copied at the moment, and this should probably stay the default behaviour, but I think there should be a way to retrieve states without this overhead when you really know you won't modify it. I, for instance, do sometimes query the whole state to search for entities with specific characteristics and know that I won't ever write to it.

I'd suggest a new keyword argument for ``get_state()``, maybe named ``copy`` with a default of ``True``. I can make a PR for this, if desired. I could even greatly simplify the ``get_state()`` code simultaneously.

What do you think? I can even file the PR for you to have a look at it directly, because that's no big deal to implement.

Best regards
Robert



Discussion:

Resolving patch:
diff --git a/appdaemon/adapi.py b/appdaemon/adapi.py
--- a/appdaemon/adapi.py
+++ b/appdaemon/adapi.py
@@ -319,12 +319,14 @@ def info_listen_state(self, handle):
         self.logger.debug("Calling info_listen_state for %s",self.name)
         return utils.run_coroutine_threadsafe(self, self.AD.state.info_state_callback(handle, self.name))
 
-    def get_state(self, entity_id=None, attribute=None, **kwargs):
+    def get_state(self, entity_id=None, attribute=None, default=None, copy=True, **kwargs):
         namespace = self._get_namespace(**kwargs)
         if "namespace" in kwargs:
             del kwargs["namespace"]
 
-        return utils.run_coroutine_threadsafe(self, self.AD.state.get_state(self.name, namespace, entity_id, attribute, **kwargs))
+        return utils.run_coroutine_threadsafe(self, self.AD.state.get_state(
+            self.name, namespace, entity_id, attribute, default, copy, **kwargs
+        ))
 
     def set_state(self, entity_id, **kwargs):
         self.logger.debug("set state: %s, %s", entity_id, kwargs)
diff --git a/appdaemon/state.py b/appdaemon/state.py
--- a/appdaemon/state.py
+++ b/appdaemon/state.py
@@ -276,56 +276,44 @@ async def add_entity(self, namespace, entity, state, attributes = None):
 
         await self.AD.events.process_event(namespace, data)
 
-    async def get_state(self, name, namespace, entity_id=None, attribute=None):
-        self.logger.debug("get_state: %s.%s", entity_id, attribute)
-        device = None
-        entity = None
+    async def get_state(
+            self, name, namespace, entity_id=None, attribute=None,
+            default=None, copy=True
+    ):
+        self.logger.debug("get_state: %s.%s %s %s",
+                          entity_id, attribute, default, copy)
+
+        maybe_copy = lambda data: deepcopy(data) if copy else data
+
         if entity_id is not None and "." in entity_id:
             if not await self.entity_exists(namespace, entity_id):
-                return None
-        if entity_id is not None:
-            if "." not in entity_id:
-                if attribute is not None:
-                    raise ValueError(
-                        "{}: Invalid entity ID: {}".format(name, entity))
-                device = entity_id
-                entity = None
-            else:
-                device, entity = entity_id.split(".")
-
-        if device is None:
-            return deepcopy(dict(self.state[namespace]))
-        elif entity is None:
-            devices = {}
-            for entity_id in self.state[namespace].keys():
-                thisdevice, thisentity = entity_id.split(".")
-                if device == thisdevice:
-                    devices[entity_id] = self.state[namespace][entity_id]
-            return deepcopy(devices)
-        elif attribute is None:
-            entity_id = "{}.{}".format(device, entity)
-            if entity_id in self.state[namespace] and "state" in self.state[namespace][entity_id]:
-                return deepcopy(self.state[namespace][entity_id]["state"])
-            else:
-                return None
-        else:
-            entity_id = "{}.{}".format(device, entity)
+                return default
+            state = self.state[namespace][entity_id]
+            if attribute is None:
+                return maybe_copy(state["state"])
             if attribute == "all":
-                if entity_id in self.state[namespace]:
-                    return deepcopy(self.state[namespace][entity_id])
-                else:
-                    return None
-            else:
-                if namespace in self.state and entity_id in self.state[namespace]:
-                    if attribute in self.state[namespace][entity_id]["attributes"]:
-                        return deepcopy(self.state[namespace][entity_id]["attributes"][
-                                            attribute])
-                    elif attribute in self.state[namespace][entity_id]:
-                        return deepcopy(self.state[namespace][entity_id][attribute])
-                    else:
-                        return None
-                else:
-                    return None
+                return maybe_copy(state)
+            if attribute in state["attributes"]:
+                return maybe_copy(state["attributes"][attribute])
+            if attribute in state:
+                return maybe_copy(state[attribute])
+            return default
+
+        if attribute is not None:
+            raise ValueError(
+                "{}: Querying a specific attribute is only possible for a single entity"
+                .format(name)
+            )
+
+        if entity_id is None:
+            return maybe_copy(self.state[namespace])
+
+        domain = entity_id.split(".", 1)[0]
+        return {
+            entity_id: maybe_copy(state)
+            for entity_id, state in self.state[namespace].items()
+            if entity_id.split(".", 1)[0] == domain
+        }
 
     def parse_state(self, entity_id, namespace, **kwargs):
         self.logger.debug("parse_state: %s, %s", entity_id, kwargs)


Test patch: