From 541e2329350f1f73a11f2200488abb293f59fde7 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 25 Sep 2023 15:12:17 +0200 Subject: [PATCH] Update models for updated schema and new resource types (#219) --- aiohue/v2/controllers/config.py | 8 ++- aiohue/v2/controllers/sensors.py | 48 ++++++++++++++++++ aiohue/v2/models/button.py | 30 ++++++++++- aiohue/v2/models/camera_motion.py | 45 +++++++++++++++++ aiohue/v2/models/contact.py | 60 ++++++++++++++++++++++ aiohue/v2/models/feature.py | 77 ++++++++++++++++++++++++++++- aiohue/v2/models/light_level.py | 26 +++++++++- aiohue/v2/models/motion.py | 19 +++---- aiohue/v2/models/relative_rotary.py | 23 ++++++++- aiohue/v2/models/resource.py | 3 ++ aiohue/v2/models/tamper.py | 59 ++++++++++++++++++++++ aiohue/v2/models/temperature.py | 30 +++++++++-- 12 files changed, 403 insertions(+), 25 deletions(-) create mode 100644 aiohue/v2/models/camera_motion.py create mode 100644 aiohue/v2/models/contact.py create mode 100644 aiohue/v2/models/tamper.py diff --git a/aiohue/v2/controllers/config.py b/aiohue/v2/controllers/config.py index 898bcad4..a86a33ec 100644 --- a/aiohue/v2/controllers/config.py +++ b/aiohue/v2/controllers/config.py @@ -15,7 +15,7 @@ from ..models.matter_fabric import MatterFabric from ..models.resource import ResourceTypes from ..models.behavior_script import BehaviorScript -from ..models.behavior_instance import BehaviorInstance +from ..models.behavior_instance import BehaviorInstance, BehaviorInstancePut from .base import BaseResourcesController, GroupedControllerBase if TYPE_CHECKING: @@ -100,6 +100,10 @@ class BehaviorInstanceController(BaseResourcesController[Type[BehaviorInstance]] item_cls = BehaviorInstance allow_parser_error = True + async def set_enabled(self, id: str, enabled: bool) -> None: + """Enable/Disable sensor.""" + await self.update(id, BehaviorInstancePut(enabled=enabled)) + class ConfigController( GroupedControllerBase[ @@ -183,6 +187,7 @@ def __init__(self, bridge: "HueBridgeV2") -> None: self.matter = MatterController(bridge) self.matter_fabric = MatterFabricController(bridge) self.behavior_script = BehaviorScriptController(bridge) + self.behavior_instance = BehaviorInstanceController(bridge) super().__init__( bridge, [ @@ -194,5 +199,6 @@ def __init__(self, bridge: "HueBridgeV2") -> None: self.matter, self.matter_fabric, self.behavior_script, + self.behavior_instance, ], ) diff --git a/aiohue/v2/controllers/sensors.py b/aiohue/v2/controllers/sensors.py index fec05c55..9453fd1f 100644 --- a/aiohue/v2/controllers/sensors.py +++ b/aiohue/v2/controllers/sensors.py @@ -5,12 +5,15 @@ from aiohue.util import dataclass_to_dict from ..models.button import Button, ButtonEvent +from ..models.camera_motion import CameraMotion, CameraMotionPut +from ..models.contact import Contact, ContactPut from ..models.relative_rotary import RelativeRotary from ..models.device_power import DevicePower from ..models.geofence_client import GeofenceClient from ..models.light_level import LightLevel, LightLevelPut from ..models.motion import Motion, MotionPut from ..models.resource import ResourceTypes +from ..models.tamper import Tamper from ..models.temperature import Temperature from ..models.zigbee_connectivity import ZigbeeConnectivity from .base import BaseResourcesController, GroupedControllerBase @@ -22,10 +25,13 @@ SENSOR_TYPES = Union[ DevicePower, Button, + CameraMotion, + Contact, GeofenceClient, LightLevel, Motion, RelativeRotary, + Tamper, Temperature, ZigbeeConnectivity, ] @@ -115,6 +121,34 @@ async def _handle_longpress_workaround(self, id: int): self._logger.debug("Long press workaround for FOH switch completed.") +class CameraMotionController(BaseResourcesController[Type[CameraMotion]]): + """Controller holding and managing HUE resources of type `camera_motion`.""" + + item_type = ResourceTypes.CAMERA_MOTION + item_cls = CameraMotion + allow_parser_error = True + + async def set_enabled(self, id: str, enabled: bool) -> None: + """Enable/Disable sensor.""" + await self.update(id, MotionPut(enabled=enabled)) + + async def set_sensitivity(self, id: str, sensitivity: int) -> None: + """Enable/Disable sensor.""" + await self.update(id, CameraMotionPut(sensitivity=sensitivity)) + + +class ContactController(BaseResourcesController[Type[Contact]]): + """Controller holding and managing HUE resources of type `contact`.""" + + item_type = ResourceTypes.CONTACT + item_cls = Contact + allow_parser_error = True + + async def set_enabled(self, id: str, enabled: bool) -> None: + """Enable/Disable sensor.""" + await self.update(id, ContactPut(enabled=enabled)) + + class GeofenceClientController(BaseResourcesController[Type[GeofenceClient]]): """Controller holding and managing HUE resources of type `geofence_client`.""" @@ -155,6 +189,14 @@ class RelativeRotaryController(BaseResourcesController[Type[Button]]): allow_parser_error = True +class TamperController(BaseResourcesController[Type[Tamper]]): + """Controller holding and managing HUE resources of type `tamper`.""" + + item_type = ResourceTypes.TAMPER + item_cls = Tamper + allow_parser_error = True + + class TemperatureController(BaseResourcesController[Type[Temperature]]): """Controller holding and managing HUE resources of type `temperature`.""" @@ -177,22 +219,28 @@ class SensorsController(GroupedControllerBase[SENSOR_TYPES]): def __init__(self, bridge: "HueBridgeV2") -> None: """Initialize instance.""" self.button = ButtonController(bridge) + self.camera_motion = CameraMotionController(bridge) + self.contact = ContactController(bridge) self.device_power = DevicePowerController(bridge) self.geofence_client = GeofenceClientController(bridge) self.light_level = LightLevelController(bridge) self.motion = MotionController(bridge) self.relative_rotary = RelativeRotaryController(bridge) + self.tamper = TamperController(bridge) self.temperature = TemperatureController(bridge) self.zigbee_connectivity = ZigbeeConnectivityController(bridge) super().__init__( bridge, [ self.button, + self.camera_motion, + self.contact, self.device_power, self.geofence_client, self.light_level, self.motion, self.relative_rotary, + self.tamper, self.temperature, self.zigbee_connectivity, ], diff --git a/aiohue/v2/models/button.py b/aiohue/v2/models/button.py index cf6e70bc..b87e83b1 100644 --- a/aiohue/v2/models/button.py +++ b/aiohue/v2/models/button.py @@ -4,8 +4,9 @@ https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_button """ from dataclasses import dataclass +from datetime import datetime from enum import Enum -from typing import Optional, Type +from typing import List, Optional, Type from .resource import ResourceIdentifier, ResourceTypes @@ -27,11 +28,36 @@ def _missing_(cls: Type, value: object): return ButtonEvent.UNKNOWN +@dataclass +class ButtonReport: + """ + Represent ButtonReport as retrieved from api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_button_get + """ + + updated: datetime + event: ButtonEvent + + @dataclass class ButtonFeature: """Represent ButtonFeature object as used by the Hue api.""" - last_event: ButtonEvent + button_report: Optional[ButtonReport] = None + last_event: Optional[ButtonEvent] = None # deprecated + repeat_interval: Optional[int] = None + event_values: Optional[List[ButtonEvent]] = None + + @property + def value(self) -> ButtonEvent: + """Return the actual/current value.""" + # prefer new style attribute (not available on older firmware versions) + if self.button_report is not None: + return self.button_report.event + if self.last_event is not None: + return self.last_event + return ButtonEvent.UNKNOWN @dataclass diff --git a/aiohue/v2/models/camera_motion.py b/aiohue/v2/models/camera_motion.py new file mode 100644 index 00000000..b57695e7 --- /dev/null +++ b/aiohue/v2/models/camera_motion.py @@ -0,0 +1,45 @@ +""" +Model(s) for camera_motion resource on HUE bridge. + +https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_camera_motion +""" +from dataclasses import dataclass +from typing import Optional +from .feature import ( + MotionSensingFeature, + MotionSensingFeatureSensitivity, + MotionSensingFeatureSensitivityPut, +) +from .resource import ResourceIdentifier, ResourceTypes + + +@dataclass +class CameraMotion: + """ + Represent a (full) `CameraMotion` resource when retrieved from the api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_camera_motion_get + """ + + id: str + owner: ResourceIdentifier + # enabled: required(boolean) + # true when sensor is activated, false when deactivated + enabled: bool + motion: MotionSensingFeature + sensitivity: Optional[MotionSensingFeatureSensitivity] + + id_v1: Optional[str] = None + type: ResourceTypes = ResourceTypes.CAMERA_MOTION + + +@dataclass +class CameraMotionPut: + """ + CameraMotion resource properties that can be set/updated with a PUT request. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_camera_motion__id__put + """ + + enabled: Optional[bool] = None + sensitivity: Optional[MotionSensingFeatureSensitivityPut] = None diff --git a/aiohue/v2/models/contact.py b/aiohue/v2/models/contact.py new file mode 100644 index 00000000..81dd1b6e --- /dev/null +++ b/aiohue/v2/models/contact.py @@ -0,0 +1,60 @@ +""" +Model(s) for contact resource on HUE bridge. + +https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_contact +""" +from dataclasses import dataclass +from datetime import datetime +from enum import Enum +from typing import Optional + +from .resource import ResourceIdentifier, ResourceTypes + + +class ContactState(Enum): + """State of a Contact sensor.""" + + CONTACT = "contact" + NO_CONTACT = "no_contact" + + +@dataclass +class ContactReport: + """ + Represent ContactReport as retrieved from api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_contact_get + """ + + changed: datetime + state: ContactState + + +@dataclass +class Contact: + """ + Represent a (full) `Contact` resource when retrieved from the api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_contact_get + """ + + id: str + owner: ResourceIdentifier + # enabled: required(boolean) + # true when sensor is activated, false when deactivated + enabled: bool + contact_report: Optional[ContactReport] = None + + id_v1: Optional[str] = None + type: ResourceTypes = ResourceTypes.CONTACT + + +@dataclass +class ContactPut: + """ + Contact resource properties that can be set/updated with a PUT request. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_contact__id__put + """ + + enabled: Optional[bool] = None diff --git a/aiohue/v2/models/feature.py b/aiohue/v2/models/feature.py index 00110271..d7ae862d 100644 --- a/aiohue/v2/models/feature.py +++ b/aiohue/v2/models/feature.py @@ -501,12 +501,14 @@ class Signal(Enum): NO_SIGNAL = "no_signal" ON_OFF = "on_off" + ON_OFF_COLOR = "on_off_color" + ALTERNATING = "alternating" UNKNOWN = "unknown" @classmethod def _missing_(cls: Type, value: object): """Set default enum member if an unknown value is provided.""" - return AlertEffectType.UNKNOWN + return Signal.UNKNOWN @dataclass @@ -515,6 +517,7 @@ class SignalingFeatureStatus: signal: Signal = Signal.UNKNOWN estimated_end: Optional[datetime] = None + colors: Optional[List[ColorFeatureBase]] = None @dataclass @@ -537,7 +540,7 @@ class PowerUpPreset(Enum): @classmethod def _missing_(cls: Type, value: object): """Set default enum member if an unknown value is provided.""" - return AlertEffectType.UNKNOWN + return PowerUpPreset.UNKNOWN class PowerUpFeatureOnMode(Enum): @@ -638,3 +641,73 @@ class PowerUpFeaturePut: on: Optional[PowerUpFeatureOnState] = None dimming: Optional[PowerUpFeatureDimmingState] = None color: Optional[PowerUpFeatureColorState] = None + + +@dataclass +class MotionReport: + """ + Represent MotionReport as retrieved from api. + + Used by `motion` and `camera_motion` resources. + """ + + changed: datetime + motion: bool + + +@dataclass +class MotionSensingFeature: + """ + Represent MotionSensingFeature object as retrieved from api. + + Used by `motion` and `camera_motion` resources. + """ + + motion_report: MotionReport + motion: Optional[bool] = None # deprecated + motion_valid: Optional[bool] = None # deprecated + + @property + def value(self) -> Optional[bool]: + """Return the actual/current value.""" + # prefer new style attribute (not available on older firmware versions) + if self.motion_report is not None: + return self.motion_report.motion + return self.motion + + +class MotionSensingFeatureSensitivityStatus(Enum): + """Enum with possible Sensitivity statuses.""" + + SET = "set" + CHANGING = "changing" + UNKNOWN = "unknown" + + @classmethod + def _missing_(cls: Type, value: object): + """Set default enum member if an unknown value is provided.""" + return MotionSensingFeatureSensitivityStatus.UNKNOWN + + +@dataclass +class MotionSensingFeatureSensitivity: + """ + Represent MotionSensingFeatureSensitivity as retrieved from api. + + Used by `motion` and `camera_motion` resources. + """ + + status: MotionSensingFeatureSensitivityStatus + sensitivity: int + sensitivity_max: int = 10 + + +@dataclass +class MotionSensingFeatureSensitivityPut: + """ + Represent MotionSensingFeatureSensitivity when set/updated with a PUT rquest. + + Used by `motion` and `camera_motion` resources. + """ + + sensitivity: int diff --git a/aiohue/v2/models/light_level.py b/aiohue/v2/models/light_level.py index 484a0078..04ff5c84 100644 --- a/aiohue/v2/models/light_level.py +++ b/aiohue/v2/models/light_level.py @@ -4,17 +4,39 @@ https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light_level """ from dataclasses import dataclass +from datetime import datetime from typing import Optional from .resource import ResourceIdentifier, ResourceTypes +@dataclass +class LightLevelReport: + """ + Represent LightLevelReport as retrieved from api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light_level_get + """ + + changed: datetime + light_level: int + + @dataclass class LightLevelFeature: """Represent LightLevel Feature used by Lightlevel resources.""" - light_level: int - light_level_valid: bool + light_level_report: Optional[LightLevelReport] + light_level: Optional[int] = None # deprecated + light_level_valid: Optional[bool] = None # deprecated + + @property + def value(self) -> Optional[int]: + """Return the actual/current value.""" + # prefer new style attribute (not available on older firmware versions) + if self.light_level_report is not None: + return self.light_level_report.light_level + return self.light_level @dataclass diff --git a/aiohue/v2/models/motion.py b/aiohue/v2/models/motion.py index be4e0c0e..ceb85d55 100644 --- a/aiohue/v2/models/motion.py +++ b/aiohue/v2/models/motion.py @@ -7,18 +7,11 @@ from typing import Optional from .resource import ResourceIdentifier, ResourceTypes - - -@dataclass -class MotionSensingFeature: - """ - Represent MotionSensingFeature as retrieved from api. - - https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_motion_get - """ - - motion: Optional[bool] # value is None/null when sensor is disabled! - motion_valid: bool +from .feature import ( + MotionSensingFeature, + MotionSensingFeatureSensitivity, + MotionSensingFeatureSensitivityPut, +) @dataclass @@ -35,6 +28,7 @@ class Motion: # true when sensor is activated, false when deactivated enabled: bool motion: MotionSensingFeature + sensitivity: Optional[MotionSensingFeatureSensitivity] id_v1: Optional[str] = None type: ResourceTypes = ResourceTypes.MOTION @@ -49,3 +43,4 @@ class MotionPut: """ enabled: Optional[bool] = None + sensitivity: Optional[MotionSensingFeatureSensitivityPut] = None diff --git a/aiohue/v2/models/relative_rotary.py b/aiohue/v2/models/relative_rotary.py index 8c778752..da56d06b 100644 --- a/aiohue/v2/models/relative_rotary.py +++ b/aiohue/v2/models/relative_rotary.py @@ -4,8 +4,9 @@ https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_relative_rotary """ from dataclasses import dataclass +from datetime import datetime from enum import Enum -from typing import Optional, Type +from typing import Optional, Type, Union from .resource import ResourceIdentifier, ResourceTypes @@ -61,11 +62,29 @@ class RelativeRotaryEvent: rotation: RelativeRotaryRotation +@dataclass +class RelativeRotaryReport: + """Represent RelativeRotaryReport object as used by the Hue api.""" + + action: RelativeRotaryAction + rotation: RelativeRotaryRotation + updated: datetime + + @dataclass class RelativeRotaryFeature: """Represent RelativeRotaryFeature object as used by the Hue api.""" - last_event: RelativeRotaryEvent + rotary_report: Optional[RelativeRotaryReport] = None + last_event: Optional[RelativeRotaryEvent] = None # deprecated + + @property + def value(self) -> Union[RelativeRotaryReport, RelativeRotaryEvent, None]: + """Return the actual/current value.""" + # prefer new style attribute (not available on older firmware versions) + if self.rotary_report is not None: + return self.rotary_report + return self.last_event @dataclass diff --git a/aiohue/v2/models/resource.py b/aiohue/v2/models/resource.py index 3b9f7108..b2545635 100644 --- a/aiohue/v2/models/resource.py +++ b/aiohue/v2/models/resource.py @@ -47,6 +47,9 @@ class ResourceTypes(Enum): HOMEKIT = "homekit" MATTER = "matter" MATTER_FABRIC = "matter_fabric" + CONTACT = "contact" + TAMPER = "tamper" + CAMERA_MOTION = "camera_motion" UNKNOWN = "unknown" @classmethod diff --git a/aiohue/v2/models/tamper.py b/aiohue/v2/models/tamper.py new file mode 100644 index 00000000..f3e7a10f --- /dev/null +++ b/aiohue/v2/models/tamper.py @@ -0,0 +1,59 @@ +""" +Model(s) for tamper resource on HUE bridge. + +https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_tamper +""" +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import List, Optional, Type + +from .resource import ResourceIdentifier, ResourceTypes + + +class TamperState(Enum): + """State of a Tamper sensor.""" + + TAMPERED = "tampered" + NOT_TAMPERED = "not_tampered" + + +class TamperSource(Enum): + """Source of a Tamper alert.""" + + BATTERY_DOOR = "battery_door" + UNKNOWN = "unknown" + + @classmethod + def _missing_(cls: Type, value: object): + """Set default enum member if an unknown value is provided.""" + return TamperSource.UNKNOWN + + +@dataclass +class TamperReport: + """ + Represent TamperReport as retrieved from api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_tamper_get + """ + + changed: datetime + source: TamperSource + state: TamperState + + +@dataclass +class Tamper: + """ + Represent a (full) `Tamper` resource when retrieved from the api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_tamper_get + """ + + id: str + owner: ResourceIdentifier + tamper_reports: List[TamperReport] = field(default_factory=list) + + id_v1: Optional[str] = None + type: ResourceTypes = ResourceTypes.CONTACT diff --git a/aiohue/v2/models/temperature.py b/aiohue/v2/models/temperature.py index db108095..0221727e 100644 --- a/aiohue/v2/models/temperature.py +++ b/aiohue/v2/models/temperature.py @@ -5,17 +5,39 @@ """ from dataclasses import dataclass +from datetime import datetime from typing import Optional from .resource import ResourceIdentifier, ResourceTypes @dataclass -class TemperatureFeature: - """Represent TemperatureFeature.""" +class TemperatureReport: + """ + Represent TemperatureReport as retrieved from api. + + https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_temperature_get + """ + changed: datetime temperature: float - temperature_valid: bool + + +@dataclass +class TemperatureSensingFeature: + """Represent TemperatureFeature.""" + + temperature_report: Optional[TemperatureReport] = None + temperature: Optional[float] = None # deprecated + temperature_valid: Optional[bool] = None # deprecated + + @property + def value(self) -> Optional[float]: + """Return the actual/current value.""" + # prefer new style attribute (not available on older firmware versions) + if self.temperature_report is not None: + return self.temperature_report.temperature + return self.temperature @dataclass @@ -29,7 +51,7 @@ class Temperature: id: str owner: ResourceIdentifier enabled: bool - temperature: TemperatureFeature + temperature: TemperatureSensingFeature id_v1: Optional[str] = None type: ResourceTypes = ResourceTypes.TEMPERATURE