Skip to content

Commit

Permalink
Initial import for HassIO (#6935)
Browse files Browse the repository at this point in the history
* Initial import for HassIO

* Cleanup api code for views

* First unittest for view

* Add test for edit view

* Finish unittest

* fix addons test

* cleanup service.yaml

* Address first round with ping command

* handle timeout dynamic

* fix lint
  • Loading branch information
pvizeli authored and balloob committed Apr 7, 2017
1 parent 74ac160 commit 3e66df5
Show file tree
Hide file tree
Showing 4 changed files with 887 additions and 1 deletion.
272 changes: 272 additions & 0 deletions homeassistant/components/hassio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
"""
Exposes regular rest commands as services.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hassio/
"""
import asyncio
import logging
import os

import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPBadGateway
import async_timeout
import voluptuous as vol

from homeassistant.config import load_yaml_config_file
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv

DOMAIN = 'hassio'
DEPENDENCIES = ['http']

_LOGGER = logging.getLogger(__name__)

LONG_TASK_TIMEOUT = 900
DEFAULT_TIMEOUT = 10

SERVICE_HOST_SHUTDOWN = 'host_shutdown'
SERVICE_HOST_REBOOT = 'host_reboot'

SERVICE_HOST_UPDATE = 'host_update'
SERVICE_SUPERVISOR_UPDATE = 'supervisor_update'
SERVICE_HOMEASSISTANT_UPDATE = 'homeassistant_update'

SERVICE_ADDON_INSTALL = 'addon_install'
SERVICE_ADDON_UNINSTALL = 'addon_uninstall'
SERVICE_ADDON_UPDATE = 'addon_update'
SERVICE_ADDON_START = 'addon_start'
SERVICE_ADDON_STOP = 'addon_stop'

ATTR_ADDON = 'addon'
ATTR_VERSION = 'version'


SCHEMA_SERVICE_UPDATE = vol.Schema({
vol.Optional(ATTR_VERSION): cv.string,
})

SCHEMA_SERVICE_ADDONS = vol.Schema({
vol.Required(ATTR_ADDON): cv.slug,
})

SCHEMA_SERVICE_ADDONS_VERSION = SCHEMA_SERVICE_ADDONS.extend({
vol.Optional(ATTR_VERSION): cv.string,
})


SERVICE_MAP = {
SERVICE_HOST_SHUTDOWN: None,
SERVICE_HOST_REBOOT: None,
SERVICE_HOST_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_SUPERVISOR_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_HOMEASSISTANT_UPDATE: SCHEMA_SERVICE_UPDATE,
SERVICE_ADDON_INSTALL: SCHEMA_SERVICE_ADDONS_VERSION,
SERVICE_ADDON_UNINSTALL: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_START: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_STOP: SCHEMA_SERVICE_ADDONS,
SERVICE_ADDON_UPDATE: SCHEMA_SERVICE_ADDONS_VERSION,
}


@asyncio.coroutine
def async_setup(hass, config):
"""Setup the hassio component."""
try:
host = os.environ['HASSIO']
except KeyError:
_LOGGER.error("No HassIO supervisor detect!")
return False

websession = async_get_clientsession(hass)
hassio = HassIO(hass.loop, websession, host)

api_ok = yield from hassio.is_connected()
if not api_ok:
_LOGGER.error("Not connected with HassIO!")
return False

# register base api views
for base in ('host', 'homeassistant'):
hass.http.register_view(HassIOBaseView(hassio, base))
for base in ('supervisor', 'network'):
hass.http.register_view(HassIOBaseEditView(hassio, base))

# register view for addons
hass.http.register_view(HassIOAddonsView(hassio))

@asyncio.coroutine
def async_service_handler(service):
"""Handle HassIO service calls."""
addon = service.data.get(ATTR_ADDON)
if ATTR_VERSION in service.data:
version = {ATTR_VERSION: service.data[ATTR_VERSION]}
else:
version = None

# map to api call
if service.service == SERVICE_HOST_UPDATE:
yield from hassio.send_command(
"/host/update", payload=version)
elif service.service == SERVICE_HOST_REBOOT:
yield from hassio.send_command("/host/reboot")
elif service.service == SERVICE_HOST_SHUTDOWN:
yield from hassio.send_command("/host/shutdown")
elif service.service == SERVICE_SUPERVISOR_UPDATE:
yield from hassio.send_command(
"/supervisor/update", payload=version)
elif service.service == SERVICE_HOMEASSISTANT_UPDATE:
yield from hassio.send_command(
"/homeassistant/update", payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_INSTALL:
yield from hassio.send_command(
"/addons/{}/install".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)
elif service.service == SERVICE_ADDON_UNINSTALL:
yield from hassio.send_command(
"/addons/{}/uninstall".format(addon))
elif service.service == SERVICE_ADDON_START:
yield from hassio.send_command("/addons/{}/start".format(addon))
elif service.service == SERVICE_ADDON_STOP:
yield from hassio.send_command("/addons/{}/stop".format(addon))
elif service.service == SERVICE_ADDON_UPDATE:
yield from hassio.send_command(
"/addons/{}/update".format(addon), payload=version,
timeout=LONG_TASK_TIMEOUT)

descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))

for service, schema in SERVICE_MAP.items():
hass.services.async_register(
DOMAIN, service, async_service_handler,
descriptions[DOMAIN][service], schema=schema)

return True


class HassIO(object):
"""Small API wrapper for HassIO."""

def __init__(self, loop, websession, ip):
"""Initialze HassIO api."""
self.loop = loop
self.websession = websession
self._ip = ip

def is_connected(self):
"""Return True if it connected to HassIO supervisor.
Return a coroutine.
"""
return self.send_command("/supervisor/ping")

@asyncio.coroutine
def send_command(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
"""Send request to API."""
answer = yield from self.send_raw(cmd, payload=payload)
if answer['result'] == 'ok':
return answer['data'] if answer['data'] else True

_LOGGER.error("%s return error %s.", cmd, answer['message'])
return False

@asyncio.coroutine
def send_raw(self, cmd, payload=None, timeout=DEFAULT_TIMEOUT):
"""Send raw request to API."""
try:
with async_timeout.timeout(timeout, loop=self.loop):
request = yield from self.websession.get(
"http://{}{}".format(self._ip, cmd),
timeout=None, json=payload
)

if request.status != 200:
_LOGGER.error("%s return code %d.", cmd, request.status)
return

return (yield from request.json())

except asyncio.TimeoutError:
_LOGGER.error("Timeout on api request %s.", cmd)

except aiohttp.ClientError:
_LOGGER.error("Client error on api request %s.", cmd)


class HassIOBaseView(HomeAssistantView):
"""HassIO view to handle base part."""

requires_auth = True

def __init__(self, hassio, base):
"""Initialize a hassio base view."""
self.hassio = hassio
self._url_info = "/{}/info".format(base)

self.url = "/api/hassio/{}".format(base)
self.name = "api:hassio:{}".format(base)

@asyncio.coroutine
def get(self, request):
"""Get base data."""
data = yield from self.hassio.send_command(self._url_info)
if not data:
raise HTTPBadGateway()
return web.json_response(data)


class HassIOBaseEditView(HassIOBaseView):
"""HassIO view to handle base with options support."""

def __init__(self, hassio, base):
"""Initialize a hassio base edit view."""
super().__init__(hassio, base)
self._url_options = "/{}/options".format(base)

@asyncio.coroutine
def post(self, request):
"""Set options on host."""
data = yield from request.json()

response = yield from self.hassio.send_raw(
self._url_options, payload=data)
if not response:
raise HTTPBadGateway()
return web.json_response(response)


class HassIOAddonsView(HomeAssistantView):
"""HassIO view to handle addons part."""

requires_auth = True
url = "/api/hassio/addons/{addon}"
name = "api:hassio:addons"

def __init__(self, hassio):
"""Initialize a hassio addon view."""
self.hassio = hassio

@asyncio.coroutine
def get(self, request, addon):
"""Get addon data."""
data = yield from self.hassio.send_command(
"/addons/{}/info".format(addon))
if not data:
raise HTTPBadGateway()
return web.json_response(data)

@asyncio.coroutine
def post(self, request, addon):
"""Set options on host."""
data = yield from request.json()

response = yield from self.hassio.send_raw(
"/addons/{}/options".format(addon), payload=data)
if not response:
raise HTTPBadGateway()
return web.json_response(response)
69 changes: 69 additions & 0 deletions homeassistant/components/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,72 @@ ffmpeg:
logger:
set_level:
description: Set log level for components.

hassio:
host_reboot:
description: Reboot host computer.

host_shutdown:
description: Poweroff host computer.

host_update:
description: Update host computer.
fields:
version:
description: Optional or it will be use the latest version.
example: '0.3'

supervisor_update:
description: Update HassIO supervisor.
fields:
version:
description: Optional or it will be use the latest version.
example: '0.3'

homeassistant_update:
description: Update HomeAssistant docker image.
fields:
version:
description: Optional or it will be use the latest version.
example: '0.40.1'

addon_install:
description: Install a HassIO docker addon.
fields:
addon:
description: Name of addon.
example: 'smb_config'
version:
description: Optional or it will be use the latest version.
example: '0.2'

addon_uninstall:
description: Uninstall a HassIO docker addon.
fields:
addon:
description: Name of addon.
example: 'smb_config'

addon_update:
description: Update a HassIO docker addon.
fields:
addon:
description: Name of addon.
example: 'smb_config'
version:
description: Optional or it will be use the latest version.
example: '0.2'

addon_start:
description: Start a HassIO docker addon.
fields:
addon:
description: Name of addon.
example: 'smb_config'

addon_stop:
description: Stop a HassIO docker addon.
fields:
addon:
description: Name of addon.
example: 'smb_config'
Loading

0 comments on commit 3e66df5

Please sign in to comment.