diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index 818a6b5b387..9da14048705 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -4,8 +4,9 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg/ """ +import asyncio import logging -from os import path +import os import voluptuous as vol @@ -13,17 +14,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA, DOMAIN) from homeassistant.components.ffmpeg import ( - get_binary, run_test, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) + DATA_FFMPEG, CONF_INPUT, CONF_OUTPUT, CONF_EXTRA_ARGUMENTS) from homeassistant.config import load_yaml_config_file -from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME, - ATTR_ENTITY_ID) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, CONF_NAME, + ATTR_ENTITY_ID) DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) +SERVICE_START = 'ffmpeg_start' +SERVICE_STOP = 'ffmpeg_stop' SERVICE_RESTART = 'ffmpeg_restart' +DATA_FFMPEG_DEVICE = 'ffmpeg_binary_sensor' + FFMPEG_SENSOR_NOISE = 'noise' FFMPEG_SENSOR_MOTION = 'motion' @@ -32,6 +38,7 @@ MAP_FFMPEG_BIN = [ FFMPEG_SENSOR_MOTION ] +CONF_INITIAL_STATE = 'initial_state' CONF_TOOL = 'tool' CONF_PEAK = 'peak' CONF_DURATION = 'duration' @@ -41,10 +48,12 @@ CONF_REPEAT = 'repeat' CONF_REPEAT_TIME = 'repeat_time' DEFAULT_NAME = 'FFmpeg' +DEFAULT_INIT_STATE = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOOL): vol.In(MAP_FFMPEG_BIN), vol.Required(CONF_INPUT): cv.string, + vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string, vol.Optional(CONF_OUTPUT): cv.string, @@ -61,7 +70,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=0)), }) -SERVICE_RESTART_SCHEMA = vol.Schema({ +SERVICE_FFMPEG_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -72,86 +81,125 @@ def restart(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_RESTART, data) -# list of all ffmpeg sensors -DEVICES = [] - - -def setup_platform(hass, config, add_entities, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Create the binary sensor.""" from haffmpeg import SensorNoise, SensorMotion # check source - if not run_test(hass, config.get(CONF_INPUT)): + if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return # generate sensor object if config.get(CONF_TOOL) == FFMPEG_SENSOR_NOISE: - entity = FFmpegNoise(SensorNoise, config) + entity = FFmpegNoise(hass, SensorNoise, config) else: - entity = FFmpegMotion(SensorMotion, config) + entity = FFmpegMotion(hass, SensorMotion, config) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, entity.shutdown_ffmpeg) + @asyncio.coroutine + def async_shutdown(event): + """Stop ffmpeg.""" + yield from entity.async_shutdown_ffmpeg() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_shutdown) + + # start on startup + if config.get(CONF_INITIAL_STATE): + @asyncio.coroutine + def async_start(event): + """Start ffmpeg.""" + yield from entity.async_start_ffmpeg() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_start) # add to system - add_entities([entity]) - DEVICES.append(entity) + yield from async_add_devices([entity]) # exists service? if hass.services.has_service(DOMAIN, SERVICE_RESTART): + hass.data[DATA_FFMPEG_DEVICE].append(entity) return + hass.data[DATA_FFMPEG_DEVICE] = [entity] - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml')) # register service - def _service_handle_restart(service): + @asyncio.coroutine + def async_service_handle(service): """Handle service binary_sensor.ffmpeg_restart.""" entity_ids = service.data.get('entity_id') if entity_ids: - _devices = [device for device in DEVICES + _devices = [device for device in hass.data[DATA_FFMPEG_DEVICE] if device.entity_id in entity_ids] else: - _devices = DEVICES + _devices = hass.data[DATA_FFMPEG_DEVICE] + tasks = [] for device in _devices: - device.restart_ffmpeg() + if service.service == SERVICE_START: + tasks.append(device.async_start_ffmpeg()) + elif service.service == SERVICE_STOP: + tasks.append(device.async_shutdown_ffmpeg()) + else: + tasks.append(device.async_restart_ffmpeg()) - hass.services.register(DOMAIN, SERVICE_RESTART, - _service_handle_restart, - descriptions.get(SERVICE_RESTART), - schema=SERVICE_RESTART_SCHEMA) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register( + DOMAIN, SERVICE_START, async_service_handle, + descriptions.get(SERVICE_START), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_STOP, async_service_handle, + descriptions.get(SERVICE_STOP), schema=SERVICE_FFMPEG_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_RESTART, async_service_handle, + descriptions.get(SERVICE_RESTART), schema=SERVICE_FFMPEG_SCHEMA) class FFmpegBinarySensor(BinarySensorDevice): """A binary sensor which use ffmpeg for noise detection.""" - def __init__(self, ffobj, config): + def __init__(self, hass, ffobj, config): """Constructor for binary sensor noise detection.""" + self._manager = hass.data[DATA_FFMPEG] self._state = False self._config = config self._name = config.get(CONF_NAME) - self._ffmpeg = ffobj(get_binary(), self._callback) + self._ffmpeg = ffobj( + self._manager.binary, hass.loop, self._async_callback) - self._start_ffmpeg(config) - - def _callback(self, state): + def _async_callback(self, state): """HA-FFmpeg callback for noise detection.""" self._state = state - self.schedule_update_ha_state() + self.hass.async_add_job(self.async_update_ha_state()) - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" - raise NotImplementedError + def async_start_ffmpeg(self): + """Start a FFmpeg instance. - def shutdown_ffmpeg(self, event): - """For STOP event to shutdown ffmpeg.""" - self._ffmpeg.close() + This method must be run in the event loop and returns a coroutine. + """ + raise NotImplementedError() - def restart_ffmpeg(self): - """Restart ffmpeg with new config.""" - self._ffmpeg.close() - self._start_ffmpeg(self._config) + def async_shutdown_ffmpeg(self): + """For STOP event to shutdown ffmpeg. + + This method must be run in the event loop and returns a coroutine. + """ + return self._ffmpeg.close() + + @asyncio.coroutine + def async_restart_ffmpeg(self): + """Restart processing.""" + yield from self.async_shutdown_ffmpeg() + yield from self.async_start_ffmpeg() @property def is_on(self): @@ -177,20 +225,23 @@ class FFmpegBinarySensor(BinarySensorDevice): class FFmpegNoise(FFmpegBinarySensor): """A binary sensor which use ffmpeg for noise detection.""" - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ # init config self._ffmpeg.set_options( - time_duration=config.get(CONF_DURATION), - time_reset=config.get(CONF_RESET), - peak=config.get(CONF_PEAK), + time_duration=self._config.get(CONF_DURATION), + time_reset=self._config.get(CONF_RESET), + peak=self._config.get(CONF_PEAK), ) # run - self._ffmpeg.open_sensor( - input_source=config.get(CONF_INPUT), - output_dest=config.get(CONF_OUTPUT), - extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), + return self._ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + output_dest=self._config.get(CONF_OUTPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) @property @@ -202,20 +253,23 @@ class FFmpegNoise(FFmpegBinarySensor): class FFmpegMotion(FFmpegBinarySensor): """A binary sensor which use ffmpeg for noise detection.""" - def _start_ffmpeg(self, config): - """Start a FFmpeg instance.""" + def async_start_ffmpeg(self): + """Start a FFmpeg instance. + + This method must be run in the event loop and returns a coroutine. + """ # init config self._ffmpeg.set_options( - time_reset=config.get(CONF_RESET), - time_repeat=config.get(CONF_REPEAT_TIME), - repeat=config.get(CONF_REPEAT), - changes=config.get(CONF_CHANGES), + time_reset=self._config.get(CONF_RESET), + time_repeat=self._config.get(CONF_REPEAT_TIME), + repeat=self._config.get(CONF_REPEAT), + changes=self._config.get(CONF_CHANGES), ) # run - self._ffmpeg.open_sensor( - input_source=config.get(CONF_INPUT), - extra_cmd=config.get(CONF_EXTRA_ARGUMENTS), + return self._ffmpeg.open_sensor( + input_source=self._config.get(CONF_INPUT), + extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) @property diff --git a/homeassistant/components/binary_sensor/services.yaml b/homeassistant/components/binary_sensor/services.yaml index 9be9915e268..a1ac8cf8b5d 100644 --- a/homeassistant/components/binary_sensor/services.yaml +++ b/homeassistant/components/binary_sensor/services.yaml @@ -1,7 +1,23 @@ # Describes the format for available binary_sensor services +ffmpeg_start: + description: Send a start command to a ffmpeg based sensor. + + fields: + entity_id: + description: Name(s) of entites that will start. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + +ffmpeg_stop: + description: Send a stop command to a ffmpeg based sensor. + + fields: + entity_id: + description: Name(s) of entites that will stop. Platform dependent. + example: 'binary_sensor.ffmpeg_noise' + ffmpeg_restart: - description: Send a restart command to a ffmpeg based sensor (party mode). + description: Send a restart command to a ffmpeg based sensor. fields: entity_id: diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 0b8d60ab7f5..6b00ae240ed 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -12,10 +12,9 @@ from aiohttp import web from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( - async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) + DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME -from homeassistant.util.async import run_coroutine_threadsafe DEPENDENCIES = ['ffmpeg'] @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" - if not async_run_test(hass, config.get(CONF_INPUT)): + if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return yield from async_add_devices([FFmpegCamera(hass, config)]) @@ -44,20 +43,17 @@ class FFmpegCamera(Camera): def __init__(self, hass, config): """Initialize a FFmpeg camera.""" super().__init__() + + self._manager = hass.data[DATA_FFMPEG] self._name = config.get(CONF_NAME) self._input = config.get(CONF_INPUT) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) - def camera_image(self): - """Return bytes of camera image.""" - return run_coroutine_threadsafe( - self.async_camera_image(), self.hass.loop).result() - @asyncio.coroutine def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg import ImageSingleAsync, IMAGE_JPEG - ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop) + from haffmpeg import ImageFrame, IMAGE_JPEG + ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) image = yield from ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, @@ -67,9 +63,9 @@ class FFmpegCamera(Camera): @asyncio.coroutine def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg import CameraMjpegAsync + from haffmpeg import CameraMjpeg - stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop) + stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) yield from stream.open_camera( self._input, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index f345153e666..56e1cb8c95d 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -10,13 +10,14 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==0.15"] +REQUIREMENTS = ["ha-ffmpeg==1.0"] _LOGGER = logging.getLogger(__name__) +DATA_FFMPEG = 'ffmpeg' + CONF_INPUT = 'input' CONF_FFMPEG_BIN = 'ffmpeg_bin' CONF_EXTRA_ARGUMENTS = 'extra_arguments' @@ -34,53 +35,54 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -FFMPEG_CONFIG = { - CONF_FFMPEG_BIN: DEFAULT_BINARY, - CONF_RUN_TEST: DEFAULT_RUN_TEST, -} -FFMPEG_TEST_CACHE = {} - - -def setup(hass, config): - """Setup the FFmpeg component.""" - if DOMAIN in config: - FFMPEG_CONFIG.update(config.get(DOMAIN)) - return True - - -def get_binary(): - """Return ffmpeg binary from config. - - Async friendly. - """ - return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) - - -def run_test(hass, input_source): - """Run test on this input. TRUE is deactivate or run correct.""" - return run_coroutine_threadsafe( - async_run_test(hass, input_source), hass.loop).result() - - @asyncio.coroutine -def async_run_test(hass, input_source): - """Run test on this input. TRUE is deactivate or run correct. +def async_setup(hass, config): + """Setup the FFmpeg component.""" + conf = config.get(DOMAIN, {}) - This method must be run in the event loop. - """ - from haffmpeg import TestAsync + hass.data[DATA_FFMPEG] = FFmpegManager( + hass, + conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY), + conf.get(CONF_RUN_TEST, DEFAULT_RUN_TEST) + ) - if FFMPEG_CONFIG.get(CONF_RUN_TEST): - # if in cache - if input_source in FFMPEG_TEST_CACHE: - return FFMPEG_TEST_CACHE[input_source] - - # run test - ffmpeg_test = TestAsync(get_binary(), loop=hass.loop) - success = yield from ffmpeg_test.run_test(input_source) - if not success: - _LOGGER.error("FFmpeg '%s' test fails!", input_source) - FFMPEG_TEST_CACHE[input_source] = False - return False - FFMPEG_TEST_CACHE[input_source] = True return True + + +class FFmpegManager(object): + """Helper for ha-ffmpeg.""" + + def __init__(self, hass, ffmpeg_bin, run_test): + """Initialize helper.""" + self.hass = hass + self._cache = {} + self._bin = ffmpeg_bin + self._run_test = run_test + + @property + def binary(self): + """Return ffmpeg binary from config.""" + return self._bin + + @asyncio.coroutine + def async_run_test(self, input_source): + """Run test on this input. TRUE is deactivate or run correct. + + This method must be run in the event loop. + """ + from haffmpeg import Test + + if self._run_test: + # if in cache + if input_source in self._cache: + return self._cache[input_source] + + # run test + ffmpeg_test = Test(self.binary, loop=self.hass.loop) + success = yield from ffmpeg_test.run_test(input_source) + if not success: + _LOGGER.error("FFmpeg '%s' test fails!", input_source) + self._cache[input_source] = False + return False + self._cache[input_source] = True + return True diff --git a/requirements_all.txt b/requirements_all.txt index df8dc7e77b9..26300090067 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,7 +170,7 @@ googlemaps==2.4.4 gps3==0.33.3 # homeassistant.components.ffmpeg -ha-ffmpeg==0.15 +ha-ffmpeg==1.0 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.1