344 lines
10 KiB
Python
344 lines
10 KiB
Python
"""
|
|
Support for IP Webcam, an Android app that acts as a full-featured webcam.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/android_ip_webcam/
|
|
"""
|
|
import asyncio
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.core import callback
|
|
from homeassistant.const import (
|
|
CONF_NAME,
|
|
CONF_HOST,
|
|
CONF_PORT,
|
|
CONF_USERNAME,
|
|
CONF_PASSWORD,
|
|
CONF_SENSORS,
|
|
CONF_SWITCHES,
|
|
CONF_TIMEOUT,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_PLATFORM,
|
|
)
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
from homeassistant.helpers import discovery
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.dispatcher import (
|
|
async_dispatcher_send,
|
|
async_dispatcher_connect,
|
|
)
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
|
from homeassistant.util.dt import utcnow
|
|
from homeassistant.components.camera.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
|
|
|
|
REQUIREMENTS = ["pydroid-ipcam==0.8"]
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
ATTR_AUD_CONNS = "Audio Connections"
|
|
ATTR_HOST = "host"
|
|
ATTR_VID_CONNS = "Video Connections"
|
|
|
|
CONF_MOTION_SENSOR = "motion_sensor"
|
|
|
|
DATA_IP_WEBCAM = "android_ip_webcam"
|
|
DEFAULT_NAME = "IP Webcam"
|
|
DEFAULT_PORT = 8080
|
|
DEFAULT_TIMEOUT = 10
|
|
DOMAIN = "android_ip_webcam"
|
|
|
|
SCAN_INTERVAL = timedelta(seconds=10)
|
|
SIGNAL_UPDATE_DATA = "android_ip_webcam_update"
|
|
|
|
KEY_MAP = {
|
|
"audio_connections": "Audio Connections",
|
|
"adet_limit": "Audio Trigger Limit",
|
|
"antibanding": "Anti-banding",
|
|
"audio_only": "Audio Only",
|
|
"battery_level": "Battery Level",
|
|
"battery_temp": "Battery Temperature",
|
|
"battery_voltage": "Battery Voltage",
|
|
"coloreffect": "Color Effect",
|
|
"exposure": "Exposure Level",
|
|
"exposure_lock": "Exposure Lock",
|
|
"ffc": "Front-facing Camera",
|
|
"flashmode": "Flash Mode",
|
|
"focus": "Focus",
|
|
"focus_homing": "Focus Homing",
|
|
"focus_region": "Focus Region",
|
|
"focusmode": "Focus Mode",
|
|
"gps_active": "GPS Active",
|
|
"idle": "Idle",
|
|
"ip_address": "IPv4 Address",
|
|
"ipv6_address": "IPv6 Address",
|
|
"ivideon_streaming": "Ivideon Streaming",
|
|
"light": "Light Level",
|
|
"mirror_flip": "Mirror Flip",
|
|
"motion": "Motion",
|
|
"motion_active": "Motion Active",
|
|
"motion_detect": "Motion Detection",
|
|
"motion_event": "Motion Event",
|
|
"motion_limit": "Motion Limit",
|
|
"night_vision": "Night Vision",
|
|
"night_vision_average": "Night Vision Average",
|
|
"night_vision_gain": "Night Vision Gain",
|
|
"orientation": "Orientation",
|
|
"overlay": "Overlay",
|
|
"photo_size": "Photo Size",
|
|
"pressure": "Pressure",
|
|
"proximity": "Proximity",
|
|
"quality": "Quality",
|
|
"scenemode": "Scene Mode",
|
|
"sound": "Sound",
|
|
"sound_event": "Sound Event",
|
|
"sound_timeout": "Sound Timeout",
|
|
"torch": "Torch",
|
|
"video_connections": "Video Connections",
|
|
"video_chunk_len": "Video Chunk Length",
|
|
"video_recording": "Video Recording",
|
|
"video_size": "Video Size",
|
|
"whitebalance": "White Balance",
|
|
"whitebalance_lock": "White Balance Lock",
|
|
"zoom": "Zoom",
|
|
}
|
|
|
|
ICON_MAP = {
|
|
"audio_connections": "mdi:speaker",
|
|
"battery_level": "mdi:battery",
|
|
"battery_temp": "mdi:thermometer",
|
|
"battery_voltage": "mdi:battery-charging-100",
|
|
"exposure_lock": "mdi:camera",
|
|
"ffc": "mdi:camera-front-variant",
|
|
"focus": "mdi:image-filter-center-focus",
|
|
"gps_active": "mdi:crosshairs-gps",
|
|
"light": "mdi:flashlight",
|
|
"motion": "mdi:run",
|
|
"night_vision": "mdi:weather-night",
|
|
"overlay": "mdi:monitor",
|
|
"pressure": "mdi:gauge",
|
|
"proximity": "mdi:map-marker-radius",
|
|
"quality": "mdi:quality-high",
|
|
"sound": "mdi:speaker",
|
|
"sound_event": "mdi:speaker",
|
|
"sound_timeout": "mdi:speaker",
|
|
"torch": "mdi:white-balance-sunny",
|
|
"video_chunk_len": "mdi:video",
|
|
"video_connections": "mdi:eye",
|
|
"video_recording": "mdi:record-rec",
|
|
"whitebalance_lock": "mdi:white-balance-auto",
|
|
}
|
|
|
|
SWITCHES = [
|
|
"exposure_lock",
|
|
"ffc",
|
|
"focus",
|
|
"gps_active",
|
|
"night_vision",
|
|
"overlay",
|
|
"torch",
|
|
"whitebalance_lock",
|
|
"video_recording",
|
|
]
|
|
|
|
SENSORS = [
|
|
"audio_connections",
|
|
"battery_level",
|
|
"battery_temp",
|
|
"battery_voltage",
|
|
"light",
|
|
"motion",
|
|
"pressure",
|
|
"proximity",
|
|
"sound",
|
|
"video_connections",
|
|
]
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.All(
|
|
cv.ensure_list,
|
|
[
|
|
vol.Schema(
|
|
{
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
vol.Required(CONF_HOST): cv.string,
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
vol.Optional(
|
|
CONF_TIMEOUT, default=DEFAULT_TIMEOUT
|
|
): cv.positive_int,
|
|
vol.Optional(
|
|
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
|
|
): cv.time_period,
|
|
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
|
|
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
|
|
vol.Optional(CONF_SWITCHES): vol.All(
|
|
cv.ensure_list, [vol.In(SWITCHES)]
|
|
),
|
|
vol.Optional(CONF_SENSORS): vol.All(
|
|
cv.ensure_list, [vol.In(SENSORS)]
|
|
),
|
|
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
|
|
}
|
|
)
|
|
],
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
@asyncio.coroutine
|
|
def async_setup(hass, config):
|
|
"""Set up the IP Webcam component."""
|
|
from pydroid_ipcam import PyDroidIPCam
|
|
|
|
webcams = hass.data[DATA_IP_WEBCAM] = {}
|
|
websession = async_get_clientsession(hass)
|
|
|
|
@asyncio.coroutine
|
|
def async_setup_ipcamera(cam_config):
|
|
"""Set up an IP camera."""
|
|
host = cam_config[CONF_HOST]
|
|
username = cam_config.get(CONF_USERNAME)
|
|
password = cam_config.get(CONF_PASSWORD)
|
|
name = cam_config[CONF_NAME]
|
|
interval = cam_config[CONF_SCAN_INTERVAL]
|
|
switches = cam_config.get(CONF_SWITCHES)
|
|
sensors = cam_config.get(CONF_SENSORS)
|
|
motion = cam_config.get(CONF_MOTION_SENSOR)
|
|
|
|
# Init ip webcam
|
|
cam = PyDroidIPCam(
|
|
hass.loop,
|
|
websession,
|
|
host,
|
|
cam_config[CONF_PORT],
|
|
username=username,
|
|
password=password,
|
|
timeout=cam_config[CONF_TIMEOUT],
|
|
)
|
|
|
|
if switches is None:
|
|
switches = [
|
|
setting for setting in cam.enabled_settings if setting in SWITCHES
|
|
]
|
|
|
|
if sensors is None:
|
|
sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS]
|
|
sensors.extend(["audio_connections", "video_connections"])
|
|
|
|
if motion is None:
|
|
motion = "motion_active" in cam.enabled_sensors
|
|
|
|
@asyncio.coroutine
|
|
def async_update_data(now):
|
|
"""Update data from IP camera in SCAN_INTERVAL."""
|
|
yield from cam.update()
|
|
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
|
|
|
async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval)
|
|
|
|
yield from async_update_data(None)
|
|
|
|
# Load platforms
|
|
webcams[host] = cam
|
|
|
|
mjpeg_camera = {
|
|
CONF_PLATFORM: "mjpeg",
|
|
CONF_MJPEG_URL: cam.mjpeg_url,
|
|
CONF_STILL_IMAGE_URL: cam.image_url,
|
|
CONF_NAME: name,
|
|
}
|
|
if username and password:
|
|
mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password})
|
|
|
|
hass.async_create_task(
|
|
discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config)
|
|
)
|
|
|
|
if sensors:
|
|
hass.async_create_task(
|
|
discovery.async_load_platform(
|
|
hass,
|
|
"sensor",
|
|
DOMAIN,
|
|
{CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors},
|
|
config,
|
|
)
|
|
)
|
|
|
|
if switches:
|
|
hass.async_create_task(
|
|
discovery.async_load_platform(
|
|
hass,
|
|
"switch",
|
|
DOMAIN,
|
|
{CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches},
|
|
config,
|
|
)
|
|
)
|
|
|
|
if motion:
|
|
hass.async_create_task(
|
|
discovery.async_load_platform(
|
|
hass,
|
|
"binary_sensor",
|
|
DOMAIN,
|
|
{CONF_HOST: host, CONF_NAME: name},
|
|
config,
|
|
)
|
|
)
|
|
|
|
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
|
if tasks:
|
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
|
|
|
return True
|
|
|
|
|
|
class AndroidIPCamEntity(Entity):
|
|
"""The Android device running IP Webcam."""
|
|
|
|
def __init__(self, host, ipcam):
|
|
"""Initialize the data object."""
|
|
self._host = host
|
|
self._ipcam = ipcam
|
|
|
|
@asyncio.coroutine
|
|
def async_added_to_hass(self):
|
|
"""Register update dispatcher."""
|
|
|
|
@callback
|
|
def async_ipcam_update(host):
|
|
"""Update callback."""
|
|
if self._host != host:
|
|
return
|
|
self.async_schedule_update_ha_state(True)
|
|
|
|
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return True if entity has to be polled for state."""
|
|
return False
|
|
|
|
@property
|
|
def available(self):
|
|
"""Return True if entity is available."""
|
|
return self._ipcam.available
|
|
|
|
@property
|
|
def device_state_attributes(self):
|
|
"""Return the state attributes."""
|
|
state_attr = {ATTR_HOST: self._host}
|
|
if self._ipcam.status_data is None:
|
|
return state_attr
|
|
|
|
state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections")
|
|
state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections")
|
|
|
|
return state_attr
|