From 6b9c2d829541b42c68ef0f0bdb907f0a91600b04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 25 Nov 2021 16:03:53 +0100 Subject: [PATCH] Add shorthand attribute support to Camera platform (#59837) --- homeassistant/components/amcrest/camera.py | 2 +- homeassistant/components/camera/__init__.py | 43 ++++++++++++++------ homeassistant/components/demo/camera.py | 38 +++++------------ homeassistant/components/hyperion/camera.py | 4 +- homeassistant/components/motioneye/camera.py | 2 +- homeassistant/components/nest/camera_sdm.py | 2 +- homeassistant/components/netatmo/camera.py | 6 +-- homeassistant/components/uvc/camera.py | 2 +- tests/components/motioneye/test_camera.py | 4 +- 9 files changed, 52 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 8333ece1030..e250b8ef59b 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -412,7 +412,7 @@ class AmcrestCam(Camera): f"{serial_number}-{self._resolution}-{self._channel}" ) _LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id) - self.is_streaming = self._get_video() + self._attr_is_streaming = self._get_video() self._is_recording = self._get_recording() self._motion_detection_enabled = self._get_motion_detection() self._audio_enabled = self._get_audio() diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 8534120a77f..fd05d3c2095 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -369,9 +369,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class Camera(Entity): """The base class for camera entities.""" + # Entity Properties + _attr_brand: str | None = None + _attr_frame_interval: float = MIN_STREAM_INTERVAL + _attr_frontend_stream_type: str | None + _attr_is_on: bool = True + _attr_is_recording: bool = False + _attr_is_streaming: bool = False + _attr_model: str | None = None + _attr_motion_detection_enabled: bool = False + _attr_should_poll: bool = False # No need to poll cameras + _attr_state: None = None # State is determined by is_on + _attr_supported_features: int = 0 + def __init__(self) -> None: """Initialize a camera.""" - self.is_streaming: bool = False self.stream: Stream | None = None self.stream_options: dict[str, str] = {} self.content_type: str = DEFAULT_CONTENT_TYPE @@ -379,45 +391,47 @@ class Camera(Entity): self._warned_old_signature = False self.async_update_token() - @property - def should_poll(self) -> bool: - """No need to poll cameras.""" - return False - @property def entity_picture(self) -> str: """Return a link to the camera feed as entity picture.""" + if self._attr_entity_picture is not None: + return self._attr_entity_picture return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) @property def supported_features(self) -> int: """Flag supported features.""" - return 0 + return self._attr_supported_features @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return False + return self._attr_is_recording + + @property + def is_streaming(self) -> bool: + """Return true if the device is streaming.""" + return self._attr_is_streaming @property def brand(self) -> str | None: """Return the camera brand.""" - return None + return self._attr_brand @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" - return False + return self._attr_motion_detection_enabled @property def model(self) -> str | None: """Return the camera model.""" - return None + return self._attr_model @property def frame_interval(self) -> float: """Return the interval between frames of the mjpeg stream.""" - return MIN_STREAM_INTERVAL + return self._attr_frame_interval @property def frontend_stream_type(self) -> str | None: @@ -427,6 +441,8 @@ class Camera(Entity): frontend which camera attributes and player to use. The default type is to use HLS, and components can override to change the type. """ + if hasattr(self, "_attr_frontend_stream_type"): + return self._attr_frontend_stream_type if not self.supported_features & SUPPORT_STREAM: return None return STREAM_TYPE_HLS @@ -508,6 +524,7 @@ class Camera(Entity): return await self.handle_async_still_stream(request, self.frame_interval) @property + @final def state(self) -> str: """Return the camera state.""" if self.is_recording: @@ -519,7 +536,7 @@ class Camera(Entity): @property def is_on(self) -> bool: """Return true if on.""" - return True + return self._attr_is_on def turn_off(self) -> None: """Turn off camera.""" diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 572a5bf331e..5131741617e 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -24,13 +24,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class DemoCamera(Camera): """The representation of a Demo camera.""" + _attr_is_streaming = True + _attr_motion_detection_enabled = False + _attr_supported_features = SUPPORT_ON_OFF + def __init__(self, name, content_type): """Initialize demo camera component.""" super().__init__() - self._name = name + self._attr_name = name self.content_type = content_type - self._motion_status = False - self.is_streaming = True self._images_index = 0 async def async_camera_image( @@ -43,42 +45,24 @@ class DemoCamera(Camera): return await self.hass.async_add_executor_job(image_path.read_bytes) - @property - def name(self): - """Return the name of this camera.""" - return self._name - - @property - def supported_features(self): - """Camera support turn on/off features.""" - return SUPPORT_ON_OFF - - @property - def is_on(self): - """Whether camera is on (streaming).""" - return self.is_streaming - - @property - def motion_detection_enabled(self): - """Camera Motion Detection Status.""" - return self._motion_status - async def async_enable_motion_detection(self): """Enable the Motion detection in base station (Arm).""" - self._motion_status = True + self._attr_motion_detection_enabled = True self.async_write_ha_state() async def async_disable_motion_detection(self): """Disable the motion detection in base station (Disarm).""" - self._motion_status = False + self._attr_motion_detection_enabled = False self.async_write_ha_state() async def async_turn_off(self): """Turn off camera.""" - self.is_streaming = False + self._attr_is_streaming = False + self._attr_is_on = False self.async_write_ha_state() async def async_turn_on(self): """Turn on camera.""" - self.is_streaming = True + self._attr_is_streaming = True + self._attr_is_on = True self.async_write_ha_state() diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index c1763c3c21c..4b6492559ea 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -186,7 +186,7 @@ class HyperionCamera(Camera): return False self._image_stream_clients += 1 - self.is_streaming = True + self._attr_is_streaming = True self.async_write_ha_state() return True @@ -196,7 +196,7 @@ class HyperionCamera(Camera): if not self._image_stream_clients: await self._client.async_send_image_stream_stop() - self.is_streaming = False + self._attr_is_streaming = False self.async_write_ha_state() @asynccontextmanager diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index dac5dd7093f..7d9be209c4e 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -159,7 +159,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) # motionEye cameras are always streaming or unavailable. - self.is_streaming = True + self._attr_is_streaming = True MotionEyeEntity.__init__( self, diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 71798eb40c3..5385eb42b26 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -79,7 +79,7 @@ class NestCamera(Camera): self._event_id: str | None = None self._event_image_bytes: bytes | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None - self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits self._placeholder_image: bytes | None = None @property diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 6cb7457f8f6..380571aba70 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -172,13 +172,13 @@ class NetatmoCamera(NetatmoBase, Camera): if data["home_id"] == self._home_id and data["camera_id"] == self._id: if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): - self.is_streaming = False + self._attr_is_streaming = False self._status = "off" elif data[WEBHOOK_PUSH_TYPE] in ( "NACamera-on", WEBHOOK_NACAMERA_CONNECTION, ): - self.is_streaming = True + self._attr_is_streaming = True self._status = "on" elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] @@ -273,7 +273,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._sd_status = camera.get("sd_status") self._alim_status = camera.get("alim_status") self._is_local = camera.get("is_local") - self.is_streaming = bool(self._status == "on") + self._attr_is_streaming = bool(self._status == "on") if self._model == "NACamera": # Smart Indoor Camera self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 77ff6a30f95..fa914de1d6a 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -86,7 +86,7 @@ class UnifiVideoCamera(Camera): self._uuid = uuid self._name = name self._password = password - self.is_streaming = False + self._attr_is_streaming = False self._connect_addr = None self._camera = None self._motion_status = False diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index c1144256ae2..15462f6c592 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -75,7 +75,7 @@ async def test_setup_camera(hass: HomeAssistant) -> None: entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) assert entity_state - assert entity_state.state == "idle" + assert entity_state.state == "streaming" assert entity_state.attributes.get("friendly_name") == TEST_CAMERA_NAME @@ -192,7 +192,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N await setup_mock_motioneye_config_entry(hass, client=client) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) assert entity_state - assert entity_state.state == "idle" + assert entity_state.state == "streaming" cameras = copy.deepcopy(TEST_CAMERAS) cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False