416 lines
13 KiB
Python
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")
|