From 44311193ef4d9b746e0b53ad27c357ea24bf1016 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Feb 2017 17:29:05 -0800 Subject: [PATCH] Add config component and hassbian example panel (#5868) * Add hassbian panel * Rename to generic config panel * Allow loading hassbian as test * Add tests * Update frontend * Lint * Lint --- homeassistant/components/config/__init__.py | 28 +++++ homeassistant/components/config/hassbian.py | 118 ++++++++++++++++++ homeassistant/components/frontend/__init__.py | 15 ++- homeassistant/components/frontend/version.py | 1 + .../www_static/home-assistant-polymer | 2 +- .../www_static/panels/ha-panel-config.html | 22 ++++ .../www_static/panels/ha-panel-config.html.gz | Bin 0 -> 3470 bytes tests/common.py | 7 +- tests/components/config/__init__.py | 1 + tests/components/config/test_hassbian.py | 70 +++++++++++ tests/components/config/test_init.py | 18 +++ 11 files changed, 273 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/config/__init__.py create mode 100644 homeassistant/components/config/hassbian.py create mode 100644 homeassistant/components/frontend/www_static/panels/ha-panel-config.html create mode 100644 homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz create mode 100644 tests/components/config/__init__.py create mode 100644 tests/components/config/test_hassbian.py create mode 100644 tests/components/config/test_init.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py new file mode 100644 index 00000000000..dc8946776f5 --- /dev/null +++ b/homeassistant/components/config/__init__.py @@ -0,0 +1,28 @@ +"""Component to interact with Hassbian tools.""" +import asyncio + +from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.components.frontend import register_built_in_panel + +DOMAIN = 'config' +DEPENDENCIES = ['http'] + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the hassbian component.""" + register_built_in_panel(hass, 'config', 'Configuration', 'mdi:settings') + + for panel_name in ('hassbian',): + panel = yield from async_prepare_setup_platform(hass, config, DOMAIN, + panel_name) + + if not panel: + continue + + success = yield from panel.async_setup(hass) + + if success: + hass.config.components.add('{}.{}'.format(DOMAIN, panel_name)) + + return True diff --git a/homeassistant/components/config/hassbian.py b/homeassistant/components/config/hassbian.py new file mode 100644 index 00000000000..c90583c5278 --- /dev/null +++ b/homeassistant/components/config/hassbian.py @@ -0,0 +1,118 @@ +"""Component to interact with Hassbian tools.""" +import asyncio +import json +import os + +from homeassistant.components.http import HomeAssistantView + + +_TEST_OUTPUT = """ +{ + "suites": [ + { + "openzwave": [ + { + "state": "installed" + }, + { + "description": "This is the description of the Open Z-Wave suite." + } + ] + }, + { + "openelec": [ + { + "state": "not_installed" + }, + { + "description": + "OpenElec is amazing. It allows you to control the TV." + } + ] + }, + { + "mosquitto": [ + { + "state": "installing" + }, + { + "description": + "Mosquitto is an MQTT broker." + } + ] + } + ] +} +""" + + +@asyncio.coroutine +def async_setup(hass): + """Setup the hassbian config.""" + # Test if is hassbian + test_mode = 'FORCE_HASSBIAN' in os.environ + is_hassbian = test_mode + + if not is_hassbian: + return False + + hass.http.register_view(HassbianSuitesView(test_mode)) + hass.http.register_view(HassbianSuiteInstallView(test_mode)) + + return True + + +@asyncio.coroutine +def hassbian_status(hass, test_mode=False): + """Query for the Hassbian status.""" + # fetch real output when not in test mode + if test_mode: + return json.loads(_TEST_OUTPUT) + + raise Exception('Real mode not implemented yet.') + + +class HassbianSuitesView(HomeAssistantView): + """Hassbian packages endpoint.""" + + url = '/api/config/hassbian/suites' + name = 'api:config:hassbian:suites' + + def __init__(self, test_mode): + """Initialize suites view.""" + self._test_mode = test_mode + + @asyncio.coroutine + def get(self, request): + """Request suite status.""" + inp = yield from hassbian_status(request.app['hass'], self._test_mode) + + # Flatten the structure a bit + suites = {} + + for suite in inp['suites']: + key = next(iter(suite)) + info = suites[key] = {} + + for item in suite[key]: + item_key = next(iter(item)) + info[item_key] = item[item_key] + + return self.json(suites) + + +class HassbianSuiteInstallView(HomeAssistantView): + """Hassbian packages endpoint.""" + + url = '/api/config/hassbian/suites/{suite}/install' + name = 'api:config:hassbian:suite' + + def __init__(self, test_mode): + """Initialize suite view.""" + self._test_mode = test_mode + + @asyncio.coroutine + def post(self, request, suite): + """Request suite status.""" + # do real install if not in test mode + return self.json({"status": "ok"}) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4d9fb8624d8..1b4602d35b4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -19,7 +19,7 @@ DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api'] URL_PANEL_COMPONENT = '/frontend/panels/{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' -STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') +STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/') MANIFEST_JSON = { "background_color": "#FFFFFF", "description": "Open-source home automation platform running on Python 3.", @@ -51,17 +51,22 @@ _LOGGER = logging.getLogger(__name__) def register_built_in_panel(hass, component_name, sidebar_title=None, sidebar_icon=None, url_path=None, config=None): """Register a built-in panel.""" - path = 'panels/ha-panel-{}.html'.format(component_name) + nondev_path = 'panels/ha-panel-{}.html'.format(component_name) if hass.http.development: url = ('/static/home-assistant-polymer/panels/' '{0}/ha-panel-{0}.html'.format(component_name)) + path = os.path.join( + STATIC_PATH, 'home-assistant-polymer/panels/', + '{0}/ha-panel-{0}.html'.format(component_name)) else: url = None # use default url generate mechanism + path = os.path.join(STATIC_PATH, nondev_path) - register_panel(hass, component_name, os.path.join(STATIC_PATH, path), - FINGERPRINTS[path], sidebar_title, sidebar_icon, url_path, - url, config) + # Fingerprint doesn't exist when adding new built-in panel + register_panel(hass, component_name, path, + FINGERPRINTS.get(nondev_path, 'dev'), sidebar_title, + sidebar_icon, url_path, url, config) def register_panel(hass, component_name, path, md5=None, sidebar_title=None, diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index a186e440f50..1832564b98f 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -5,6 +5,7 @@ FINGERPRINTS = { "frontend.html": "cfd75c944ab14912cfbb4fdd9027579c", "mdi.html": "c1dde43ccf5667f687c418fc8daf9668", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", + "panels/ha-panel-config.html": "fb50a25da4b4cfbb0d9a74bb22a31db9", "panels/ha-panel-dev-event.html": "5c82300b3cf543a92cf4297506e450e7", "panels/ha-panel-dev-info.html": "0469024d94d6270a8680df2be44ba916", "panels/ha-panel-dev-service.html": "9f749635e518a4ca7991975bdefdb10a", diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 518d0b38da4..ba48e40a2bb 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 518d0b38da47fa5923c84a963d11e9ed59424c2f +Subproject commit ba48e40a2bba273b1cdcdf637ecbef4f4db14284 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html new file mode 100644 index 00000000000..aa49194bfbc --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz new file mode 100644 index 0000000000000000000000000000000000000000..0680d628cc006a71aa2214448149a94c7f0013d5 GIT binary patch literal 3470 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 literal 0 HcmV?d00001 diff --git a/tests/common.py b/tests/common.py index 70afae75155..5527a26b63e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -210,9 +210,9 @@ def mock_state_change_event(hass, new_state, old_state=None): hass.bus.fire(EVENT_STATE_CHANGED, event_data) -def mock_http_component(hass): +def mock_http_component(hass, api_password=None): """Mock the HTTP component.""" - hass.http = MagicMock() + hass.http = MagicMock(api_password=api_password) hass.config.components.add('http') hass.http.views = {} @@ -229,7 +229,8 @@ def mock_http_component(hass): def mock_http_component_app(hass, api_password=None): """Create an aiohttp.web.Application instance for testing.""" - hass.http = MagicMock(api_password=api_password) + if 'http' not in hass.config.components: + mock_http_component(hass, api_password) app = web.Application(middlewares=[auth_middleware], loop=hass.loop) app['hass'] = hass app[KEY_USE_X_FORWARDED_FOR] = False diff --git a/tests/components/config/__init__.py b/tests/components/config/__init__.py new file mode 100644 index 00000000000..53629c7e8f7 --- /dev/null +++ b/tests/components/config/__init__.py @@ -0,0 +1 @@ +"""Tests for the config component.""" diff --git a/tests/components/config/test_hassbian.py b/tests/components/config/test_hassbian.py new file mode 100644 index 00000000000..5ed48fa7794 --- /dev/null +++ b/tests/components/config/test_hassbian.py @@ -0,0 +1,70 @@ +"""Test hassbian config.""" +import asyncio +import os +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components.config.hassbian import ( + HassbianSuitesView, HassbianSuiteInstallView) +from tests.common import ( + mock_http_component, mock_http_component_app) + + +def test_setup_check_env_prevents_load(hass, loop): + """Test it does not set up hassbian if environment var not present.""" + mock_http_component(hass) + with patch.dict(os.environ, clear=True): + loop.run_until_complete(async_setup_component(hass, 'config', {})) + assert 'config' in hass.config.components + assert HassbianSuitesView.name not in hass.http.views + assert HassbianSuiteInstallView.name not in hass.http.views + + +def test_setup_check_env_works(hass, loop): + """Test it sets up hassbian if environment var present.""" + mock_http_component(hass) + with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}): + loop.run_until_complete(async_setup_component(hass, 'config', {})) + assert 'config' in hass.config.components + assert HassbianSuitesView.name in hass.http.views + assert HassbianSuiteInstallView.name in hass.http.views + + +@asyncio.coroutine +def test_get_suites(hass, test_client): + """Test getting suites.""" + app = mock_http_component_app(hass) + + with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[HassbianSuitesView.name].register(app.router) + + client = yield from test_client(app) + resp = yield from client.get('/api/config/hassbian/suites') + assert resp.status == 200 + result = yield from resp.json() + + assert 'mosquitto' in result + info = result['mosquitto'] + assert info['state'] == 'installing' + assert info['description'] == 'Mosquitto is an MQTT broker.' + + +@asyncio.coroutine +def test_install_suite(hass, test_client): + """Test getting suites.""" + app = mock_http_component_app(hass) + + with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}): + yield from async_setup_component(hass, 'config', {}) + + hass.http.views[HassbianSuiteInstallView.name].register(app.router) + + client = yield from test_client(app) + resp = yield from client.post( + '/api/config/hassbian/suites/openzwave/install') + assert resp.status == 200 + result = yield from resp.json() + + assert result == {"status": "ok"} diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py new file mode 100644 index 00000000000..1194c6c2b3d --- /dev/null +++ b/tests/components/config/test_init.py @@ -0,0 +1,18 @@ +"""Test config init.""" +import pytest + +from homeassistant.bootstrap import async_setup_component + +from tests.common import mock_http_component + + +@pytest.fixture(autouse=True) +def stub_http(hass): + """Stub the HTTP component.""" + mock_http_component(hass) + + +def test_config_setup(hass, loop): + """Test it sets up hassbian.""" + loop.run_until_complete(async_setup_component(hass, 'config', {})) + assert 'config' in hass.config.components