core/homeassistant/components/netatmo/camera.py

416 lines
13 KiB
Python

"""Support for the Netatmo cameras."""
import logging
from pyatmo import NoDevice
import requests
import voluptuous as vol
from homeassistant.components.camera import (
CAMERA_SERVICE_SCHEMA,
PLATFORM_SCHEMA,
SUPPORT_STREAM,
Camera,
)
from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from . import CameraData
from .const import DATA_NETATMO_AUTH, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_HOME = "home"
CONF_CAMERAS = "cameras"
CONF_QUALITY = "quality"
DEFAULT_QUALITY = "high"
VALID_QUALITIES = ["high", "medium", "low", "poor"]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_QUALITY, default=DEFAULT_QUALITY): vol.All(
cv.string, vol.In(VALID_QUALITIES)
),
}
)
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up access to Netatmo cameras."""
home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True)
quality = config.get(CONF_QUALITY, DEFAULT_QUALITY)
auth = hass.data[DATA_NETATMO_AUTH]
try:
data = CameraData(hass, auth, home)
for camera_name in data.get_camera_names():
camera_type = data.get_camera_type(camera=camera_name, home=home)
if CONF_CAMERAS in config:
if (
config[CONF_CAMERAS] != []
and camera_name not in config[CONF_CAMERAS]
):
continue
add_entities(
[
NetatmoCamera(
data, camera_name, home, camera_type, verify_ssl, quality
)
]
)
data.get_persons()
except NoDevice:
return None
async def async_service_handler(call):
"""Handle service call."""
_LOGGER.debug(
"Service handler invoked with service=%s and data=%s",
call.service,
call.data,
)
service = call.service
entity_id = call.data["entity_id"][0]
async_dispatcher_send(hass, f"{service}_{entity_id}")
hass.services.async_register(
DOMAIN, "set_light_auto", async_service_handler, CAMERA_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_light_on", async_service_handler, CAMERA_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, "set_light_off", async_service_handler, CAMERA_SERVICE_SCHEMA
)
class NetatmoCamera(Camera):
"""Representation of the images published from a Netatmo camera."""
def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality):
"""Set up for access to the Netatmo camera images."""
super().__init__()
self._data = data
self._camera_name = camera_name
self._home = home
if home:
self._name = home + " / " + camera_name
else:
self._name = camera_name
self._cameratype = camera_type
self._verify_ssl = verify_ssl
self._quality = quality
# URLs.
self._vpnurl = None
self._localurl = None
# Identifier
self._id = None
# Monitoring status.
self._status = None
# SD Card status
self._sd_status = None
# Power status
self._alim_status = None
# Is local
self._is_local = None
# VPN URL
self._vpn_url = None
# Light mode status
self._light_mode_status = None
def camera_image(self):
"""Return a still image response from the camera."""
try:
if self._localurl:
response = requests.get(
f"{self._localurl}/live/snapshot_720.jpg", timeout=10
)
elif self._vpnurl:
response = requests.get(
f"{self._vpnurl}/live/snapshot_720.jpg",
timeout=10,
verify=self._verify_ssl,
)
else:
_LOGGER.error("Welcome VPN URL is None")
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
except requests.exceptions.RequestException as error:
_LOGGER.error("Welcome URL changed: %s", error)
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
return response.content
# Entity property overrides
@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 name(self):
"""Return the name of this Netatmo camera device."""
return self._name
@property
def device_state_attributes(self):
"""Return the Netatmo-specific camera state attributes."""
_LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name)
attr = {}
attr["id"] = self._id
attr["status"] = self._status
attr["sd_status"] = self._sd_status
attr["alim_status"] = self._alim_status
attr["is_local"] = self._is_local
attr["vpn_url"] = self._vpn_url
if self.model == "Presence":
attr["light_mode_status"] = self._light_mode_status
_LOGGER.debug("Attributes of '%s' = %s", self._name, attr)
return attr
@property
def available(self):
"""Return True if entity is available."""
return bool(self._alim_status == "on")
@property
def supported_features(self):
"""Return supported features."""
return SUPPORT_STREAM
@property
def is_recording(self):
"""Return true if the device is recording."""
return bool(self._status == "on")
@property
def brand(self):
"""Return the camera brand."""
return "Netatmo"
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return bool(self._status == "on")
@property
def is_on(self):
"""Return true if on."""
return self.is_streaming
async def stream_source(self):
"""Return the stream source."""
url = "{0}/live/files/{1}/index.m3u8"
if self._localurl:
return url.format(self._localurl, self._quality)
return url.format(self._vpnurl, self._quality)
@property
def model(self):
"""Return the camera model."""
if self._cameratype == "NOC":
return "Presence"
if self._cameratype == "NACamera":
return "Welcome"
return None
# Other Entity method overrides
async def async_added_to_hass(self):
"""Subscribe to signals and add camera to list."""
_LOGGER.debug("Registering services for entity_id=%s", self.entity_id)
async_dispatcher_connect(
self.hass, f"set_light_auto_{self.entity_id}", self.set_light_auto
)
async_dispatcher_connect(
self.hass, f"set_light_on_{self.entity_id}", self.set_light_on
)
async_dispatcher_connect(
self.hass, f"set_light_off_{self.entity_id}", self.set_light_off
)
def update(self):
"""Update entity status."""
_LOGGER.debug("Updating camera netatmo '%s'", self._name)
# Refresh camera data.
self._data.update()
# URLs.
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
# Identifier
self._id = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["id"]
# Monitoring status.
self._status = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["status"]
_LOGGER.debug("Status of '%s' = %s", self._name, self._status)
# SD Card status
self._sd_status = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["sd_status"]
# Power status
self._alim_status = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["alim_status"]
# Is local
self._is_local = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["is_local"]
# VPN URL
self._vpn_url = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["vpn_url"]
self.is_streaming = self._alim_status == "on"
if self.model == "Presence":
# Light mode status
self._light_mode_status = self._data.camera_data.cameraByName(
camera=self._camera_name, home=self._home
)["light_mode_status"]
# Camera method overrides
def enable_motion_detection(self):
"""Enable motion detection in the camera."""
_LOGGER.debug("Enable motion detection of the camera '%s'", self._name)
self._enable_motion_detection(True)
def disable_motion_detection(self):
"""Disable motion detection in camera."""
_LOGGER.debug("Disable motion detection of the camera '%s'", self._name)
self._enable_motion_detection(False)
def _enable_motion_detection(self, enable):
"""Enable or disable motion detection."""
try:
if self._localurl:
requests.get(
f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}",
timeout=10,
)
elif self._vpnurl:
requests.get(
f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}",
timeout=10,
verify=self._verify_ssl,
)
else:
_LOGGER.error("Welcome/Presence VPN URL is None")
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
except requests.exceptions.RequestException as error:
_LOGGER.error("Welcome/Presence URL changed: %s", error)
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
else:
self.async_schedule_update_ha_state(True)
# Netatmo Presence specific camera method.
def set_light_auto(self):
"""Set flood light in automatic mode."""
_LOGGER.debug(
"Set the flood light in automatic mode for the camera '%s'", self._name
)
self._set_light_mode("auto")
def set_light_on(self):
"""Set flood light on."""
_LOGGER.debug("Set the flood light on for the camera '%s'", self._name)
self._set_light_mode("on")
def set_light_off(self):
"""Set flood light off."""
_LOGGER.debug("Set the flood light off for the camera '%s'", self._name)
self._set_light_mode("off")
def _set_light_mode(self, mode):
"""Set light mode ('auto', 'on', 'off')."""
if self.model == "Presence":
try:
config = '{"mode":"' + mode + '"}'
if self._localurl:
requests.get(
f"{self._localurl}/command/floodlight_set_config?config={config}",
timeout=10,
)
elif self._vpnurl:
requests.get(
f"{self._vpnurl}/command/floodlight_set_config?config={config}",
timeout=10,
verify=self._verify_ssl,
)
else:
_LOGGER.error("Presence VPN URL is None")
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
except requests.exceptions.RequestException as error:
_LOGGER.error("Presence URL changed: %s", error)
self._data.update()
(self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls(
camera=self._camera_name
)
return None
else:
self.async_schedule_update_ha_state(True)
else:
_LOGGER.error("Unsupported camera model for light mode")