diff --git a/.coveragerc b/.coveragerc index 9bffb4350f9..221b43998c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -567,6 +567,7 @@ omit = homeassistant/components/qnap/sensor.py homeassistant/components/qrcode/image_processing.py homeassistant/components/quantum_gateway/device_tracker.py + homeassistant/components/qvr_pro/* homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/radarr/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 35ff288879c..8f821f43fec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -278,6 +278,7 @@ homeassistant/components/pvoutput/* @fabaff homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan +homeassistant/components/qvr_pro/* @oblogic7 homeassistant/components/qwikswitch/* @kellerza homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator diff --git a/homeassistant/components/qvr_pro/__init__.py b/homeassistant/components/qvr_pro/__init__.py new file mode 100644 index 00000000000..f2840d49299 --- /dev/null +++ b/homeassistant/components/qvr_pro/__init__.py @@ -0,0 +1,100 @@ +"""Support for QVR Pro NVR software by QNAP.""" + +import logging + +from pyqvrpro import Client +from pyqvrpro.client import AuthenticationError, InsufficientPermissionsError +import voluptuous as vol + +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +from .const import ( + CONF_EXCLUDE_CHANNELS, + DOMAIN, + SERVICE_START_RECORD, + SERVICE_STOP_RECORD, +) + +SERVICE_CHANNEL_GUID = "guid" + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_EXCLUDE_CHANNELS, default=[]): vol.All( + cv.ensure_list_csv, [cv.positive_int] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SERVICE_CHANNEL_RECORD_SCHEMA = vol.Schema( + {vol.Required(SERVICE_CHANNEL_GUID): cv.string} +) + + +def setup(hass, config): + """Set up the QVR Pro component.""" + conf = config[DOMAIN] + user = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + host = conf[CONF_HOST] + excluded_channels = conf[CONF_EXCLUDE_CHANNELS] + + try: + qvrpro = Client(user, password, host) + + channel_resp = qvrpro.get_channel_list() + + except InsufficientPermissionsError: + _LOGGER.error("User must have Surveillance Management permission") + return False + except AuthenticationError: + _LOGGER.error("Authentication failed") + return False + + channels = [] + + for channel in channel_resp["channels"]: + if channel["channel_index"] + 1 in excluded_channels: + continue + + channels.append(channel) + + hass.data[DOMAIN] = {"channels": channels, "client": qvrpro} + + load_platform(hass, CAMERA_DOMAIN, DOMAIN, {}, config) + + # Register services + def handle_start_record(call): + guid = call.data[SERVICE_CHANNEL_GUID] + qvrpro.start_recording(guid) + + def handle_stop_record(call): + guid = call.data[SERVICE_CHANNEL_GUID] + qvrpro.stop_recording(guid) + + hass.services.register( + DOMAIN, + SERVICE_START_RECORD, + handle_start_record, + schema=SERVICE_CHANNEL_RECORD_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_STOP_RECORD, + handle_stop_record, + schema=SERVICE_CHANNEL_RECORD_SCHEMA, + ) + + return True diff --git a/homeassistant/components/qvr_pro/camera.py b/homeassistant/components/qvr_pro/camera.py new file mode 100644 index 00000000000..28f607165a7 --- /dev/null +++ b/homeassistant/components/qvr_pro/camera.py @@ -0,0 +1,102 @@ +"""Support for QVR Pro streams.""" + +import logging + +from pyqvrpro.client import QVRResponseError + +from homeassistant.components.camera import Camera + +from .const import DOMAIN, SHORT_NAME + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the QVR Pro camera platform.""" + if discovery_info is None: + return + + client = hass.data[DOMAIN]["client"] + + entities = [] + + for channel in hass.data[DOMAIN]["channels"]: + + stream_source = get_stream_source(channel["guid"], client) + entities.append( + QVRProCamera(**channel, stream_source=stream_source, client=client) + ) + + add_entities(entities) + + +def get_stream_source(guid, client): + """Get channel stream source.""" + try: + resp = client.get_channel_live_stream(guid, protocol="rtsp") + + full_url = resp["resourceUris"] + + protocol = full_url[:7] + auth = f"{client.get_auth_string()}@" + url = full_url[7:] + + return f"{protocol}{auth}{url}" + + except QVRResponseError as ex: + _LOGGER.error(ex) + return None + + +class QVRProCamera(Camera): + """Representation of a QVR Pro camera.""" + + def __init__(self, name, model, brand, channel_index, guid, stream_source, client): + """Init QVR Pro camera.""" + + self._name = f"{SHORT_NAME} {name}" + self._model = model + self._brand = brand + self.index = channel_index + self.guid = guid + self._client = client + self._stream_source = stream_source + + self._supported_features = 0 + + super().__init__() + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def model(self): + """Return the model of the entity.""" + return self._model + + @property + def brand(self): + """Return the brand of the entity.""" + return self._brand + + @property + def device_state_attributes(self): + """Get the state attributes.""" + attrs = {"qvr_guid": self.guid} + + return attrs + + def camera_image(self): + """Get image bytes from camera.""" + return self._client.get_snapshot(self.guid) + + async def stream_source(self): + """Get stream source.""" + return self._stream_source + + @property + def supported_features(self): + """Get supported features.""" + return self._supported_features diff --git a/homeassistant/components/qvr_pro/const.py b/homeassistant/components/qvr_pro/const.py new file mode 100644 index 00000000000..eadf756a1c2 --- /dev/null +++ b/homeassistant/components/qvr_pro/const.py @@ -0,0 +1,9 @@ +"""Constants for QVR Pro component.""" + +DOMAIN = "qvr_pro" +SHORT_NAME = "QVR" + +CONF_EXCLUDE_CHANNELS = "exclude_channels" + +SERVICE_STOP_RECORD = "stop_record" +SERVICE_START_RECORD = "start_record" diff --git a/homeassistant/components/qvr_pro/manifest.json b/homeassistant/components/qvr_pro/manifest.json new file mode 100644 index 00000000000..3bef827a019 --- /dev/null +++ b/homeassistant/components/qvr_pro/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "qvr_pro", + "name": "QVR Pro", + "documentation": "https://www.home-assistant.io/integrations/qvr_pro", + "requirements": ["pyqvrpro==0.51"], + "dependencies": [], + "codeowners": ["@oblogic7"] +} diff --git a/homeassistant/components/qvr_pro/services.yaml b/homeassistant/components/qvr_pro/services.yaml new file mode 100644 index 00000000000..cc6866fee63 --- /dev/null +++ b/homeassistant/components/qvr_pro/services.yaml @@ -0,0 +1,13 @@ +start_record: + description: Start QVR Pro recording on specified channel. + fields: + guid: + description: GUID of the channel to start recording. + example: '245EBE933C0A597EBE865C0A245E0002' + +stop_record: + description: Stop QVR Pro recording on specified channel. + fields: + guid: + description: GUID of the channel to stop recording. + example: '245EBE933C0A597EBE865C0A245E0002' \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 6787b02969d..628f98404ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1472,6 +1472,9 @@ pypoint==1.1.2 # homeassistant.components.ps4 pyps4-2ndscreen==1.0.7 +# homeassistant.components.qvr_pro +pyqvrpro==0.51 + # homeassistant.components.qwikswitch pyqwikswitch==0.93