mirror of
				https://github.com/home-assistant/core.git
				synced 2025-10-31 06:29:31 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			294 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			294 lines
		
	
	
		
			9.6 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, default=None):
 | |
|             vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
 | |
|         vol.Optional(CONF_SENSORS, default=None):
 | |
|             vol.All(cv.ensure_list, [vol.In(SENSORS)]),
 | |
|         vol.Optional(CONF_MOTION_SENSOR, default=None): 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[CONF_SWITCHES]
 | |
|         sensors = cam_config[CONF_SENSORS]
 | |
|         motion = cam_config[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_add_job(discovery.async_load_platform(
 | |
|             hass, 'camera', 'mjpeg', mjpeg_camera, config))
 | |
| 
 | |
|         if sensors:
 | |
|             hass.async_add_job(discovery.async_load_platform(
 | |
|                 hass, 'sensor', DOMAIN, {
 | |
|                     CONF_NAME: name,
 | |
|                     CONF_HOST: host,
 | |
|                     CONF_SENSORS: sensors,
 | |
|                 }, config))
 | |
| 
 | |
|         if switches:
 | |
|             hass.async_add_job(discovery.async_load_platform(
 | |
|                 hass, 'switch', DOMAIN, {
 | |
|                     CONF_NAME: name,
 | |
|                     CONF_HOST: host,
 | |
|                     CONF_SWITCHES: switches,
 | |
|                 }, config))
 | |
| 
 | |
|         if motion:
 | |
|             hass.async_add_job(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 oject."""
 | |
|         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.hass.async_add_job(self.async_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
 | 
