diff --git a/.coveragerc b/.coveragerc index a574c2ead43..de778888097 100644 --- a/.coveragerc +++ b/.coveragerc @@ -451,6 +451,7 @@ omit = homeassistant/components/proxy/camera.py homeassistant/components/ps4/__init__.py homeassistant/components/ps4/media_player.py + homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9f5b83bf941..6d27c8563fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -175,6 +175,7 @@ homeassistant/components/pi_hole/* @fabaff homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 +homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/qnap/* @colinodell diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3959eb88035..d63caf9e76f 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log' # hass.data key for logging information. DATA_LOGGING = 'logging' +DEBUGGER_INTEGRATIONS = {'ptvsd', } CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification') LOGGING_INTEGRATIONS = {'logger', 'system_log'} STAGE_1_INTEGRATIONS = { @@ -306,6 +307,15 @@ async def _async_set_up_integrations( """Set up all the integrations.""" domains = _get_domains(hass, config) + # Start up debuggers. Start these first in case they want to wait. + debuggers = domains & DEBUGGER_INTEGRATIONS + if debuggers: + _LOGGER.debug("Starting up debuggers %s", debuggers) + await asyncio.gather(*[ + async_setup_component(hass, domain, config) + for domain in debuggers]) + domains -= DEBUGGER_INTEGRATIONS + # Resolve all dependencies of all components so we can find the logging # and integrations that need faster initialization. resolved_domains_task = asyncio.gather(*[ @@ -339,7 +349,7 @@ async def _async_set_up_integrations( stage_2_domains = domains - logging_domains - stage_1_domains if logging_domains: - _LOGGER.debug("Setting up %s", logging_domains) + _LOGGER.info("Setting up %s", logging_domains) await asyncio.gather(*[ async_setup_component(hass, domain, config) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py new file mode 100644 index 00000000000..2a86e15ddd2 --- /dev/null +++ b/homeassistant/components/ptvsd/__init__.py @@ -0,0 +1,63 @@ +""" +Enable ptvsd debugger to attach to HA. + +Attach ptvsd debugger by default to port 5678. +""" + +import logging +from threading import Thread +from asyncio import Event + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +DOMAIN = 'ptvsd' + +CONF_WAIT = 'wait' + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional( + CONF_HOST, default='0.0.0.0' + ): cv.string, + vol.Optional( + CONF_PORT, default=5678 + ): cv.port, + vol.Optional( + CONF_WAIT, default=False + ): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up ptvsd debugger.""" + import ptvsd + + conf = config[DOMAIN] + host = conf[CONF_HOST] + port = conf[CONF_PORT] + + ptvsd.enable_attach((host, port)) + + wait = conf[CONF_WAIT] + if wait: + _LOGGER.warning("Waiting for ptvsd connection on %s:%s", host, port) + ready = Event() + + def waitfor(): + ptvsd.wait_for_attach() + hass.loop.call_soon_threadsafe(ready.set) + Thread(target=waitfor).start() + + await ready.wait() + else: + _LOGGER.warning("Listening for ptvsd connection on %s:%s", host, port) + + return True diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json new file mode 100644 index 00000000000..8bd46c3dc32 --- /dev/null +++ b/homeassistant/components/ptvsd/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ptvsd", + "name": "ptvsd", + "documentation": "https://www.home-assistant.io/components/ptvsd", + "requirements": [ + "ptvsd==4.2.8" + ], + "dependencies": [], + "codeowners": ["@swamp-ig"] +} diff --git a/requirements_all.txt b/requirements_all.txt index b4f78ab24da..a95ce69ae2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -884,6 +884,9 @@ protobuf==3.6.1 # homeassistant.components.systemmonitor psutil==5.6.1 +# homeassistant.components.ptvsd +ptvsd==4.2.8 + # homeassistant.components.wink pubnubsub-handler==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 402647172e3..b22a3be31b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,6 +204,9 @@ pmsensor==0.4 # homeassistant.components.prometheus prometheus_client==0.2.0 +# homeassistant.components.ptvsd +ptvsd==4.2.8 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dc8ed652d11..ad0a833436e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -94,6 +94,7 @@ TEST_REQUIREMENTS = ( 'pilight', 'pmsensor', 'prometheus_client', + 'ptvsd', 'pushbullet.py', 'py-canary', 'pyblackbird', diff --git a/tests/components/ptvsd/__init__py b/tests/components/ptvsd/__init__py new file mode 100644 index 00000000000..e2a1a9ba0a6 --- /dev/null +++ b/tests/components/ptvsd/__init__py @@ -0,0 +1 @@ +"""Tests for PTVSD Debugger""" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py new file mode 100644 index 00000000000..169ab8fb97a --- /dev/null +++ b/tests/components/ptvsd/test_ptvsd.py @@ -0,0 +1,56 @@ +"""Tests for PTVSD Debugger.""" + +from unittest.mock import patch +from asynctest import CoroutineMock +from pytest import mark + +import homeassistant.components.ptvsd as ptvsd_component +from homeassistant.setup import async_setup_component +from homeassistant.bootstrap import _async_set_up_integrations + + +@mark.skip('causes code cover to fail') +async def test_ptvsd(hass): + """Test loading ptvsd component.""" + with patch('ptvsd.enable_attach') as attach: + with patch('ptvsd.wait_for_attach') as wait: + assert await async_setup_component( + hass, ptvsd_component.DOMAIN, { + ptvsd_component.DOMAIN: {} + }) + + attach.assert_called_once_with(('0.0.0.0', 5678)) + assert wait.call_count == 0 + + +@mark.skip('causes code cover to fail') +async def test_ptvsd_wait(hass): + """Test loading ptvsd component with wait.""" + with patch('ptvsd.enable_attach') as attach: + with patch('ptvsd.wait_for_attach') as wait: + assert await async_setup_component( + hass, ptvsd_component.DOMAIN, { + ptvsd_component.DOMAIN: { + ptvsd_component.CONF_WAIT: True + } + }) + + attach.assert_called_once_with(('0.0.0.0', 5678)) + assert wait.call_count == 1 + + +async def test_ptvsd_bootstrap(hass): + """Test loading ptvsd component with wait.""" + config = { + ptvsd_component.DOMAIN: { + ptvsd_component.CONF_WAIT: True + } + } + + with patch( + 'homeassistant.components.ptvsd.async_setup', + CoroutineMock()) as setup_mock: + setup_mock.return_value = True + await _async_set_up_integrations(hass, config) + + assert setup_mock.call_count == 1