diff --git a/CHANGELOG.md b/CHANGELOG.md index 634c2f88..8a2bbfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v1.2.2 (2024-06-13) + +### Fix + + +- Restore some unreachable code in _process_device_update (#50) ([`c638cd3`](https://github.com/uilibs/uiprotect/commit/c638cd3b087d63279bd8f798bd8831fc2e11a916)) + + +## v1.2.1 (2024-06-13) + +### Fix + + +- Blocking i/o in the event loop (#49) ([`36a4355`](https://github.com/uilibs/uiprotect/commit/36a4355170566b9d7cfb1632d9c35c28b693d9ce)) + + ## v1.2.0 (2024-06-13) ### Feature diff --git a/docs/conf.py b/docs/conf.py index 8cced782..2850b14a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,7 @@ project = "uiprotect" copyright = "2024, UI Protect Maintainers" author = "UI Protect Maintainers" -release = "1.2.0" +release = "1.2.2" # General configuration extensions = [ diff --git a/pyproject.toml b/pyproject.toml index 172b59f0..5aa8cfda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uiprotect" -version = "1.2.0" +version = "1.2.2" description = "Python API for Unifi Protect (Unofficial)" authors = ["UI Protect Maintainers "] license = "MIT" diff --git a/src/uiprotect/data/bootstrap.py b/src/uiprotect/data/bootstrap.py index a2322f71..898d4f0f 100644 --- a/src/uiprotect/data/bootstrap.py +++ b/src/uiprotect/data/bootstrap.py @@ -80,12 +80,9 @@ } -def _process_light_event(event: Event) -> None: - if event.light is None: - return - - if _event_is_in_range(event, event.light.last_motion): - event.light.last_motion_event_id = event.id +def _process_light_event(event: Event, light: Light) -> None: + if _event_is_in_range(event, light.last_motion): + light.last_motion_event_id = event.id def _event_is_in_range(event: Event, dt: datetime | None) -> bool: @@ -95,22 +92,20 @@ def _event_is_in_range(event: Event, dt: datetime | None) -> bool: ) -def _process_sensor_event(event: Event) -> None: - if event.sensor is None: - return +def _process_sensor_event(event: Event, sensor: Sensor) -> None: if event.type is EventType.MOTION_SENSOR: - if _event_is_in_range(event, event.sensor.motion_detected_at): - event.sensor.last_motion_event_id = event.id + if _event_is_in_range(event, sensor.motion_detected_at): + sensor.last_motion_event_id = event.id elif event.type in {EventType.SENSOR_CLOSED, EventType.SENSOR_OPENED}: - if _event_is_in_range(event, event.sensor.open_status_changed_at): - event.sensor.last_contact_event_id = event.id + if _event_is_in_range(event, sensor.open_status_changed_at): + sensor.last_contact_event_id = event.id elif event.type is EventType.SENSOR_EXTREME_VALUE: - if _event_is_in_range(event, event.sensor.extreme_value_detected_at): - event.sensor.extreme_value_detected_at = event.end - event.sensor.last_value_event_id = event.id + if _event_is_in_range(event, sensor.extreme_value_detected_at): + sensor.extreme_value_detected_at = event.end + sensor.last_value_event_id = event.id elif event.type is EventType.SENSOR_ALARM: - if _event_is_in_range(event, event.sensor.alarm_triggered_at): - event.sensor.last_value_event_id = event.id + if _event_is_in_range(event, sensor.alarm_triggered_at): + sensor.last_value_event_id = event.id _CAMERA_SMART_AND_LINE_EVENTS = { @@ -120,10 +115,7 @@ def _process_sensor_event(event: Event) -> None: _CAMERA_SMART_AUDIO_EVENT = EventType.SMART_AUDIO_DETECT -def _process_camera_event(event: Event) -> None: - if (camera := event.camera) is None: - return - +def _process_camera_event(event: Event, camera: Camera) -> None: event_type = event.type dt_attr, event_attr = CAMERA_EVENT_ATTR_MAP[event_type] dt: datetime | None = getattr(camera, dt_attr) @@ -322,12 +314,13 @@ def get_device_from_id(self, device_id: str) -> ProtectAdoptableDeviceModel | No return cast(ProtectAdoptableDeviceModel, devices.get(ref.id)) def process_event(self, event: Event) -> None: - if event.type in CAMERA_EVENT_ATTR_MAP and event.camera is not None: - _process_camera_event(event) - elif event.type == EventType.MOTION_LIGHT and event.light is not None: - _process_light_event(event) - elif event.type == EventType.MOTION_SENSOR and event.sensor is not None: - _process_sensor_event(event) + event_type = event.type + if event_type in CAMERA_EVENT_ATTR_MAP and (camera := event.camera): + _process_camera_event(event, camera) + elif event_type is EventType.MOTION_LIGHT and (light := event.light): + _process_light_event(event, light) + elif event_type is EventType.MOTION_SENSOR and (sensor := event.sensor): + _process_sensor_event(event, sensor) self.events[event.id] = event @@ -473,47 +466,48 @@ def _process_device_update( key = f"{model_type}s" devices: dict[str, ProtectModelWithId] = getattr(self, key) action_id: str = action["id"] - if action_id in devices: - if action_id not in devices: - raise ValueError(f"Unknown device update for {model_type}: {action_id}") - obj = devices[action_id] - data = obj.unifi_dict_to_dict(data) - old_obj = obj.copy() - obj = obj.update_from_dict(deepcopy(data)) - - if isinstance(obj, Event): - self.process_event(obj) - elif 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 ( - isinstance(obj, Sensor) - and "alarm_triggered_at" in data - and obj.alarm_triggered_at - ): + if action_id not in devices: + # ignore updates to events that phase out + if model_type != _ModelType_Event_value: + _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 TYPE_CHECKING: + assert isinstance(obj, Event) + self.process_event(obj) + elif model 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: + if TYPE_CHECKING: + assert isinstance(obj, Sensor) + if "alarm_triggered_at" in data and obj.alarm_triggered_at: is_recent = obj.alarm_triggered_at + RECENT_EVENT_MAX >= utc_now() _LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id, is_recent) if is_recent: obj.set_alarm_timeout() - devices[action_id] = obj - - self._create_stat(packet, data, False) - return WSSubscriptionMessage( - action=WSAction.UPDATE, - new_update_id=self.last_update_id, - changed_data=data, - new_obj=obj, - old_obj=old_obj, - ) - - # ignore updates to events that phase out - if model_type != _ModelType_Event_value: - _LOGGER.debug("Unexpected %s: %s", key, action_id) - return None + devices[action_id] = obj + self._create_stat(packet, data, False) + return WSSubscriptionMessage( + action=WSAction.UPDATE, + new_update_id=self.last_update_id, + changed_data=data, + new_obj=obj, + old_obj=old_obj, + ) def process_ws_packet( self, diff --git a/src/uiprotect/data/nvr.py b/src/uiprotect/data/nvr.py index 7b4d4fe7..e8f6fc2f 100644 --- a/src/uiprotect/data/nvr.py +++ b/src/uiprotect/data/nvr.py @@ -1198,16 +1198,15 @@ async def reboot(self) -> None: async def _read_cache_file(self, file_path: Path) -> set[Version] | None: versions: set[Version] | None = None - - if file_path.is_file(): - try: - _LOGGER.debug("Reading release cache file: %s", file_path) - async with aiofiles.open(file_path, "rb") as cache_file: - versions = { - Version(v) for v in orjson.loads(await cache_file.read()) - } - except Exception: - _LOGGER.warning("Failed to parse cache file: %s", file_path) + try: + _LOGGER.debug("Reading release cache file: %s", file_path) + async with aiofiles.open(file_path, "rb") as cache_file: + versions = {Version(v) for v in orjson.loads(await cache_file.read())} + except FileNotFoundError: + # ignore missing file + pass + except Exception: + _LOGGER.warning("Failed to parse cache file: %s", file_path) return versions