Merge pull request #1051 from home-assistant/dev

Release 161
This commit is contained in:
Pascal Vizeli 2019-04-23 14:41:55 +02:00 committed by GitHub
commit f1bcbf2416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 145 additions and 14 deletions

17
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

23
API.md
View File

@ -516,7 +516,8 @@ Get all available addons.
"ingress": "bool", "ingress": "bool",
"ingress_entry": "null|/api/hassio_ingress/slug", "ingress_entry": "null|/api/hassio_ingress/slug",
"ingress_url": "null|/api/hassio_ingress/slug/entry.html", "ingress_url": "null|/api/hassio_ingress/slug/entry.html",
"ingress_port": "null|int" "ingress_port": "null|int",
"ingress_panel": "null|bool"
} }
``` ```
@ -537,7 +538,8 @@ Get all available addons.
}, },
"options": {}, "options": {},
"audio_output": "null|0,0", "audio_output": "null|0,0",
"audio_input": "null|0,0" "audio_input": "null|0,0",
"ingress_panel": "bool"
} }
``` ```
@ -602,6 +604,23 @@ Create a new Session for access to ingress service.
} }
``` ```
- GET `/ingress/panels`
Return a list of enabled panels.
```json
{
"panels": {
"addon_slug": {
"enable": "boolean",
"icon": "mdi:...",
"title": "title",
"admin": "boolean"
}
}
}
```
- VIEW `/ingress/{token}` - VIEW `/ingress/{token}`
Ingress WebUI for this Add-on. The addon need support HASS Auth! Ingress WebUI for this Add-on. The addon need support HASS Auth!

View File

@ -14,8 +14,8 @@ pr:
- dev - dev
variables: variables:
versionHadolint: v1.16.3 versionHadolint: 'v1.16.3'
versionBuilder: 1.1 versionBuilder: '1.1'
jobs: jobs:

View File

@ -47,6 +47,10 @@ from ..const import (
ATTR_INGRESS_ENTRY, ATTR_INGRESS_ENTRY,
ATTR_INGRESS_PORT, ATTR_INGRESS_PORT,
ATTR_INGRESS_TOKEN, ATTR_INGRESS_TOKEN,
ATTR_INGRESS_PANEL,
ATTR_INGRESS_PANEL_ADMIN,
ATTR_INGRESS_PANEL_ICON,
ATTR_INGRESS_PANEL_TITLE,
ATTR_KERNEL_MODULES, ATTR_KERNEL_MODULES,
ATTR_LEGACY, ATTR_LEGACY,
ATTR_LOCATON, ATTR_LOCATON,
@ -449,6 +453,21 @@ class Addon(CoreSysAttributes):
return self.sys_ingress.get_dynamic_port(self.slug) return self.sys_ingress.get_dynamic_port(self.slug)
return port return port
@property
def ingress_icon(self) -> str:
"""Return panel icon for Ingress frame."""
return self._mesh[ATTR_INGRESS_PANEL_ICON]
@property
def ingress_title(self) -> str:
"""Return panel icon for Ingress frame."""
return self._mesh.get(ATTR_INGRESS_PANEL_TITLE, self.name)
@property
def ingress_admin(self) -> str:
"""Return panel icon for Ingress frame."""
return self._mesh[ATTR_INGRESS_PANEL_ADMIN]
@property @property
def host_network(self): def host_network(self):
"""Return True if add-on run on host network.""" """Return True if add-on run on host network."""
@ -538,6 +557,18 @@ class Addon(CoreSysAttributes):
"""Return True if the add-on access support ingress.""" """Return True if the add-on access support ingress."""
return self._mesh[ATTR_INGRESS] return self._mesh[ATTR_INGRESS]
@property
def ingress_panel(self) -> Optional[bool]:
"""Return True if the add-on access support ingress."""
if self.is_installed:
return self._data.user[self._id][ATTR_INGRESS_PANEL]
return None
@ingress_panel.setter
def ingress_panel(self, value: bool):
"""Return True if the add-on access support ingress."""
self._data.user[self._id][ATTR_INGRESS_PANEL] = value
@property @property
def with_gpio(self): def with_gpio(self):
"""Return True if the add-on access to GPIO interface.""" """Return True if the add-on access to GPIO interface."""

View File

@ -41,6 +41,10 @@ from ..const import (
ATTR_INGRESS_ENTRY, ATTR_INGRESS_ENTRY,
ATTR_INGRESS_PORT, ATTR_INGRESS_PORT,
ATTR_INGRESS_TOKEN, ATTR_INGRESS_TOKEN,
ATTR_INGRESS_PANEL,
ATTR_INGRESS_PANEL_ADMIN,
ATTR_INGRESS_PANEL_ICON,
ATTR_INGRESS_PANEL_TITLE,
ATTR_KERNEL_MODULES, ATTR_KERNEL_MODULES,
ATTR_LEGACY, ATTR_LEGACY,
ATTR_LOCATON, ATTR_LOCATON,
@ -159,6 +163,9 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_INGRESS, default=False): vol.Boolean(), vol.Optional(ATTR_INGRESS, default=False): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(NETWORK_PORT, vol.Equal(0)), vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(NETWORK_PORT, vol.Equal(0)),
vol.Optional(ATTR_INGRESS_ENTRY): vol.Coerce(str), vol.Optional(ATTR_INGRESS_ENTRY): vol.Coerce(str),
vol.Optional(ATTR_INGRESS_PANEL_ICON, default="mdi:puzzle"): vol.Coerce(str),
vol.Optional(ATTR_INGRESS_PANEL_TITLE): vol.Coerce(str),
vol.Optional(ATTR_INGRESS_PANEL_ADMIN, default=True): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(vol.Coerce(str)), vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(vol.Coerce(str)),
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(), vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(), vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
@ -239,6 +246,7 @@ SCHEMA_ADDON_USER = vol.Schema({
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(), vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
}, extra=vol.REMOVE_EXTRA) }, extra=vol.REMOVE_EXTRA)

View File

@ -195,6 +195,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes([ self.webapp.add_routes([
web.post('/ingress/session', api_ingress.create_session), web.post('/ingress/session', api_ingress.create_session),
web.get('/ingress/panels', api_ingress.panels),
web.view('/ingress/{token}/{path:.*}', api_ingress.handler), web.view('/ingress/{token}/{path:.*}', api_ingress.handler),
]) ])

View File

@ -46,6 +46,7 @@ from ..const import (
ATTR_INGRESS_ENTRY, ATTR_INGRESS_ENTRY,
ATTR_INGRESS_PORT, ATTR_INGRESS_PORT,
ATTR_INGRESS_URL, ATTR_INGRESS_URL,
ATTR_INGRESS_PANEL,
ATTR_INSTALLED, ATTR_INSTALLED,
ATTR_IP_ADDRESS, ATTR_IP_ADDRESS,
ATTR_KERNEL_MODULES, ATTR_KERNEL_MODULES,
@ -100,6 +101,7 @@ SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(), vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_DEVICE,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE, vol.Optional(ATTR_AUDIO_INPUT): ALSA_DEVICE,
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
}) })
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
@ -227,6 +229,7 @@ class APIAddons(CoreSysAttributes):
ATTR_INGRESS_ENTRY: addon.ingress_entry, ATTR_INGRESS_ENTRY: addon.ingress_entry,
ATTR_INGRESS_URL: addon.ingress_url, ATTR_INGRESS_URL: addon.ingress_url,
ATTR_INGRESS_PORT: addon.ingress_port, ATTR_INGRESS_PORT: addon.ingress_port,
ATTR_INGRESS_PANEL: addon.ingress_panel,
} }
@api_process @api_process
@ -251,6 +254,9 @@ class APIAddons(CoreSysAttributes):
addon.audio_input = body[ATTR_AUDIO_INPUT] addon.audio_input = body[ATTR_AUDIO_INPUT]
if ATTR_AUDIO_OUTPUT in body: if ATTR_AUDIO_OUTPUT in body:
addon.audio_output = body[ATTR_AUDIO_OUTPUT] addon.audio_output = body[ATTR_AUDIO_OUTPUT]
if ATTR_INGRESS_PANEL in body:
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
await self.sys_ingress.update_hass_panel(addon)
addon.save_data() addon.save_data()

View File

@ -14,7 +14,17 @@ from aiohttp.web_exceptions import (
from multidict import CIMultiDict, istr from multidict import CIMultiDict, istr
from ..addons.addon import Addon from ..addons.addon import Addon
from ..const import ATTR_SESSION, HEADER_TOKEN, REQUEST_FROM, COOKIE_INGRESS from ..const import (
ATTR_ADMIN,
ATTR_ICON,
ATTR_SESSION,
ATTR_TITLE,
ATTR_PANELS,
ATTR_ENABLE,
COOKIE_INGRESS,
HEADER_TOKEN,
REQUEST_FROM,
)
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from .utils import api_process from .utils import api_process
@ -45,6 +55,20 @@ class APIIngress(CoreSysAttributes):
"""Create URL to container.""" """Create URL to container."""
return f"http://{addon.ip_address}:{addon.ingress_port}/{path}" return f"http://{addon.ip_address}:{addon.ingress_port}/{path}"
@api_process
async def panels(self, request: web.Request) -> Dict[str, Any]:
"""Create a list of panel data."""
addons = {}
for addon in self.sys_ingress.addons:
addons[addon.slug] = {
ATTR_TITLE: addon.ingress_title,
ATTR_ICON: addon.ingress_icon,
ATTR_ADMIN: addon.ingress_admin,
ATTR_ENABLE: addon.ingress_panel,
}
return {ATTR_PANELS: addons}
@api_process @api_process
async def create_session(self, request: web.Request) -> Dict[str, Any]: async def create_session(self, request: web.Request) -> Dict[str, Any]:
"""Create a new session.""" """Create a new session."""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1 +1 @@
!function(e){function n(n){for(var t,o,a=n[0],i=n[1],c=0,u=[];c<a.length;c++)o=a[c],r[o]&&u.push(r[o][0]),r[o]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(f&&f(n);u.length;)u.shift()()}var t={},r={4:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var a=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=a);var i,c=document.createElement("script");c.charset="utf-8",c.timeout=120,o.nc&&c.setAttribute("nonce",o.nc),c.src=function(e){return o.p+"chunk."+{0:"564a2f7b1c38ddaa4ce0",1:"659084fef4e3b7b66a76",2:"510634470d399e194ace",3:"f15d7f41c0d302cbbc7a",5:"5d31a1778f717ac8b063",6:"b60fb48c5280275dd7e2",7:"3a63ad36bccf4ea567fa",8:"a571dfa106202cc57af6",9:"a7e5fb452cd1b3a5faef",10:"b3340b3df270d20af4a1",11:"6e9c87e51920a9c354e5",12:"083a9da4ddb0a000aec4",13:"739b67c99ab56cdbd75d"}[e]+".js"}(e),i=function(n){c.onerror=c.onload=null,clearTimeout(f);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src,i=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");i.type=o,i.request=a,t[1](i)}r[e]=void 0}};var f=setTimeout(function(){i({type:"timeout",target:c})},12e4);c.onerror=c.onload=i,document.head.appendChild(c)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],i=a.push.bind(a);a.push=n,a=a.slice();for(var c=0;c<a.length;c++)n(a[c]);var f=i;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(1),t.e(5)]).then(t.bind(null,2)),Promise.all([t.e(1),t.e(9),t.e(6)]).then(t.bind(null,1))});var r=document.createElement("style");r.innerHTML="\nbody {\n font-family: Roboto, sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-weight: 400;\n margin: 0;\n padding: 0;\n height: 100vh;\n}\n",document.head.appendChild(r)}]); !function(e){function n(n){for(var t,o,a=n[0],i=n[1],f=0,u=[];f<a.length;f++)o=a[f],r[o]&&u.push(r[o][0]),r[o]=0;for(t in i)Object.prototype.hasOwnProperty.call(i,t)&&(e[t]=i[t]);for(c&&c(n);u.length;)u.shift()()}var t={},r={4:0};function o(n){if(t[n])return t[n].exports;var r=t[n]={i:n,l:!1,exports:{}};return e[n].call(r.exports,r,r.exports,o),r.l=!0,r.exports}o.e=function(e){var n=[],t=r[e];if(0!==t)if(t)n.push(t[2]);else{var a=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=a);var i,f=document.createElement("script");f.charset="utf-8",f.timeout=120,o.nc&&f.setAttribute("nonce",o.nc),f.src=function(e){return o.p+"chunk."+{0:"564a2f7b1c38ddaa4ce0",1:"659084fef4e3b7b66a76",2:"510634470d399e194ace",3:"f15d7f41c0d302cbbc7a",5:"5d31a1778f717ac8b063",6:"b60fb48c5280275dd7e2",7:"3a63ad36bccf4ea567fa",8:"a571dfa106202cc57af6",9:"a7e5fb452cd1b3a5faef",10:"b3340b3df270d20af4a1",11:"6e9c87e51920a9c354e5",12:"61f4e5888ff9846fe4b8",13:"739b67c99ab56cdbd75d"}[e]+".js"}(e),i=function(n){f.onerror=f.onload=null,clearTimeout(c);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),a=n&&n.target&&n.target.src,i=new Error("Loading chunk "+e+" failed.\n("+o+": "+a+")");i.type=o,i.request=a,t[1](i)}r[e]=void 0}};var c=setTimeout(function(){i({type:"timeout",target:f})},12e4);f.onerror=f.onload=i,document.head.appendChild(f)}return Promise.all(n)},o.m=e,o.c=t,o.d=function(e,n,t){o.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.t=function(e,n){if(1&n&&(e=o(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(o.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)o.d(t,r,function(n){return e[n]}.bind(null,r));return t},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="/api/hassio/app/",o.oe=function(e){throw console.error(e),e};var a=window.webpackJsonp=window.webpackJsonp||[],i=a.push.bind(a);a.push=n,a=a.slice();for(var f=0;f<a.length;f++)n(a[f]);var c=i;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(1),t.e(5)]).then(t.bind(null,2)),Promise.all([t.e(1),t.e(9),t.e(6)]).then(t.bind(null,1))});var r=document.createElement("style");r.innerHTML="\nbody {\n font-family: Roboto, sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-weight: 400;\n margin: 0;\n padding: 0;\n height: 100vh;\n}\n",document.head.appendChild(r)}]);

Binary file not shown.

View File

@ -3,7 +3,7 @@ from pathlib import Path
from ipaddress import ip_network from ipaddress import ip_network
HASSIO_VERSION = "160" HASSIO_VERSION = "161"
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons" URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
URL_HASSIO_VERSION = "https://s3.amazonaws.com/hassio-version/{channel}.json" URL_HASSIO_VERSION = "https://s3.amazonaws.com/hassio-version/{channel}.json"
@ -198,8 +198,16 @@ ATTR_INGRESS_PORT = "ingress_port"
ATTR_INGRESS_ENTRY = "ingress_entry" ATTR_INGRESS_ENTRY = "ingress_entry"
ATTR_INGRESS_TOKEN = "ingress_token" ATTR_INGRESS_TOKEN = "ingress_token"
ATTR_INGRESS_URL = "ingress_url" ATTR_INGRESS_URL = "ingress_url"
ATTR_INGRESS_PANEL = "ingress_panel"
ATTR_INGRESS_PANEL_ICON = "ingress_panel_icon"
ATTR_INGRESS_PANEL_TITLE = "ingress_panel_title"
ATTR_INGRESS_PANEL_ADMIN = "ingress_panel_admin"
ATTR_TITLE = "title"
ATTR_ENABLE = "enable"
ATTR_IP_ADDRESS = "ip_address" ATTR_IP_ADDRESS = "ip_address"
ATTR_SESSION = "session" ATTR_SESSION = "session"
ATTR_ADMIN = "admin"
ATTR_PANELS = "panels"
PROVIDE_SERVICE = "provide" PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need" NEED_SERVICE = "need"

View File

@ -3,7 +3,7 @@ from datetime import timedelta
import logging import logging
import random import random
import secrets import secrets
from typing import Dict, Optional from typing import Dict, List, Optional
from .addons.addon import Addon from .addons.addon import Addon
from .const import ATTR_PORTS, ATTR_SESSION, FILE_HASSIO_INGRESS from .const import ATTR_PORTS, ATTR_SESSION, FILE_HASSIO_INGRESS
@ -40,6 +40,16 @@ class Ingress(JsonConfig, CoreSysAttributes):
"""Return list of dynamic ports.""" """Return list of dynamic ports."""
return self._data[ATTR_PORTS] return self._data[ATTR_PORTS]
@property
def addons(self) -> List[Addon]:
"""Return list of ingress Add-ons."""
addons = []
for addon in self.sys_addons.list_installed:
if not addon.with_ingress:
continue
addons.append(addon)
return addons
async def load(self) -> None: async def load(self) -> None:
"""Update internal data.""" """Update internal data."""
self._update_token_list() self._update_token_list()
@ -77,9 +87,7 @@ class Ingress(JsonConfig, CoreSysAttributes):
self.tokens.clear() self.tokens.clear()
# Read all ingress token and build a map # Read all ingress token and build a map
for addon in self.sys_addons.list_installed: for addon in self.addons:
if not addon.with_ingress:
continue
self.tokens[addon.ingress_token] = addon.slug self.tokens[addon.ingress_token] = addon.slug
def create_session(self) -> str: def create_session(self) -> str:
@ -118,3 +126,12 @@ class Ingress(JsonConfig, CoreSysAttributes):
self.ports[addon_slug] = port self.ports[addon_slug] = port
self.save_data() self.save_data()
return port return port
async def update_hass_panel(self, addon: Addon):
"""Return True if Home Assistant up and running."""
method = "post" if addon.ingress_panel else "delete"
async with self.sys_homeassistant.make_request(method, f"api/hassio_push/panel/{addon.slug}") as resp:
if resp.status in (200, 201):
_LOGGER.info("Update Ingress as panel for %s", addon.slug)
else:
_LOGGER.warning("Fails Ingress panel for %s with %i", addon.slug, resp.status)

View File

@ -70,7 +70,7 @@ class Hardware:
def audio_devices(self): def audio_devices(self):
"""Return all available audio interfaces.""" """Return all available audio interfaces."""
if not ASOUND_CARDS.exists(): if not ASOUND_CARDS.exists():
_LOGGER.info("No audio devices found") _LOGGER.debug("No audio devices found")
return {} return {}
try: try:

@ -1 +1 @@
Subproject commit bbae3291e1187e764088bdbbc28e95d3e20382d1 Subproject commit ad40d9927ba40927302b1256ad0cf2f3f3858903