Add shorthand attribute support to Camera platform (#59837)

This commit is contained in:
Franck Nijhof 2021-11-25 16:03:53 +01:00 committed by GitHub
parent 57fd632cd9
commit 6b9c2d8295
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 52 additions and 51 deletions

View File

@ -412,7 +412,7 @@ class AmcrestCam(Camera):
f"{serial_number}-{self._resolution}-{self._channel}" f"{serial_number}-{self._resolution}-{self._channel}"
) )
_LOGGER.debug("Assigned unique_id=%s", self._attr_unique_id) _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._is_recording = self._get_recording()
self._motion_detection_enabled = self._get_motion_detection() self._motion_detection_enabled = self._get_motion_detection()
self._audio_enabled = self._get_audio() self._audio_enabled = self._get_audio()

View File

@ -369,9 +369,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class Camera(Entity): class Camera(Entity):
"""The base class for camera entities.""" """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: def __init__(self) -> None:
"""Initialize a camera.""" """Initialize a camera."""
self.is_streaming: bool = False
self.stream: Stream | None = None self.stream: Stream | None = None
self.stream_options: dict[str, str] = {} self.stream_options: dict[str, str] = {}
self.content_type: str = DEFAULT_CONTENT_TYPE self.content_type: str = DEFAULT_CONTENT_TYPE
@ -379,45 +391,47 @@ class Camera(Entity):
self._warned_old_signature = False self._warned_old_signature = False
self.async_update_token() self.async_update_token()
@property
def should_poll(self) -> bool:
"""No need to poll cameras."""
return False
@property @property
def entity_picture(self) -> str: def entity_picture(self) -> str:
"""Return a link to the camera feed as entity picture.""" """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]) return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return 0 return self._attr_supported_features
@property @property
def is_recording(self) -> bool: def is_recording(self) -> bool:
"""Return true if the device is recording.""" """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 @property
def brand(self) -> str | None: def brand(self) -> str | None:
"""Return the camera brand.""" """Return the camera brand."""
return None return self._attr_brand
@property @property
def motion_detection_enabled(self) -> bool: def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return False return self._attr_motion_detection_enabled
@property @property
def model(self) -> str | None: def model(self) -> str | None:
"""Return the camera model.""" """Return the camera model."""
return None return self._attr_model
@property @property
def frame_interval(self) -> float: def frame_interval(self) -> float:
"""Return the interval between frames of the mjpeg stream.""" """Return the interval between frames of the mjpeg stream."""
return MIN_STREAM_INTERVAL return self._attr_frame_interval
@property @property
def frontend_stream_type(self) -> str | None: 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 frontend which camera attributes and player to use. The default type
is to use HLS, and components can override to change the 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: if not self.supported_features & SUPPORT_STREAM:
return None return None
return STREAM_TYPE_HLS return STREAM_TYPE_HLS
@ -508,6 +524,7 @@ class Camera(Entity):
return await self.handle_async_still_stream(request, self.frame_interval) return await self.handle_async_still_stream(request, self.frame_interval)
@property @property
@final
def state(self) -> str: def state(self) -> str:
"""Return the camera state.""" """Return the camera state."""
if self.is_recording: if self.is_recording:
@ -519,7 +536,7 @@ class Camera(Entity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if on.""" """Return true if on."""
return True return self._attr_is_on
def turn_off(self) -> None: def turn_off(self) -> None:
"""Turn off camera.""" """Turn off camera."""

View File

@ -24,13 +24,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class DemoCamera(Camera): class DemoCamera(Camera):
"""The representation of a Demo 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): def __init__(self, name, content_type):
"""Initialize demo camera component.""" """Initialize demo camera component."""
super().__init__() super().__init__()
self._name = name self._attr_name = name
self.content_type = content_type self.content_type = content_type
self._motion_status = False
self.is_streaming = True
self._images_index = 0 self._images_index = 0
async def async_camera_image( async def async_camera_image(
@ -43,42 +45,24 @@ class DemoCamera(Camera):
return await self.hass.async_add_executor_job(image_path.read_bytes) 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): async def async_enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm).""" """Enable the Motion detection in base station (Arm)."""
self._motion_status = True self._attr_motion_detection_enabled = True
self.async_write_ha_state() self.async_write_ha_state()
async def async_disable_motion_detection(self): async def async_disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm).""" """Disable the motion detection in base station (Disarm)."""
self._motion_status = False self._attr_motion_detection_enabled = False
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self): async def async_turn_off(self):
"""Turn off camera.""" """Turn off camera."""
self.is_streaming = False self._attr_is_streaming = False
self._attr_is_on = False
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_on(self): async def async_turn_on(self):
"""Turn on camera.""" """Turn on camera."""
self.is_streaming = True self._attr_is_streaming = True
self._attr_is_on = True
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -186,7 +186,7 @@ class HyperionCamera(Camera):
return False return False
self._image_stream_clients += 1 self._image_stream_clients += 1
self.is_streaming = True self._attr_is_streaming = True
self.async_write_ha_state() self.async_write_ha_state()
return True return True
@ -196,7 +196,7 @@ class HyperionCamera(Camera):
if not self._image_stream_clients: if not self._image_stream_clients:
await self._client.async_send_image_stream_stop() await self._client.async_send_image_stream_stop()
self.is_streaming = False self._attr_is_streaming = False
self.async_write_ha_state() self.async_write_ha_state()
@asynccontextmanager @asynccontextmanager

View File

@ -159,7 +159,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera):
self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False) self._motion_detection_enabled: bool = camera.get(KEY_MOTION_DETECTION, False)
# motionEye cameras are always streaming or unavailable. # motionEye cameras are always streaming or unavailable.
self.is_streaming = True self._attr_is_streaming = True
MotionEyeEntity.__init__( MotionEyeEntity.__init__(
self, self,

View File

@ -79,7 +79,7 @@ class NestCamera(Camera):
self._event_id: str | None = None self._event_id: str | None = None
self._event_image_bytes: bytes | None = None self._event_image_bytes: bytes | None = None
self._event_image_cleanup_unsub: Callable[[], None] | 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 self._placeholder_image: bytes | None = None
@property @property

View File

@ -172,13 +172,13 @@ class NetatmoCamera(NetatmoBase, Camera):
if data["home_id"] == self._home_id and data["camera_id"] == self._id: if data["home_id"] == self._home_id and data["camera_id"] == self._id:
if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"):
self.is_streaming = False self._attr_is_streaming = False
self._status = "off" self._status = "off"
elif data[WEBHOOK_PUSH_TYPE] in ( elif data[WEBHOOK_PUSH_TYPE] in (
"NACamera-on", "NACamera-on",
WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_NACAMERA_CONNECTION,
): ):
self.is_streaming = True self._attr_is_streaming = True
self._status = "on" self._status = "on"
elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE:
self._light_state = data["sub_type"] self._light_state = data["sub_type"]
@ -273,7 +273,7 @@ class NetatmoCamera(NetatmoBase, Camera):
self._sd_status = camera.get("sd_status") self._sd_status = camera.get("sd_status")
self._alim_status = camera.get("alim_status") self._alim_status = camera.get("alim_status")
self._is_local = camera.get("is_local") 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 if self._model == "NACamera": # Smart Indoor Camera
self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(

View File

@ -86,7 +86,7 @@ class UnifiVideoCamera(Camera):
self._uuid = uuid self._uuid = uuid
self._name = name self._name = name
self._password = password self._password = password
self.is_streaming = False self._attr_is_streaming = False
self._connect_addr = None self._connect_addr = None
self._camera = None self._camera = None
self._motion_status = False self._motion_status = False

View File

@ -75,7 +75,7 @@ async def test_setup_camera(hass: HomeAssistant) -> None:
entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
assert entity_state assert entity_state
assert entity_state.state == "idle" assert entity_state.state == "streaming"
assert entity_state.attributes.get("friendly_name") == TEST_CAMERA_NAME 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) await setup_mock_motioneye_config_entry(hass, client=client)
entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID)
assert entity_state assert entity_state
assert entity_state.state == "idle" assert entity_state.state == "streaming"
cameras = copy.deepcopy(TEST_CAMERAS) cameras = copy.deepcopy(TEST_CAMERAS)
cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False cameras[KEY_CAMERAS][0][KEY_VIDEO_STREAMING] = False