Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: only process incoming websocket packet model type once #52

Merged
merged 7 commits into from
Jun 14, 2024
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
4 changes: 2 additions & 2 deletions src/uiprotect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,7 @@

async def get_devices_raw(self, model_type: ModelType) -> list[dict[str, Any]]:
"""Gets a raw device list given a model_type"""
return await self.api_request_list(f"{model_type.value}s")
return await self.api_request_list(model_type.devices_key)

async def get_devices(
self,
Expand Down Expand Up @@ -1703,7 +1703,7 @@

async def adopt_device(self, model_type: ModelType, device_id: str) -> None:
"""Adopts a device"""
key = f"{model_type.value}s"
key = model_type.devices_key

Check warning on line 1706 in src/uiprotect/api.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/api.py#L1706

Added line #L1706 was not covered by tests
data = await self.api_request_obj(
"devices/adopt",
method="post",
Expand Down
80 changes: 41 additions & 39 deletions src/uiprotect/data/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@
)
data["macLookup"] = {}
data["idLookup"] = {}
for model_type in ModelType.bootstrap_models():
key = f"{model_type}s"
for model_type in ModelType.bootstrap_models_types_set():
key = model_type.devices_key
items: dict[str, ProtectModel] = {}
for item in data[key]:
if (
Expand Down Expand Up @@ -234,8 +234,8 @@
if "idLookup" in data:
del data["idLookup"]

for model_type in ModelType.bootstrap_models():
attr = f"{model_type}s"
for model_type in ModelType.bootstrap_models_types_set():
attr = model_type.devices_key
if attr in data and isinstance(data[attr], dict):
data[attr] = list(data[attr].values())

Expand Down Expand Up @@ -302,15 +302,15 @@
if ref is None:
return None

devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
devices: dict[str, ProtectModelWithId] = getattr(self, ref.model.devices_key)
return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))

def get_device_from_id(self, device_id: str) -> ProtectAdoptableDeviceModel | None:
"""Retrieve a device from device ID (without knowing model type)."""
ref = self.id_lookup.get(device_id)
if ref is None:
return None
devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
devices: dict[str, ProtectModelWithId] = getattr(self, ref.model.devices_key)

Check warning on line 313 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L313

Added line #L313 was not covered by tests
return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))

def process_event(self, event: Event) -> None:
Expand Down Expand Up @@ -344,21 +344,24 @@

def _process_add_packet(
self,
model_type: ModelType,
packet: WSPacket,
data: dict[str, Any],
) -> WSSubscriptionMessage | None:
obj = create_from_unifi_dict(data, api=self._api)

if isinstance(obj, Event):
obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
if model_type is ModelType.EVENT:
if TYPE_CHECKING:
assert isinstance(obj, Event)
self.process_event(obj)
elif isinstance(obj, NVR):
if model_type is ModelType.NVR:
if TYPE_CHECKING:
assert isinstance(obj, NVR)
self.nvr = obj
elif (
isinstance(obj, ProtectAdoptableDeviceModel)
and obj.model is not None
and obj.model.value in ModelType.bootstrap_models_set()
):
key = f"{obj.model.value}s"
elif model_type in ModelType.bootstrap_models_types_set():
if TYPE_CHECKING:
assert isinstance(obj, ProtectAdoptableDeviceModel)
assert isinstance(obj.model, ModelType)
key = obj.model.devices_key

Check warning on line 364 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L364

Added line #L364 was not covered by tests
if not self._api.ignore_unadopted or (
obj.is_adopted and not obj.is_adopted_by_other
):
Expand All @@ -381,9 +384,12 @@
new_obj=obj,
)

def _process_remove_packet(self, packet: WSPacket) -> WSSubscriptionMessage | None:
model: str | None = packet.action_frame.data.get("modelKey")
devices: dict[str, ProtectDeviceModel] | None = getattr(self, f"{model}s", None)
def _process_remove_packet(
self, model_type: ModelType, packet: WSPacket
) -> WSSubscriptionMessage | None:
devices: dict[str, ProtectDeviceModel] | None = getattr(

Check warning on line 390 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L390

Added line #L390 was not covered by tests
self, model_type.devices_key, None
)

if devices is None:
return None
Expand Down Expand Up @@ -443,54 +449,52 @@

def _process_device_update(
self,
model_type: ModelType,
packet: WSPacket,
action: dict[str, Any],
data: dict[str, Any],
ignore_stats: bool,
) -> WSSubscriptionMessage | None:
model_type = action["modelKey"]
remove_keys = (
STATS_AND_IGNORE_DEVICE_KEYS if ignore_stats else IGNORE_DEVICE_KEYS
)
for key in remove_keys.intersection(data):
del data[key]
# `last_motion` from cameras update every 100 milliseconds when a motion event is active
# this overrides the behavior to only update `last_motion` when a new event starts
if model_type == "camera" and "lastMotion" in data:
if model_type is ModelType.CAMERA and "lastMotion" in data:
del data["lastMotion"]
# nothing left to process
if not data:
self._create_stat(packet, None, True)
return None

key = f"{model_type}s"
devices: dict[str, ProtectModelWithId] = getattr(self, key)
devices: dict[str, ProtectModelWithId] = getattr(self, model_type.devices_key)
action_id: str = action["id"]
if action_id not in devices:
# ignore updates to events that phase out
if model_type != _ModelType_Event_value:
if model_type is not ModelType.EVENT:
_LOGGER.debug("Unexpected %s: %s", key, action_id)
return None

obj = devices[action_id]
model = obj.model
data = obj.unifi_dict_to_dict(data)
old_obj = obj.copy()
obj = obj.update_from_dict(deepcopy(data))

if model is ModelType.EVENT:
if model_type is ModelType.EVENT:
if TYPE_CHECKING:
assert isinstance(obj, Event)
self.process_event(obj)
elif model is ModelType.CAMERA:
elif model_type is ModelType.CAMERA:
if TYPE_CHECKING:
assert isinstance(obj, Camera)
if "last_ring" in data and obj.last_ring:
is_recent = obj.last_ring + RECENT_EVENT_MAX >= utc_now()
_LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
if is_recent:
obj.set_ring_timeout()
elif model is ModelType.SENSOR:
elif model_type is ModelType.SENSOR:
if TYPE_CHECKING:
assert isinstance(obj, Sensor)
if "alarm_triggered_at" in data and obj.alarm_triggered_at:
Expand Down Expand Up @@ -532,30 +536,28 @@
self._create_stat(packet, None, True)
return None

if models and ModelType(model_key) not in models:
model_type = ModelType.from_string(model_key)
if models and model_type not in models:
self._create_stat(packet, None, True)
return None

action_action: str = action["action"]
if action_action == "remove":
return self._process_remove_packet(packet)
return self._process_remove_packet(model_type, packet)

Check warning on line 546 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L546

Added line #L546 was not covered by tests

if not data:
self._create_stat(packet, None, True)
return None

try:
if action_action == "add":
return self._process_add_packet(packet, data)
return self._process_add_packet(model_type, packet, data)
if action_action == "update":
if model_key == _ModelType_NVR_value:
if model_type is ModelType.NVR:
return self._process_nvr_update(packet, data, ignore_stats)

if (
model_key in ModelType.bootstrap_models_set()
or model_key == _ModelType_Event_value
):
if model_type in ModelType.bootstrap_models_types_and_event_set():
return self._process_device_update(
model_type,
packet,
action,
data,
Expand All @@ -576,7 +578,7 @@
msg = f"Validation error processing event: {action['id']}. Ignoring event."
else:
try:
model_type = ModelType(action["modelKey"])
model_type = ModelType.from_string(action["modelKey"])

Check warning on line 581 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L581

Added line #L581 was not covered by tests
device_id: str = action["id"]
task = asyncio.create_task(self.refresh_device(model_type, device_id))
self._refresh_tasks.add(task)
Expand Down Expand Up @@ -609,7 +611,7 @@
self.nvr = device
else:
devices: dict[str, ProtectModelWithId] = getattr(
self, f"{model_type.value}s"
self, model_type.devices_key
)
devices[device.id] = device
_LOGGER.debug("Successfully refresh model: %s %s", model_type, device_id)
Expand Down
4 changes: 4 additions & 0 deletions src/uiprotect/data/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def create_from_unifi_dict(
data: dict[str, Any],
api: ProtectApiClient | None = None,
klass: type[ProtectModel] | None = None,
model_type: ModelType | None = None,
) -> ProtectModel:
"""
Helper method to read the `modelKey` from a UFP JSON dict and convert to currect Python class.
Expand All @@ -71,6 +72,9 @@ def create_from_unifi_dict(
if "modelKey" not in data:
raise DataDecodeError("No modelKey")

if model_type is not None and klass is None:
klass = MODEL_TO_CLASS.get(model_type)

if klass is None:
klass = get_klass_from_dict(data)

Expand Down
57 changes: 44 additions & 13 deletions src/uiprotect/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import enum
from collections.abc import Callable, Coroutine
from functools import cache
from functools import cache, cached_property
from typing import Any, Literal, Optional, TypeVar, Union

from packaging.version import Version as BaseVersion
Expand Down Expand Up @@ -109,31 +109,62 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
RECORDING_SCHEDULE = "recordingSchedule"
UNKNOWN = "unknown"

@cached_property
def devices_key(self) -> str:
"""Return the devices key."""
return f"{self.value}s"

@classmethod
@cache
def from_string(cls, value: str) -> ModelType:
return cls(value)

@staticmethod
@cache
def bootstrap_models() -> tuple[str, ...]:
def bootstrap_model_types() -> tuple[ModelType, ...]:
"""Return the bootstrap models as a tuple."""
# TODO:
# legacyUFV
# display

return (
ModelType.CAMERA.value,
ModelType.USER.value,
ModelType.GROUP.value,
ModelType.LIVEVIEW.value,
ModelType.VIEWPORT.value,
ModelType.LIGHT.value,
ModelType.BRIDGE.value,
ModelType.SENSOR.value,
ModelType.DOORLOCK.value,
ModelType.CHIME.value,
ModelType.CAMERA,
ModelType.USER,
ModelType.GROUP,
ModelType.LIVEVIEW,
ModelType.VIEWPORT,
ModelType.LIGHT,
ModelType.BRIDGE,
ModelType.SENSOR,
ModelType.DOORLOCK,
ModelType.CHIME,
)

@staticmethod
@cache
def bootstrap_models() -> tuple[str, ...]:
"""Return the bootstrap models strings as a tuple."""
return tuple(
model_type.value for model_type in ModelType.bootstrap_model_types()
bdraco marked this conversation as resolved.
Show resolved Hide resolved
)

@staticmethod
@cache
def bootstrap_models_set() -> set[str]:
"""Return the set of bootstrap models strings as a set."""
return set(ModelType.bootstrap_models())

@staticmethod
@cache
def bootstrap_models_types_set() -> set[ModelType]:
"""Return the set of bootstrap models as a set."""
return set(ModelType.bootstrap_model_types())

@staticmethod
@cache
def bootstrap_models_types_and_event_set() -> set[ModelType]:
"""Return the set of bootstrap models and the event model as a set."""
return ModelType.bootstrap_models_types_set() | {ModelType.EVENT}


@enum.unique
class EventType(str, ValuesEnumMixin, enum.Enum):
Expand Down
2 changes: 1 addition & 1 deletion src/uiprotect/data/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
if self.obj_ids == {"self"} or self.obj_ids is None:
return None

devices = getattr(self._api.bootstrap, f"{self.model.value}s")
devices = getattr(self._api.bootstrap, self.model.devices_key)

Check warning on line 57 in src/uiprotect/data/user.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/user.py#L57

Added line #L57 was not covered by tests
bdraco marked this conversation as resolved.
Show resolved Hide resolved
return [devices[oid] for oid in self.obj_ids]


Expand Down
Loading