Update Ezviz Component (#45722)
* Update Ezviz Component * Update Ezviz for pylint test * Update Ezviz component pylint tests * Update Ezviz component tests * Update Ezviz Component tests * Update Ezviz component pylint error * Fix ezviz component config flow tests * Update ezviz component * Update Ezviz component * Add sensor platforms * issue with requirements file * Update binary_sensor to include switches * Updates to Ezviz sensors * Removed enum private method. * Fix switch args * Update homeassistant/components/ezviz/switch.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * config flow checks login info * Config_flow now imports ezviz from camera platform * Update test * Updated config_flow with unique_id and remove period from logging * Added two camera services and clarified service descryptions in services.yaml * Fixed variable name mistake with new service * Added french integration translation * Config_flow add camera rtsp credentials as seperate entities, with user step and import step * rerun hassfest after rebase * Removed region from legacy config schema, removed logging in camera platform setup that could contain credentials, removed unused constant. * Regenerate requirements * Fix tests and add config_flow import config test * Added addition test to config_flow to test successfull camera entity create. * Add to tests method to end in create entry, config_flow cleanup, use entry instead of entry.data * Removed all services, sorted platforms in init file. * Changed RTSP logging to debug from warning. (Forgot to change this before commit) * Cleanup typing, change platform order, bump pyezviz version * Added types to entries, allow creation of main entry if deleted by validating existance of type * Config_flow doesn't store serial under entry data, camera rtsp read from entry and not stored in hass, removed duplicate abort if unique id from config flow * Fix test of config_flow * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/ezviz/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Bumped pyezviz api version, added api pyezvizerror exception raised in api (on HTTPError), cleanup unused imports. * rebase * cleanup coordinator, bump pyezviz api version, move async_setup_entry to add entry options to camera entries. (order change) * Added discovery step in config_flow if cameras detected without rtsp config entry * Reload main integration after addition or completion of camera rtsp config entry * Add tests for discovery config_flow, added a few other output asserts * Camera platform call discover flow with hass.async_create_task. Fixes to config_flow for discovery step * Fix config_flow discovery, add check to legacy yaml camera platform import, move camera private method to camera import step * Remove not needed check from config_flow import step. * Cleanup config_flow * Added config_flow description for discovered camera * Reordered description in config_flow confim step. * Added serial to flow_step description for discovered camera, readded camera attributes for rtsp stream url (allows user to check RTSP cred), added local ip and firmware upgade available. * Bumped pyezviz version and changed region code to region url. (Russia uses a completly different url). PyEzviz adds a Local IP sensor, removed camera entity attributes. * Add RSTP describe auth check from API to config_flow * url as vol.in options in Config_flow * Config_flow changes to discovery step, added exceptions, fixed tests, added rtsp config validate module mock to test disovery confirm step * Add test for config_flow step user_camera * Added tests for abort flow * Extend tests on custom url flow step * Fix exceptions in config_flow, fix test for discovery import exception test * Bump pyezviz api version * Bump api version, added config_flow function to wake hybernating camera before testing credentials, removed "user camera" step from config flow not needed as cameras are discovered. * Create pyezviz Api instance for config_flow wake hybernating camera, fixed tests and added fixture to mock method * Added alarm_control_panel with support to arm/disarm all cameras, fixed camera is available attribute (returns 2 if unavailable, 1 if available) * Skip ignored entities when setup up camera RTSP stream * Remove alarm_control_panel, add additional config_flow tests * Cleanup tests, add tests for discovery_step. * Add test for config_flow rtsp test step1 exceptions * Removed redundant except from second step in test RTSP method * All tests to CREATE or ABORT, added step exception for general HTTP error so user can retry in case of trasient network condition * Ammended tests with output checks for step_id, error, data, create entry method calls. * bumped ezviz api now rases library exceptions. Config_flow, coordiantor and init raises library exceptions. Updated test sideeffect for library exceptions * Bump api version, Create mock ezviz cloud account on discovery tests first to allow more complete testing of step. * Add abort to rtsp verification method if cloud account was deleted and add tests * Update tests/components/ezviz/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/ezviz/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Undo config import change to password key for yaml, move hass.data.setdefault to async_setup_entry and remove async_setup * Fixed tests by removing _patch_async_setup as this was removed from init. * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/ezviz/camera.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Changed L67 on camera config to complete suggestion for cleanup Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
e30cf88459
commit
155322584d
19 changed files with 1843 additions and 154 deletions
|
@ -1,28 +1,30 @@
|
|||
"""This component provides basic support for Ezviz IP cameras."""
|
||||
"""Support ezviz camera devices."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
# pylint: disable=import-error
|
||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
||||
from pyezviz.camera import EzvizCamera
|
||||
from pyezviz.client import EzvizClient, PyEzvizError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IGNORE, SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CAMERAS = "cameras"
|
||||
|
||||
DEFAULT_CAMERA_USERNAME = "admin"
|
||||
DEFAULT_RTSP_PORT = "554"
|
||||
|
||||
DATA_FFMPEG = "ffmpeg"
|
||||
|
||||
EZVIZ_DATA = "ezviz"
|
||||
ENTITIES = "entities"
|
||||
from .const import (
|
||||
ATTR_SERIAL,
|
||||
CONF_CAMERAS,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
DATA_COORDINATOR,
|
||||
DEFAULT_CAMERA_USERNAME,
|
||||
DEFAULT_FFMPEG_ARGUMENTS,
|
||||
DEFAULT_RTSP_PORT,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
CAMERA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
|
@ -36,162 +38,162 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ezviz IP Cameras."""
|
||||
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
|
||||
|
||||
conf_cameras = config[CONF_CAMERAS]
|
||||
|
||||
account = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up a Ezviz IP Camera from platform config."""
|
||||
_LOGGER.warning(
|
||||
"Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards"
|
||||
)
|
||||
|
||||
try:
|
||||
ezviz_client = EzvizClient(account, password)
|
||||
ezviz_client.login()
|
||||
cameras = ezviz_client.load_cameras()
|
||||
|
||||
except PyEzvizError as exp:
|
||||
_LOGGER.error(exp)
|
||||
# Check if entry config exists and skips import if it does.
|
||||
if hass.config_entries.async_entries(DOMAIN):
|
||||
return
|
||||
|
||||
# now, let's build the HASS devices
|
||||
# Check if importing camera account.
|
||||
if CONF_CAMERAS in config:
|
||||
cameras_conf = config[CONF_CAMERAS]
|
||||
for serial, camera in cameras_conf.items():
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
ATTR_SERIAL: serial,
|
||||
CONF_USERNAME: camera[CONF_USERNAME],
|
||||
CONF_PASSWORD: camera[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Check if importing main ezviz cloud account.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up Ezviz cameras based on a config entry."""
|
||||
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR]
|
||||
camera_config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
camera_entities = []
|
||||
|
||||
# Add the cameras as devices in HASS
|
||||
for camera in cameras:
|
||||
|
||||
camera_username = DEFAULT_CAMERA_USERNAME
|
||||
camera_password = ""
|
||||
camera_rtsp_stream = ""
|
||||
camera_serial = camera["serial"]
|
||||
for idx, camera in enumerate(coordinator.data):
|
||||
|
||||
# There seem to be a bug related to localRtspPort in Ezviz API...
|
||||
local_rtsp_port = DEFAULT_RTSP_PORT
|
||||
if camera["local_rtsp_port"] and camera["local_rtsp_port"] != 0:
|
||||
|
||||
camera_rtsp_entry = [
|
||||
item
|
||||
for item in camera_config_entries
|
||||
if item.unique_id == camera[ATTR_SERIAL]
|
||||
]
|
||||
|
||||
if camera["local_rtsp_port"] != 0:
|
||||
local_rtsp_port = camera["local_rtsp_port"]
|
||||
|
||||
if camera_serial in conf_cameras:
|
||||
camera_username = conf_cameras[camera_serial][CONF_USERNAME]
|
||||
camera_password = conf_cameras[camera_serial][CONF_PASSWORD]
|
||||
camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}"
|
||||
if camera_rtsp_entry:
|
||||
conf_cameras = camera_rtsp_entry[0]
|
||||
|
||||
# Skip ignored entities.
|
||||
if conf_cameras.source == SOURCE_IGNORE:
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = conf_cameras.options.get(
|
||||
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
|
||||
)
|
||||
|
||||
camera_username = conf_cameras.data[CONF_USERNAME]
|
||||
camera_password = conf_cameras.data[CONF_PASSWORD]
|
||||
|
||||
camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}"
|
||||
_LOGGER.debug(
|
||||
"Camera %s source stream: %s", camera["serial"], camera_rtsp_stream
|
||||
"Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream
|
||||
)
|
||||
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Found camera with serial %s without configuration. Add it to configuration.yaml to see the camera stream",
|
||||
camera_serial,
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_DISCOVERY},
|
||||
data={
|
||||
ATTR_SERIAL: camera[ATTR_SERIAL],
|
||||
CONF_IP_ADDRESS: camera["local_ip"],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
camera["username"] = camera_username
|
||||
camera["password"] = camera_password
|
||||
camera["rtsp_stream"] = camera_rtsp_stream
|
||||
camera_username = DEFAULT_CAMERA_USERNAME
|
||||
camera_password = ""
|
||||
camera_rtsp_stream = ""
|
||||
ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS
|
||||
_LOGGER.warning(
|
||||
"Found camera with serial %s without configuration. Please go to integration to complete setup",
|
||||
camera[ATTR_SERIAL],
|
||||
)
|
||||
|
||||
camera["ezviz_camera"] = EzvizCamera(ezviz_client, camera_serial)
|
||||
camera_entities.append(
|
||||
EzvizCamera(
|
||||
hass,
|
||||
coordinator,
|
||||
idx,
|
||||
camera_username,
|
||||
camera_password,
|
||||
camera_rtsp_stream,
|
||||
local_rtsp_port,
|
||||
ffmpeg_arguments,
|
||||
)
|
||||
)
|
||||
|
||||
camera_entities.append(HassEzvizCamera(**camera))
|
||||
|
||||
add_entities(camera_entities)
|
||||
async_add_entities(camera_entities)
|
||||
|
||||
|
||||
class HassEzvizCamera(Camera):
|
||||
"""An implementation of a Foscam IP camera."""
|
||||
class EzvizCamera(CoordinatorEntity, Camera, RestoreEntity):
|
||||
"""An implementation of a Ezviz security camera."""
|
||||
|
||||
def __init__(self, **data):
|
||||
"""Initialize an Ezviz camera."""
|
||||
super().__init__()
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
coordinator,
|
||||
idx,
|
||||
camera_username,
|
||||
camera_password,
|
||||
camera_rtsp_stream,
|
||||
local_rtsp_port,
|
||||
ffmpeg_arguments,
|
||||
):
|
||||
"""Initialize a Ezviz security camera."""
|
||||
super().__init__(coordinator)
|
||||
Camera.__init__(self)
|
||||
self._username = camera_username
|
||||
self._password = camera_password
|
||||
self._rtsp_stream = camera_rtsp_stream
|
||||
self._idx = idx
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._local_rtsp_port = local_rtsp_port
|
||||
self._ffmpeg_arguments = ffmpeg_arguments
|
||||
|
||||
self._username = data["username"]
|
||||
self._password = data["password"]
|
||||
self._rtsp_stream = data["rtsp_stream"]
|
||||
|
||||
self._ezviz_camera = data["ezviz_camera"]
|
||||
self._serial = data["serial"]
|
||||
self._name = data["name"]
|
||||
self._status = data["status"]
|
||||
self._privacy = data["privacy"]
|
||||
self._audio = data["audio"]
|
||||
self._ir_led = data["ir_led"]
|
||||
self._state_led = data["state_led"]
|
||||
self._follow_move = data["follow_move"]
|
||||
self._alarm_notify = data["alarm_notify"]
|
||||
self._alarm_sound_mod = data["alarm_sound_mod"]
|
||||
self._encrypted = data["encrypted"]
|
||||
self._local_ip = data["local_ip"]
|
||||
self._detection_sensibility = data["detection_sensibility"]
|
||||
self._device_sub_category = data["device_sub_category"]
|
||||
self._local_rtsp_port = data["local_rtsp_port"]
|
||||
|
||||
self._ffmpeg = None
|
||||
|
||||
def update(self):
|
||||
"""Update the camera states."""
|
||||
|
||||
data = self._ezviz_camera.status()
|
||||
|
||||
self._name = data["name"]
|
||||
self._status = data["status"]
|
||||
self._privacy = data["privacy"]
|
||||
self._audio = data["audio"]
|
||||
self._ir_led = data["ir_led"]
|
||||
self._state_led = data["state_led"]
|
||||
self._follow_move = data["follow_move"]
|
||||
self._alarm_notify = data["alarm_notify"]
|
||||
self._alarm_sound_mod = data["alarm_sound_mod"]
|
||||
self._encrypted = data["encrypted"]
|
||||
self._local_ip = data["local_ip"]
|
||||
self._detection_sensibility = data["detection_sensibility"]
|
||||
self._device_sub_category = data["device_sub_category"]
|
||||
self._local_rtsp_port = data["local_rtsp_port"]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to ffmpeg and add camera to list."""
|
||||
self._ffmpeg = self.hass.data[DATA_FFMPEG]
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if entity has to be polled for state.
|
||||
|
||||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Return the Ezviz-specific camera state attributes."""
|
||||
return {
|
||||
# if privacy == true, the device closed the lid or did a 180° tilt
|
||||
"privacy": self._privacy,
|
||||
# is the camera listening ?
|
||||
"audio": self._audio,
|
||||
# infrared led on ?
|
||||
"ir_led": self._ir_led,
|
||||
# state led on ?
|
||||
"state_led": self._state_led,
|
||||
# if true, the camera will move automatically to follow movements
|
||||
"follow_move": self._follow_move,
|
||||
# if true, if some movement is detected, the app is notified
|
||||
"alarm_notify": self._alarm_notify,
|
||||
# if true, if some movement is detected, the camera makes some sound
|
||||
"alarm_sound_mod": self._alarm_sound_mod,
|
||||
# are the camera's stored videos/images encrypted?
|
||||
"encrypted": self._encrypted,
|
||||
# camera's local ip on local network
|
||||
"local_ip": self._local_ip,
|
||||
# from 1 to 9, the higher is the sensibility, the more it will detect small movements
|
||||
"detection_sensibility": self._detection_sensibility,
|
||||
}
|
||||
self._serial = self.coordinator.data[self._idx]["serial"]
|
||||
self._name = self.coordinator.data[self._idx]["name"]
|
||||
self._local_ip = self.coordinator.data[self._idx]["local_ip"]
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._status
|
||||
if self.coordinator.data[self._idx]["status"] == 2:
|
||||
return False
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the camera brand."""
|
||||
return "Ezviz"
|
||||
return True
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -200,20 +202,40 @@ class HassEzvizCamera(Camera):
|
|||
return SUPPORT_STREAM
|
||||
return 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Return the camera model."""
|
||||
return self._device_sub_category
|
||||
"""Return the model of this device."""
|
||||
return self.coordinator.data[self._idx]["device_sub_category"]
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Return the manufacturer of this device."""
|
||||
return MANUFACTURER
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._status
|
||||
return bool(self.coordinator.data[self._idx]["status"])
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
return self.coordinator.data[self._idx]["alarm_notify"]
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
return self.coordinator.data[self._idx]["alarm_notify"]
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the name of this camera."""
|
||||
return self._name
|
||||
return self._serial
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a frame from the camera stream."""
|
||||
|
@ -224,12 +246,24 @@ class HassEzvizCamera(Camera):
|
|||
)
|
||||
return image
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._serial)},
|
||||
"name": self.coordinator.data[self._idx]["name"],
|
||||
"model": self.coordinator.data[self._idx]["device_sub_category"],
|
||||
"manufacturer": MANUFACTURER,
|
||||
"sw_version": self.coordinator.data[self._idx]["version"],
|
||||
}
|
||||
|
||||
async def stream_source(self):
|
||||
"""Return the stream source."""
|
||||
local_ip = self.coordinator.data[self._idx]["local_ip"]
|
||||
if self._local_rtsp_port:
|
||||
rtsp_stream_source = (
|
||||
f"rtsp://{self._username}:{self._password}@"
|
||||
f"{self._local_ip}:{self._local_rtsp_port}"
|
||||
f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}"
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Camera %s source stream: %s", self._serial, rtsp_stream_source
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue