Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial import for HassIO #6935

Merged
merged 10 commits into from
Apr 7, 2017
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 262 additions & 0 deletions homeassistant/components/hassio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
"""
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__)

TIMEOUT = 900
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this timeout so high?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A docker image can have a long time to pull


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."""
websession = async_get_clientsession(hass)
hassio = HassIO(hass.loop, websession)

if not hassio.connected:
_LOGGER.error("No HassIO supervisor detect!")
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you create a dict that maps services to commands, payloads and schemas and lookup that here. See cover component for example. It will use a lot fewer lines of code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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)
elif service.service == SERVICE_ADDON_INSTALL:
yield from hassio.send_command(
"/addons/{}/install".format(addon), payload=version)
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you make SERVICE_MAP nested you could include a key-pair for the command string and another key-pair for the payload. Then have two cases at lookup-time, ie here, one where addon is None which just passes the command string to the send_command method and another case where addon is not None which uses string formatting on the command string before passing it along.

You would not need this long if - else trail.

Copy link
Member Author

@pvizeli pvizeli Apr 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can do that later. for the moment it help for more readable of code and maby we change some things in next version. That is also the reason of v1 on name :)


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):
"""Initialze HassIO api."""
self.loop = loop
self.websession = websession
try:
self._ip = os.environ['HASSIO']
except KeyError:
self._ip = None

@property
def connected(self):
"""Return True if it connected to HassIO supervisor."""
return self._ip is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a no-op API command so that we can connect to HASSIO and verify that we can make a connection? We do something similar for our normal API https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/remote.py#L81-L86

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok


@asyncio.coroutine
def send_command(self, cmd, payload=None):
"""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):
"""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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if self._ip is None, this will return an error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving IP should also happen inside setup, as it's a Home Assistant concern where it comes from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that. But without IP it will not load the component

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we wrap these in async_timeout ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do that on HassIO object

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have voluptuous schemas for data. That way any vulnerability in hassio won't be exposed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we make only a passthrought. HassIO will give a error back to frontend if a option will not support it.


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