diff --git a/custom_components/dirigera_platform/dirigera_lib_patch.py b/custom_components/dirigera_platform/dirigera_lib_patch.py index f849c96..2a514cd 100644 --- a/custom_components/dirigera_platform/dirigera_lib_patch.py +++ b/custom_components/dirigera_platform/dirigera_lib_patch.py @@ -80,6 +80,26 @@ def delete_empty_scenes(self): if scene.name.startswith("dirigera_integration_empty_scene_"): logging.debug(f"Deleting Scene id: {scene.id} name: {scene.name}...") self.delete_scene(scene.id) + + def get_motion_sensors(self) -> List[MotionSensorX]: + """ + Fetches all motion sensors registered in the Hub. + Includes both motionSensor and occupancySensor device types. + IKEA MYGGSPRAY sensors report as occupancySensor instead of motionSensor. + """ + devices = self.get("/devices") + sensors = list(filter(lambda x: x["deviceType"] in ("motionSensor", "occupancySensor"), devices)) + return [dict_to_motion_sensor_x(sensor, self) for sensor in sensors] + + def get_motion_sensor_by_id(self, id_: str) -> MotionSensorX: + """ + Fetches a motion sensor by ID. + Accepts both motionSensor and occupancySensor device types. + """ + motion_sensor = self._get_device_data_by_id(id_) + if motion_sensor["deviceType"] not in ("motionSensor", "occupancySensor"): + raise ValueError("Device is not a MotionSensor or OccupancySensor") + return dict_to_motion_sensor_x(motion_sensor, self) class ControllerAttributesX(Attributes): is_on: Optional[bool] = None @@ -136,4 +156,35 @@ def trigger(self) -> HackScene: self.hub.post(route=f"/scenes/{self.id}/trigger") def undo(self) -> HackScene: - self.hub.post(route=f"/scenes/{self.id}/undo") \ No newline at end of file + self.hub.post(route=f"/scenes/{self.id}/undo") + + +# Motion sensor patch for MYGGSPRAY (occupancySensor) +# MYGGSPRAY sensors don't have is_on attribute, so we make it optional +class MotionSensorAttributesX(Attributes): + battery_percentage: Optional[int] = None + is_on: Optional[bool] = None # Made optional for MYGGSPRAY compatibility + light_level: Optional[float] = None + is_detected: Optional[bool] = False + + +class MotionSensorX(Device): + dirigera_client: AbstractSmartHomeHub + attributes: MotionSensorAttributesX + + def reload(self) -> "MotionSensorX": + data = self.dirigera_client.get(route=f"/devices/{self.id}") + return MotionSensorX(dirigeraClient=self.dirigera_client, **data) + + def set_name(self, name: str) -> None: + if "customName" not in self.capabilities.can_receive: + raise AssertionError("This sensor does not support the set_name function") + data = [{"attributes": {"customName": name}}] + self.dirigera_client.patch(route=f"/devices/{self.id}", data=data) + self.attributes.custom_name = name + + +def dict_to_motion_sensor_x( + data: Dict[str, Any], dirigera_client: AbstractSmartHomeHub +) -> MotionSensorX: + return MotionSensorX(dirigeraClient=dirigera_client, **data) \ No newline at end of file diff --git a/custom_components/dirigera_platform/hub_event_listener.py b/custom_components/dirigera_platform/hub_event_listener.py index e5ef40a..be82389 100644 --- a/custom_components/dirigera_platform/hub_event_listener.py +++ b/custom_components/dirigera_platform/hub_event_listener.py @@ -19,6 +19,7 @@ process_events_from = { "motionSensor" : ["isDetected","isOn","batteryPercentage"], + "occupancySensor" : ["isDetected","isOn","batteryPercentage"], "outlet" : [ "isOn", "currentAmps", "currentActivePower",