mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-23 07:59:21 +00:00
Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
32c9198fb2 | ||
![]() |
6983dcc267 | ||
![]() |
813fcc41f0 | ||
![]() |
f4e9dd0f1c | ||
![]() |
7f074142bf | ||
![]() |
b6df37628d | ||
![]() |
7867eded50 | ||
![]() |
311abb8a90 | ||
![]() |
21303f4b05 | ||
![]() |
da3270af67 | ||
![]() |
35aae69f23 | ||
![]() |
118a2e1951 | ||
![]() |
9053341581 | ||
![]() |
27532a8a00 | ||
![]() |
7fdfa630b5 | ||
![]() |
3974d5859f | ||
![]() |
aa1c765c4b | ||
![]() |
e78385e7ea | ||
![]() |
9d59b56c94 | ||
![]() |
9d72dcabfc | ||
![]() |
a0b5d0b67e | ||
![]() |
2b5520405f | ||
![]() |
ca376b3fcd | ||
![]() |
11e3c0c547 | ||
![]() |
9da136e037 | ||
![]() |
9b3e59d876 | ||
![]() |
7a592795b5 | ||
![]() |
5b92137699 | ||
![]() |
7520cdfeb4 | ||
![]() |
0ada791e3a | ||
![]() |
73afced4dc | ||
![]() |
633a2e93bf | ||
![]() |
07c4058a8c | ||
![]() |
b6f3938b14 | ||
![]() |
57534fac96 | ||
![]() |
4a03e72983 | ||
![]() |
ddb29ea9b1 | ||
![]() |
95179c30f7 | ||
![]() |
f49970ce2c | ||
![]() |
790818d1aa | ||
![]() |
62f675e613 | ||
![]() |
f33434fb01 | ||
![]() |
254d6aee32 | ||
![]() |
a5ecd597ed | ||
![]() |
0fab3e940a | ||
![]() |
60fbebc16b | ||
![]() |
ec366d8112 | ||
![]() |
b8818788c9 | ||
![]() |
e23f6f6998 | ||
![]() |
05b58d76b9 | ||
![]() |
644d13e3fa | ||
![]() |
9de71472d4 | ||
![]() |
bf28227b91 | ||
![]() |
4c1ee49068 | ||
![]() |
6e7cf5e4c9 | ||
![]() |
11f8c97347 | ||
![]() |
a1461fd518 | ||
![]() |
fa5c2e37d3 | ||
![]() |
1f091b20ad | ||
![]() |
d3b4a03851 | ||
![]() |
fb12fee59b | ||
![]() |
7a87d2334a | ||
![]() |
9591e71138 | ||
![]() |
cecad526a2 | ||
![]() |
53dab4ee45 | ||
![]() |
8abbba46c7 | ||
![]() |
0f01ac1b59 | ||
![]() |
aa8ab593c0 | ||
![]() |
84f791220e | ||
![]() |
cee2c5469f | ||
![]() |
6e75964a8b | ||
![]() |
5ab5036504 | ||
![]() |
000a3c1f7e | ||
![]() |
8ea123eb94 | ||
![]() |
571c42ef7d | ||
![]() |
8443da0b9f | ||
![]() |
7dbbcf24c8 | ||
![]() |
468cb0c36b | ||
![]() |
78e093df96 | ||
![]() |
ec4d7dab21 | ||
![]() |
d00ee0adea | ||
![]() |
55d5ee4ed4 | ||
![]() |
0e51d74265 | ||
![]() |
916f3caedd | ||
![]() |
ff80ccce64 | ||
![]() |
23f28b38e9 | ||
![]() |
da425a0530 | ||
![]() |
79dca1608e | ||
![]() |
33b615e40d | ||
![]() |
c825c40c4d | ||
![]() |
8beb723cc2 | ||
![]() |
94fd24c251 | ||
![]() |
bf75a8a439 | ||
![]() |
36cdb05387 | ||
![]() |
dccc652d42 | ||
![]() |
74e03a9a2e | ||
![]() |
2f6df3a946 | ||
![]() |
2872be6385 | ||
![]() |
af19e95c81 | ||
![]() |
e5451973bd | ||
![]() |
4ef8c9d633 | ||
![]() |
4a9dcb540e | ||
![]() |
61eefea358 | ||
![]() |
f2a5512bbf | ||
![]() |
2f4e114f25 | ||
![]() |
c91bac2527 | ||
![]() |
52da7605f5 | ||
![]() |
267791833e | ||
![]() |
67dcf1563b | ||
![]() |
ccff0f5b9e | ||
![]() |
9f8ad05471 | ||
![]() |
c2299ef8da | ||
![]() |
f5845564db | ||
![]() |
17904d70d8 | ||
![]() |
622e99e04c | ||
![]() |
061420f279 | ||
![]() |
3d459f1b8b | ||
![]() |
5f3dd6190a | ||
![]() |
ac824d3af6 | ||
![]() |
dd25c29544 | ||
![]() |
5cbdbffbb2 | ||
![]() |
bb81f14c2c | ||
![]() |
cecefd6972 | ||
![]() |
ff7f6a0b4c |
@@ -7,3 +7,7 @@
|
|||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
|
|
||||||
|
# virtualenv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
29
.github/ISSUE_TEMPLATE.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!-- READ THIS FIRST:
|
||||||
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
||||||
|
- Do not report issues for components here, plaese refer to https://github.com/home-assistant/home-assistant/issues
|
||||||
|
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||||
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||||
|
- If you have a problem with a Add-on, make a issue on there repository.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Home Assistant release with the issue:**
|
||||||
|
<!--
|
||||||
|
- Frontend -> Developer tools -> Info
|
||||||
|
- Or use this command: hass --version
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Operating environment (HassOS/Generic):**
|
||||||
|
<!--
|
||||||
|
Please provide details about your environment.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Supervisor logs:**
|
||||||
|
<!--
|
||||||
|
- Frontend -> Hass.io -> System
|
||||||
|
- Or use this command: hassio su logs
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
**Description of problem:**
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -90,3 +90,6 @@ ENV/
|
|||||||
|
|
||||||
# pylint
|
# pylint
|
||||||
.pylint.d/
|
.pylint.d/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
|||||||
[submodule "home-assistant-polymer"]
|
[submodule "home-assistant-polymer"]
|
||||||
path = home-assistant-polymer
|
path = home-assistant-polymer
|
||||||
url = https://github.com/home-assistant/home-assistant-polymer
|
url = https://github.com/home-assistant/home-assistant-polymer
|
||||||
|
branch = dev
|
||||||
|
102
API.md
102
API.md
@@ -1,4 +1,4 @@
|
|||||||
# Hass.io Server
|
# Hass.io
|
||||||
|
|
||||||
## Hass.io RESTful API
|
## Hass.io RESTful API
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ For access to API you need set the `X-HASSIO-KEY` they will be available for Add
|
|||||||
### Hass.io
|
### Hass.io
|
||||||
|
|
||||||
- GET `/supervisor/ping`
|
- GET `/supervisor/ping`
|
||||||
|
|
||||||
|
This API call don't need a token.
|
||||||
|
|
||||||
- GET `/supervisor/info`
|
- GET `/supervisor/info`
|
||||||
|
|
||||||
The addons from `addons` are only installed one.
|
The addons from `addons` are only installed one.
|
||||||
@@ -311,9 +314,10 @@ Load host configs from a USB stick.
|
|||||||
"CARD_ID": {
|
"CARD_ID": {
|
||||||
"name": "xy",
|
"name": "xy",
|
||||||
"type": "microphone",
|
"type": "microphone",
|
||||||
"devices": {
|
"devices": [
|
||||||
"DEV_ID": "type of device"
|
"chan_id": "channel ID",
|
||||||
}
|
"chan_type": "type of device"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +346,7 @@ Load host configs from a USB stick.
|
|||||||
{
|
{
|
||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"last_version": "LAST_VERSION",
|
"last_version": "LAST_VERSION",
|
||||||
|
"arch": "arch",
|
||||||
"machine": "Image machine type",
|
"machine": "Image machine type",
|
||||||
"image": "str",
|
"image": "str",
|
||||||
"custom": "bool -> if custom image",
|
"custom": "bool -> if custom image",
|
||||||
@@ -349,7 +354,7 @@ Load host configs from a USB stick.
|
|||||||
"port": 8123,
|
"port": 8123,
|
||||||
"ssl": "bool",
|
"ssl": "bool",
|
||||||
"watchdog": "bool",
|
"watchdog": "bool",
|
||||||
"startup_time": 600
|
"wait_boot": 600
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -383,7 +388,7 @@ Output is the raw Docker log.
|
|||||||
"password": "",
|
"password": "",
|
||||||
"refresh_token": "",
|
"refresh_token": "",
|
||||||
"watchdog": "bool",
|
"watchdog": "bool",
|
||||||
"startup_time": 600
|
"wait_boot": 600
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -412,6 +417,8 @@ Proxy to real websocket instance.
|
|||||||
|
|
||||||
### RESTful for API addons
|
### RESTful for API addons
|
||||||
|
|
||||||
|
If an add-on will call itself, you can use `/addons/self/...`.
|
||||||
|
|
||||||
- GET `/addons`
|
- GET `/addons`
|
||||||
|
|
||||||
Get all available addons.
|
Get all available addons.
|
||||||
@@ -423,11 +430,11 @@ Get all available addons.
|
|||||||
"name": "xy bla",
|
"name": "xy bla",
|
||||||
"slug": "xy",
|
"slug": "xy",
|
||||||
"description": "description",
|
"description": "description",
|
||||||
"arch": ["armhf", "aarch64", "i386", "amd64"],
|
|
||||||
"repository": "core|local|REP_ID",
|
"repository": "core|local|REP_ID",
|
||||||
"version": "LAST_VERSION",
|
"version": "LAST_VERSION",
|
||||||
"installed": "none|INSTALL_VERSION",
|
"installed": "none|INSTALL_VERSION",
|
||||||
"detached": "bool",
|
"detached": "bool",
|
||||||
|
"available": "bool",
|
||||||
"build": "bool",
|
"build": "bool",
|
||||||
"url": "null|url",
|
"url": "null|url",
|
||||||
"icon": "bool",
|
"icon": "bool",
|
||||||
@@ -458,6 +465,9 @@ Get all available addons.
|
|||||||
"auto_update": "bool",
|
"auto_update": "bool",
|
||||||
"url": "null|url of addon",
|
"url": "null|url of addon",
|
||||||
"detached": "bool",
|
"detached": "bool",
|
||||||
|
"available": "bool",
|
||||||
|
"arch": ["armhf", "aarch64", "i386", "amd64"],
|
||||||
|
"machine": "[raspberrypi2, tinker]",
|
||||||
"repository": "12345678|null",
|
"repository": "12345678|null",
|
||||||
"version": "null|VERSION_INSTALLED",
|
"version": "null|VERSION_INSTALLED",
|
||||||
"last_version": "LAST_VERSION",
|
"last_version": "LAST_VERSION",
|
||||||
@@ -467,6 +477,7 @@ Get all available addons.
|
|||||||
"options": "{}",
|
"options": "{}",
|
||||||
"network": "{}|null",
|
"network": "{}|null",
|
||||||
"host_network": "bool",
|
"host_network": "bool",
|
||||||
|
"host_pid": "bool",
|
||||||
"host_ipc": "bool",
|
"host_ipc": "bool",
|
||||||
"host_dbus": "bool",
|
"host_dbus": "bool",
|
||||||
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
|
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
|
||||||
@@ -477,20 +488,23 @@ Get all available addons.
|
|||||||
"logo": "bool",
|
"logo": "bool",
|
||||||
"changelog": "bool",
|
"changelog": "bool",
|
||||||
"hassio_api": "bool",
|
"hassio_api": "bool",
|
||||||
|
"hassio_role": "default|homeassistant|manager|admin",
|
||||||
"homeassistant_api": "bool",
|
"homeassistant_api": "bool",
|
||||||
|
"auth_api": "bool",
|
||||||
"full_access": "bool",
|
"full_access": "bool",
|
||||||
"protected": "bool",
|
"protected": "bool",
|
||||||
"rating": "1-6",
|
"rating": "1-6",
|
||||||
"stdin": "bool",
|
"stdin": "bool",
|
||||||
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
||||||
"gpio": "bool",
|
"gpio": "bool",
|
||||||
|
"kernel_modules": "bool",
|
||||||
"devicetree": "bool",
|
"devicetree": "bool",
|
||||||
"docker_api": "bool",
|
"docker_api": "bool",
|
||||||
"audio": "bool",
|
"audio": "bool",
|
||||||
"audio_input": "null|0,0",
|
"audio_input": "null|0,0",
|
||||||
"audio_output": "null|0,0",
|
"audio_output": "null|0,0",
|
||||||
"services": "null|['mqtt']",
|
"services_role": "['service:access']",
|
||||||
"discovery": "null|['component/platform']"
|
"discovery": "['service']"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -510,7 +524,6 @@ Get all available addons.
|
|||||||
"CONTAINER": "port|[ip, port]"
|
"CONTAINER": "port|[ip, port]"
|
||||||
},
|
},
|
||||||
"options": {},
|
"options": {},
|
||||||
"protected": "bool",
|
|
||||||
"audio_output": "null|0,0",
|
"audio_output": "null|0,0",
|
||||||
"audio_input": "null|0,0"
|
"audio_input": "null|0,0"
|
||||||
}
|
}
|
||||||
@@ -518,6 +531,16 @@ Get all available addons.
|
|||||||
|
|
||||||
Reset custom network/audio/options, set it `null`.
|
Reset custom network/audio/options, set it `null`.
|
||||||
|
|
||||||
|
- POST `/addons/{addon}/security`
|
||||||
|
|
||||||
|
This function is not callable by itself.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"protected": "bool",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- POST `/addons/{addon}/start`
|
- POST `/addons/{addon}/start`
|
||||||
|
|
||||||
- POST `/addons/{addon}/stop`
|
- POST `/addons/{addon}/stop`
|
||||||
@@ -555,39 +578,36 @@ Write data to add-on stdin
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service discovery
|
### discovery
|
||||||
|
|
||||||
- GET `/services/discovery`
|
- GET `/discovery`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"discovery": [
|
"discovery": [
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "slug",
|
||||||
|
"service": "name",
|
||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"component": "component",
|
|
||||||
"platform": "null|platform",
|
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- GET `/services/discovery/{UUID}`
|
- GET `/discovery/{UUID}`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "slug",
|
||||||
|
"service": "name",
|
||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"component": "component",
|
|
||||||
"platform": "null|platform",
|
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/services/discovery`
|
- POST `/discovery`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"component": "component",
|
"service": "name",
|
||||||
"platform": "null|platform",
|
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -599,7 +619,9 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- DEL `/services/discovery/{UUID}`
|
- DEL `/discovery/{UUID}`
|
||||||
|
|
||||||
|
### Services
|
||||||
|
|
||||||
- GET `/services`
|
- GET `/services`
|
||||||
```json
|
```json
|
||||||
@@ -608,7 +630,7 @@ return:
|
|||||||
{
|
{
|
||||||
"slug": "name",
|
"slug": "name",
|
||||||
"available": "bool",
|
"available": "bool",
|
||||||
"provider": "null|name|list"
|
"providers": "list"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -616,12 +638,10 @@ return:
|
|||||||
|
|
||||||
#### MQTT
|
#### MQTT
|
||||||
|
|
||||||
This service performs an auto discovery to Home-Assistant.
|
|
||||||
|
|
||||||
- GET `/services/mqtt`
|
- GET `/services/mqtt`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"provider": "name",
|
"addon": "name",
|
||||||
"host": "xy",
|
"host": "xy",
|
||||||
"port": "8883",
|
"port": "8883",
|
||||||
"ssl": "bool",
|
"ssl": "bool",
|
||||||
@@ -644,3 +664,31 @@ This service performs an auto discovery to Home-Assistant.
|
|||||||
```
|
```
|
||||||
|
|
||||||
- DEL `/services/mqtt`
|
- DEL `/services/mqtt`
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
- GET `/info`
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"supervisor": "version",
|
||||||
|
"homeassistant": "version",
|
||||||
|
"hassos": "null|version",
|
||||||
|
"hostname": "name",
|
||||||
|
"machine": "type",
|
||||||
|
"arch": "arch",
|
||||||
|
"supported_arch": ["arch1", "arch2"],
|
||||||
|
"channel": "stable|beta|dev"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth / SSO API
|
||||||
|
|
||||||
|
You can use the user system on homeassistant. We handle this auth system on
|
||||||
|
supervisor.
|
||||||
|
|
||||||
|
You can call post `/auth`
|
||||||
|
|
||||||
|
We support:
|
||||||
|
- Json `{ "user|name": "...", "password": "..." }`
|
||||||
|
- application/x-www-form-urlencoded `user|name=...&password=...`
|
||||||
|
- BasicAuth
|
||||||
|
@@ -3,6 +3,9 @@ FROM $BUILD_FROM
|
|||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
openssl \
|
||||||
|
libffi \
|
||||||
|
musl \
|
||||||
git \
|
git \
|
||||||
socat \
|
socat \
|
||||||
glib \
|
glib \
|
||||||
@@ -14,6 +17,10 @@ COPY requirements.txt /usr/src/
|
|||||||
RUN apk add --no-cache --virtual .build-dependencies \
|
RUN apk add --no-cache --virtual .build-dependencies \
|
||||||
make \
|
make \
|
||||||
g++ \
|
g++ \
|
||||||
|
openssl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
musl-dev \
|
||||||
|
&& export MAKEFLAGS="-j$(nproc)" \
|
||||||
&& pip3 install --no-cache-dir -r /usr/src/requirements.txt \
|
&& pip3 install --no-cache-dir -r /usr/src/requirements.txt \
|
||||||
&& apk del .build-dependencies \
|
&& apk del .build-dependencies \
|
||||||
&& rm -f /usr/src/requirements.txt
|
&& rm -f /usr/src/requirements.txt
|
||||||
|
16
README.md
16
README.md
@@ -10,9 +10,19 @@ and updating software.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- [Hass.io Addons](https://github.com/home-assistant/hassio-addons)
|
|
||||||
- [Hass.io Build](https://github.com/home-assistant/hassio-build)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Installation instructions can be found at <https://home-assistant.io/hassio>.
|
Installation instructions can be found at <https://home-assistant.io/hassio>.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
The development of the supervisor is a bit tricky. Not difficult but tricky.
|
||||||
|
|
||||||
|
- You can use the builder to build your supervisor: https://github.com/home-assistant/hassio-build/tree/master/builder
|
||||||
|
- Go into a HassOS device or VM and pull your supervisor.
|
||||||
|
- Set the developer modus on updater.json
|
||||||
|
- Tag it as `homeassistant/xy-hassio-supervisor:latest`
|
||||||
|
- Restart the service like `systemctl restart hassos-supervisor | journalctl -fu hassos-supervisor`
|
||||||
|
- Test your changes
|
||||||
|
|
||||||
|
Small Bugfix or improvements, make a PR. Significant change makes first an RFC.
|
||||||
|
@@ -1 +1 @@
|
|||||||
"""Init file for HassIO."""
|
"""Init file for Hass.io."""
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Main file for HassIO."""
|
"""Main file for Hass.io."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import logging
|
import logging
|
||||||
@@ -9,7 +9,7 @@ from hassio import bootstrap
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def attempt_use_uvloop():
|
def initialize_event_loop():
|
||||||
"""Attempt to use uvloop."""
|
"""Attempt to use uvloop."""
|
||||||
try:
|
try:
|
||||||
import uvloop
|
import uvloop
|
||||||
@@ -17,13 +17,17 @@ def attempt_use_uvloop():
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
return asyncio.get_event_loop()
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bootstrap.initialize_logging()
|
bootstrap.initialize_logging()
|
||||||
attempt_use_uvloop()
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
|
# Init async event loop
|
||||||
|
loop = initialize_event_loop()
|
||||||
|
|
||||||
|
# Check if all information are available to setup Hass.io
|
||||||
if not bootstrap.check_environment():
|
if not bootstrap.check_environment():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -31,8 +35,8 @@ if __name__ == "__main__":
|
|||||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||||
loop.set_default_executor(executor)
|
loop.set_default_executor(executor)
|
||||||
|
|
||||||
_LOGGER.info("Initialize Hassio setup")
|
_LOGGER.info("Initialize Hass.io setup")
|
||||||
coresys = bootstrap.initialize_coresys(loop)
|
coresys = loop.run_until_complete(bootstrap.initialize_coresys())
|
||||||
|
|
||||||
bootstrap.migrate_system_env(coresys)
|
bootstrap.migrate_system_env(coresys)
|
||||||
|
|
||||||
@@ -43,13 +47,13 @@ if __name__ == "__main__":
|
|||||||
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
|
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Run HassIO")
|
_LOGGER.info("Run Hass.io")
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
_LOGGER.info("Stopping HassIO")
|
_LOGGER.info("Stopping Hass.io")
|
||||||
loop.run_until_complete(coresys.core.stop())
|
loop.run_until_complete(coresys.core.stop())
|
||||||
executor.shutdown(wait=False)
|
executor.shutdown(wait=False)
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
_LOGGER.info("Close Hassio")
|
_LOGGER.info("Close Hass.io")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for Hass.io add-ons."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -14,10 +14,10 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
|
|||||||
|
|
||||||
|
|
||||||
class AddonManager(CoreSysAttributes):
|
class AddonManager(CoreSysAttributes):
|
||||||
"""Manage addons inside HassIO."""
|
"""Manage add-ons inside Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self.data = AddonsData(coresys)
|
self.data = AddonsData(coresys)
|
||||||
self.addons_obj = {}
|
self.addons_obj = {}
|
||||||
@@ -25,44 +25,44 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def list_addons(self):
|
def list_addons(self):
|
||||||
"""Return a list of all addons."""
|
"""Return a list of all add-ons."""
|
||||||
return list(self.addons_obj.values())
|
return list(self.addons_obj.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_installed(self):
|
def list_installed(self):
|
||||||
"""Return a list of installed addons."""
|
"""Return a list of installed add-ons."""
|
||||||
return [addon for addon in self.addons_obj.values()
|
return [addon for addon in self.addons_obj.values()
|
||||||
if addon.is_installed]
|
if addon.is_installed]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_repositories(self):
|
def list_repositories(self):
|
||||||
"""Return list of addon repositories."""
|
"""Return list of add-on repositories."""
|
||||||
return list(self.repositories_obj.values())
|
return list(self.repositories_obj.values())
|
||||||
|
|
||||||
def get(self, addon_slug):
|
def get(self, addon_slug):
|
||||||
"""Return an add-on from slug."""
|
"""Return an add-on from slug."""
|
||||||
return self.addons_obj.get(addon_slug)
|
return self.addons_obj.get(addon_slug)
|
||||||
|
|
||||||
def from_uuid(self, uuid):
|
def from_token(self, token):
|
||||||
"""Return an add-on from uuid."""
|
"""Return an add-on from Hass.io token."""
|
||||||
for addon in self.list_addons:
|
for addon in self.list_addons:
|
||||||
if addon.is_installed and uuid == addon.uuid:
|
if addon.is_installed and token == addon.hassio_token:
|
||||||
return addon
|
return addon
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Startup addon management."""
|
"""Start up add-on management."""
|
||||||
self.data.reload()
|
self.data.reload()
|
||||||
|
|
||||||
# init hassio built-in repositories
|
# Init Hass.io built-in repositories
|
||||||
repositories = \
|
repositories = \
|
||||||
set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
|
set(self.sys_config.addons_repositories) | BUILTIN_REPOSITORIES
|
||||||
|
|
||||||
# init custom repositories & load addons
|
# Init custom repositories and load add-ons
|
||||||
await self.load_repositories(repositories)
|
await self.load_repositories(repositories)
|
||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
"""Update addons from repo and reload list."""
|
"""Update add-ons from repository and reload list."""
|
||||||
tasks = [repository.update() for repository in
|
tasks = [repository.update() for repository in
|
||||||
self.repositories_obj.values()]
|
self.repositories_obj.values()]
|
||||||
if tasks:
|
if tasks:
|
||||||
@@ -106,14 +106,14 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await self.load_addons()
|
await self.load_addons()
|
||||||
|
|
||||||
async def load_addons(self):
|
async def load_addons(self):
|
||||||
"""Update/add internal addon store."""
|
"""Update/add internal add-on store."""
|
||||||
all_addons = set(self.data.system) | set(self.data.cache)
|
all_addons = set(self.data.system) | set(self.data.cache)
|
||||||
|
|
||||||
# calc diff
|
# calc diff
|
||||||
add_addons = all_addons - set(self.addons_obj)
|
add_addons = all_addons - set(self.addons_obj)
|
||||||
del_addons = set(self.addons_obj) - all_addons
|
del_addons = set(self.addons_obj) - all_addons
|
||||||
|
|
||||||
_LOGGER.info("Load addons: %d all - %d new - %d remove",
|
_LOGGER.info("Load add-ons: %d all - %d new - %d remove",
|
||||||
len(all_addons), len(add_addons), len(del_addons))
|
len(all_addons), len(add_addons), len(del_addons))
|
||||||
|
|
||||||
# new addons
|
# new addons
|
||||||
@@ -132,14 +132,14 @@ class AddonManager(CoreSysAttributes):
|
|||||||
self.addons_obj.pop(addon_slug)
|
self.addons_obj.pop(addon_slug)
|
||||||
|
|
||||||
async def boot(self, stage):
|
async def boot(self, stage):
|
||||||
"""Boot addons with mode auto."""
|
"""Boot add-ons with mode auto."""
|
||||||
tasks = []
|
tasks = []
|
||||||
for addon in self.addons_obj.values():
|
for addon in self.addons_obj.values():
|
||||||
if addon.is_installed and addon.boot == BOOT_AUTO and \
|
if addon.is_installed and addon.boot == BOOT_AUTO and \
|
||||||
addon.startup == stage:
|
addon.startup == stage:
|
||||||
tasks.append(addon.start())
|
tasks.append(addon.start())
|
||||||
|
|
||||||
_LOGGER.info("Startup %s run %d addons", stage, len(tasks))
|
_LOGGER.info("Startup %s run %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
await asyncio.sleep(self.sys_config.wait_boot)
|
await asyncio.sleep(self.sys_config.wait_boot)
|
||||||
@@ -153,6 +153,6 @@ class AddonManager(CoreSysAttributes):
|
|||||||
addon.startup == stage:
|
addon.startup == stage:
|
||||||
tasks.append(addon.stop())
|
tasks.append(addon.stop())
|
||||||
|
|
||||||
_LOGGER.info("Shutdown %s stop %d addons", stage, len(tasks))
|
_LOGGER.info("Shutdown %s stop %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
|
@@ -1,38 +1,42 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for Hass.io add-ons."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from .validate import (
|
|
||||||
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME, RE_SERVICE)
|
|
||||||
from .utils import check_installed, remove_data
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
|
ATTR_ACCESS_TOKEN, ATTR_APPARMOR, ATTR_ARCH, ATTR_AUDIO, ATTR_AUDIO_INPUT,
|
||||||
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
|
ATTR_AUDIO_OUTPUT, ATTR_AUTH_API, ATTR_AUTO_UART, ATTR_AUTO_UPDATE,
|
||||||
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
|
ATTR_BOOT, ATTR_DESCRIPTON, ATTR_DEVICES, ATTR_DEVICETREE, ATTR_DISCOVERY,
|
||||||
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP, ATTR_UUID,
|
ATTR_DOCKER_API, ATTR_ENVIRONMENT, ATTR_FULL_ACCESS, ATTR_GPIO,
|
||||||
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
|
ATTR_HASSIO_API, ATTR_HASSIO_ROLE, ATTR_HOMEASSISTANT_API, ATTR_HOST_DBUS,
|
||||||
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
|
ATTR_HOST_IPC, ATTR_HOST_NETWORK, ATTR_HOST_PID, ATTR_IMAGE,
|
||||||
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
ATTR_KERNEL_MODULES, ATTR_LEGACY, ATTR_LOCATON, ATTR_MACHINE, ATTR_MAP,
|
||||||
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
|
ATTR_NAME, ATTR_NETWORK, ATTR_OPTIONS, ATTR_PORTS, ATTR_PRIVILEGED,
|
||||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES,
|
ATTR_PROTECTED, ATTR_REPOSITORY, ATTR_SCHEMA, ATTR_SERVICES, ATTR_SLUG,
|
||||||
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_FULL_ACCESS,
|
ATTR_STARTUP, ATTR_STATE, ATTR_STDIN, ATTR_SYSTEM, ATTR_TIMEOUT,
|
||||||
ATTR_PROTECTED,
|
ATTR_TMPFS, ATTR_URL, ATTR_USER, ATTR_UUID, ATTR_VERSION, ATTR_WEBUI,
|
||||||
SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
|
SECURITY_DEFAULT, SECURITY_DISABLE, SECURITY_PROFILE, STATE_NONE,
|
||||||
|
STATE_STARTED, STATE_STOPPED)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.addon import DockerAddon
|
from ..docker.addon import DockerAddon
|
||||||
from ..utils.json import write_json_file, read_json_file
|
|
||||||
from ..utils.apparmor import adjust_profile
|
|
||||||
from ..exceptions import HostAppArmorError
|
from ..exceptions import HostAppArmorError
|
||||||
|
from ..utils import create_token
|
||||||
|
from ..utils.apparmor import adjust_profile
|
||||||
|
from ..utils.json import read_json_file, write_json_file
|
||||||
|
from .utils import check_installed, remove_data
|
||||||
|
from .validate import (
|
||||||
|
MACHINE_ALL, RE_SERVICE, RE_VOLUME, SCHEMA_ADDON_SNAPSHOT,
|
||||||
|
validate_options)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -42,7 +46,7 @@ RE_WEBUI = re.compile(
|
|||||||
|
|
||||||
|
|
||||||
class Addon(CoreSysAttributes):
|
class Addon(CoreSysAttributes):
|
||||||
"""Hold data for addon inside HassIO."""
|
"""Hold data for add-on inside Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys, slug):
|
def __init__(self, coresys, slug):
|
||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
@@ -53,65 +57,87 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Async initialize of object."""
|
"""Async initialize of object."""
|
||||||
if self.is_installed:
|
if not self.is_installed:
|
||||||
|
return
|
||||||
await self.instance.attach()
|
await self.instance.attach()
|
||||||
|
|
||||||
|
# NOTE: Can't be removed after soon
|
||||||
|
if ATTR_IMAGE not in self._data.user[self._id]:
|
||||||
|
self._data.user[self._id][ATTR_IMAGE] = self.image_name
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slug(self):
|
def slug(self):
|
||||||
"""Return slug/id of addon."""
|
"""Return slug/id of add-on."""
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _mesh(self):
|
def _mesh(self):
|
||||||
"""Return addon data from system or cache."""
|
"""Return add-on data from system or cache."""
|
||||||
return self._data.system.get(self._id, self._data.cache.get(self._id))
|
return self._data.system.get(self._id, self._data.cache.get(self._id))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _data(self):
|
def _data(self):
|
||||||
"""Return addons data storage."""
|
"""Return add-ons data storage."""
|
||||||
return self.sys_addons.data
|
return self.sys_addons.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_installed(self):
|
def is_installed(self):
|
||||||
"""Return True if an addon is installed."""
|
"""Return True if an add-on is installed."""
|
||||||
return self._id in self._data.system
|
return self._id in self._data.system
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_detached(self):
|
def is_detached(self):
|
||||||
"""Return True if addon is detached."""
|
"""Return True if add-on is detached."""
|
||||||
return self._id not in self._data.cache
|
return self._id not in self._data.cache
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if this add-on is available on this platform."""
|
||||||
|
# Architecture
|
||||||
|
if not self.sys_arch.is_supported(self.supported_arch):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Machine / Hardware
|
||||||
|
if self.sys_machine not in self.supported_machine:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version_installed(self):
|
def version_installed(self):
|
||||||
"""Return installed version."""
|
"""Return installed version."""
|
||||||
return self._data.user.get(self._id, {}).get(ATTR_VERSION)
|
return self._data.user.get(self._id, {}).get(ATTR_VERSION)
|
||||||
|
|
||||||
def _set_install(self, version):
|
def _set_install(self, image: str, version: str) -> None:
|
||||||
"""Set addon as installed."""
|
"""Set addon as installed."""
|
||||||
self._data.system[self._id] = deepcopy(self._data.cache[self._id])
|
self._data.system[self._id] = deepcopy(self._data.cache[self._id])
|
||||||
self._data.user[self._id] = {
|
self._data.user[self._id] = {
|
||||||
ATTR_OPTIONS: {},
|
ATTR_OPTIONS: {},
|
||||||
ATTR_VERSION: version,
|
ATTR_VERSION: version,
|
||||||
|
ATTR_IMAGE: image,
|
||||||
}
|
}
|
||||||
self._data.save_data()
|
self.save_data()
|
||||||
|
|
||||||
def _set_uninstall(self):
|
def _set_uninstall(self) -> None:
|
||||||
"""Set addon as uninstalled."""
|
"""Set add-on as uninstalled."""
|
||||||
self._data.system.pop(self._id, None)
|
self._data.system.pop(self._id, None)
|
||||||
self._data.user.pop(self._id, None)
|
self._data.user.pop(self._id, None)
|
||||||
self._data.save_data()
|
self.save_data()
|
||||||
|
|
||||||
def _set_update(self, version):
|
def _set_update(self, image: str, version: str) -> None:
|
||||||
"""Update version of addon."""
|
"""Update version of add-on."""
|
||||||
self._data.system[self._id] = deepcopy(self._data.cache[self._id])
|
self._data.system[self._id] = deepcopy(self._data.cache[self._id])
|
||||||
self._data.user[self._id][ATTR_VERSION] = version
|
self._data.user[self._id][ATTR_VERSION] = version
|
||||||
self._data.save_data()
|
self.save_data()
|
||||||
|
|
||||||
def _restore_data(self, user, system):
|
def _restore_data(self, user: Dict[str, Any], system: Dict[str, Any], image: str) -> None:
|
||||||
"""Restore data to addon."""
|
"""Restore data to add-on."""
|
||||||
self._data.user[self._id] = deepcopy(user)
|
self._data.user[self._id] = deepcopy(user)
|
||||||
self._data.system[self._id] = deepcopy(system)
|
self._data.system[self._id] = deepcopy(system)
|
||||||
self._data.save_data()
|
|
||||||
|
self._data.user[self._id][ATTR_IMAGE] = image
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self):
|
def options(self):
|
||||||
@@ -125,7 +151,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@options.setter
|
@options.setter
|
||||||
def options(self, value):
|
def options(self, value):
|
||||||
"""Store user addon options."""
|
"""Store user add-on options."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self._data.user[self._id][ATTR_OPTIONS] = {}
|
self._data.user[self._id][ATTR_OPTIONS] = {}
|
||||||
else:
|
else:
|
||||||
@@ -157,7 +183,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of addon."""
|
"""Return name of add-on."""
|
||||||
return self._mesh[ATTR_NAME]
|
return self._mesh[ATTR_NAME]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -172,9 +198,16 @@ class Addon(CoreSysAttributes):
|
|||||||
return self._data.user[self._id][ATTR_UUID]
|
return self._data.user[self._id][ATTR_UUID]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hassio_token(self):
|
||||||
|
"""Return access token for Hass.io API."""
|
||||||
|
if self.is_installed:
|
||||||
|
return self._data.user[self._id].get(ATTR_ACCESS_TOKEN)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
"""Return description of addon."""
|
"""Return description of add-on."""
|
||||||
return self._mesh[ATTR_DESCRIPTON]
|
return self._mesh[ATTR_DESCRIPTON]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -192,56 +225,55 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def repository(self):
|
def repository(self):
|
||||||
"""Return repository of addon."""
|
"""Return repository of add-on."""
|
||||||
return self._mesh[ATTR_REPOSITORY]
|
return self._mesh[ATTR_REPOSITORY]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_version(self):
|
def last_version(self):
|
||||||
"""Return version of addon."""
|
"""Return version of add-on."""
|
||||||
if self._id in self._data.cache:
|
if self._id in self._data.cache:
|
||||||
return self._data.cache[self._id][ATTR_VERSION]
|
return self._data.cache[self._id][ATTR_VERSION]
|
||||||
return self.version_installed
|
return self.version_installed
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def protected(self):
|
def protected(self):
|
||||||
"""Return if addon is in protected mode."""
|
"""Return if add-on is in protected mode."""
|
||||||
if self.is_installed:
|
if self.is_installed:
|
||||||
return self._data.user[self._id][ATTR_PROTECTED]
|
return self._data.user[self._id][ATTR_PROTECTED]
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@protected.setter
|
@protected.setter
|
||||||
def protected(self, value):
|
def protected(self, value):
|
||||||
"""Set addon in protected mode."""
|
"""Set add-on in protected mode."""
|
||||||
self._data.user[self._id][ATTR_PROTECTED] = value
|
self._data.user[self._id][ATTR_PROTECTED] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def startup(self):
|
def startup(self):
|
||||||
"""Return startup type of addon."""
|
"""Return startup type of add-on."""
|
||||||
return self._mesh.get(ATTR_STARTUP)
|
return self._mesh.get(ATTR_STARTUP)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):
|
def services_role(self):
|
||||||
"""Return dict of services with rights."""
|
"""Return dict of services with rights."""
|
||||||
raw_services = self._mesh.get(ATTR_SERVICES)
|
raw_services = self._mesh.get(ATTR_SERVICES)
|
||||||
if not raw_services:
|
if not raw_services:
|
||||||
return None
|
return {}
|
||||||
|
|
||||||
formated_services = {}
|
services = {}
|
||||||
for data in raw_services:
|
for data in raw_services:
|
||||||
service = RE_SERVICE.match(data)
|
service = RE_SERVICE.match(data)
|
||||||
formated_services[service.group('service')] = \
|
services[service.group('service')] = service.group('rights')
|
||||||
service.group('rights') or 'ro'
|
|
||||||
|
|
||||||
return formated_services
|
return services
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def discovery(self):
|
def discovery(self):
|
||||||
"""Return list of discoverable components/platforms."""
|
"""Return list of discoverable components/platforms."""
|
||||||
return self._mesh.get(ATTR_DISCOVERY)
|
return self._mesh.get(ATTR_DISCOVERY, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
"""Return ports of addon."""
|
"""Return ports of add-on."""
|
||||||
if self.host_network or ATTR_PORTS not in self._mesh:
|
if self.host_network or ATTR_PORTS not in self._mesh:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -252,7 +284,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@ports.setter
|
@ports.setter
|
||||||
def ports(self, value):
|
def ports(self, value):
|
||||||
"""Set custom ports of addon."""
|
"""Set custom ports of add-on."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self._data.user[self._id].pop(ATTR_NETWORK, None)
|
self._data.user[self._id].pop(ATTR_NETWORK, None)
|
||||||
else:
|
else:
|
||||||
@@ -296,47 +328,52 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def host_network(self):
|
def host_network(self):
|
||||||
"""Return True if addon run on host network."""
|
"""Return True if add-on run on host network."""
|
||||||
return self._mesh[ATTR_HOST_NETWORK]
|
return self._mesh[ATTR_HOST_NETWORK]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_pid(self):
|
||||||
|
"""Return True if add-on run on host PID namespace."""
|
||||||
|
return self._mesh[ATTR_HOST_PID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host_ipc(self):
|
def host_ipc(self):
|
||||||
"""Return True if addon run on host IPC namespace."""
|
"""Return True if add-on run on host IPC namespace."""
|
||||||
return self._mesh[ATTR_HOST_IPC]
|
return self._mesh[ATTR_HOST_IPC]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host_dbus(self):
|
def host_dbus(self):
|
||||||
"""Return True if addon run on host DBUS."""
|
"""Return True if add-on run on host D-BUS."""
|
||||||
return self._mesh[ATTR_HOST_DBUS]
|
return self._mesh[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
"""Return devices of addon."""
|
"""Return devices of add-on."""
|
||||||
return self._mesh.get(ATTR_DEVICES)
|
return self._mesh.get(ATTR_DEVICES)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_uart(self):
|
def auto_uart(self):
|
||||||
"""Return True if we should map all uart device."""
|
"""Return True if we should map all UART device."""
|
||||||
return self._mesh.get(ATTR_AUTO_UART)
|
return self._mesh.get(ATTR_AUTO_UART)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tmpfs(self):
|
def tmpfs(self):
|
||||||
"""Return tmpfs of addon."""
|
"""Return tmpfs of add-on."""
|
||||||
return self._mesh.get(ATTR_TMPFS)
|
return self._mesh.get(ATTR_TMPFS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self):
|
def environment(self):
|
||||||
"""Return environment of addon."""
|
"""Return environment of add-on."""
|
||||||
return self._mesh.get(ATTR_ENVIRONMENT)
|
return self._mesh.get(ATTR_ENVIRONMENT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def privileged(self):
|
def privileged(self):
|
||||||
"""Return list of privilege."""
|
"""Return list of privilege."""
|
||||||
return self._mesh.get(ATTR_PRIVILEGED)
|
return self._mesh.get(ATTR_PRIVILEGED, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def apparmor(self):
|
def apparmor(self):
|
||||||
"""Return True if apparmor is enabled."""
|
"""Return True if AppArmor is enabled."""
|
||||||
if not self._mesh.get(ATTR_APPARMOR):
|
if not self._mesh.get(ATTR_APPARMOR):
|
||||||
return SECURITY_DISABLE
|
return SECURITY_DISABLE
|
||||||
elif self.sys_host.apparmor.exists(self.slug):
|
elif self.sys_host.apparmor.exists(self.slug):
|
||||||
@@ -345,24 +382,29 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def legacy(self):
|
def legacy(self):
|
||||||
"""Return if the add-on don't support hass labels."""
|
"""Return if the add-on don't support Home Assistant labels."""
|
||||||
return self._mesh.get(ATTR_LEGACY)
|
return self._mesh.get(ATTR_LEGACY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_docker_api(self):
|
def access_docker_api(self):
|
||||||
"""Return if the add-on need read-only docker API access."""
|
"""Return if the add-on need read-only Docker API access."""
|
||||||
return self._mesh.get(ATTR_DOCKER_API)
|
return self._mesh.get(ATTR_DOCKER_API)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_hassio_api(self):
|
def access_hassio_api(self):
|
||||||
"""Return True if the add-on access to hassio api."""
|
"""Return True if the add-on access to Hass.io REASTful API."""
|
||||||
return self._mesh[ATTR_HASSIO_API]
|
return self._mesh[ATTR_HASSIO_API]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_homeassistant_api(self):
|
def access_homeassistant_api(self):
|
||||||
"""Return True if the add-on access to Home-Assistant api proxy."""
|
"""Return True if the add-on access to Home Assistant API proxy."""
|
||||||
return self._mesh[ATTR_HOMEASSISTANT_API]
|
return self._mesh[ATTR_HOMEASSISTANT_API]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hassio_role(self):
|
||||||
|
"""Return Hass.io role for API."""
|
||||||
|
return self._mesh[ATTR_HASSIO_ROLE]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_stdin(self):
|
def with_stdin(self):
|
||||||
"""Return True if the add-on access use stdin input."""
|
"""Return True if the add-on access use stdin input."""
|
||||||
@@ -370,9 +412,14 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@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."""
|
||||||
return self._mesh[ATTR_GPIO]
|
return self._mesh[ATTR_GPIO]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_kernel_modules(self):
|
||||||
|
"""Return True if the add-on access to kernel modules."""
|
||||||
|
return self._mesh[ATTR_KERNEL_MODULES]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_full_access(self):
|
def with_full_access(self):
|
||||||
"""Return True if the add-on want full access to hardware."""
|
"""Return True if the add-on want full access to hardware."""
|
||||||
@@ -383,6 +430,11 @@ class Addon(CoreSysAttributes):
|
|||||||
"""Return True if the add-on read access to devicetree."""
|
"""Return True if the add-on read access to devicetree."""
|
||||||
return self._mesh[ATTR_DEVICETREE]
|
return self._mesh[ATTR_DEVICETREE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_auth_api(self):
|
||||||
|
"""Return True if the add-on access to login/auth backend."""
|
||||||
|
return self._mesh[ATTR_AUTH_API]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_audio(self):
|
def with_audio(self):
|
||||||
"""Return True if the add-on access to audio."""
|
"""Return True if the add-on access to audio."""
|
||||||
@@ -427,7 +479,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
"""Return url of addon."""
|
"""Return URL of add-on."""
|
||||||
return self._mesh.get(ATTR_URL)
|
return self._mesh.get(ATTR_URL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -451,27 +503,47 @@ class Addon(CoreSysAttributes):
|
|||||||
return self._mesh[ATTR_ARCH]
|
return self._mesh[ATTR_ARCH]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def supported_machine(self):
|
||||||
"""Return image name of addon."""
|
"""Return list of supported machine."""
|
||||||
addon_data = self._mesh
|
return self._mesh.get(ATTR_MACHINE) or MACHINE_ALL
|
||||||
|
|
||||||
# Repository with dockerhub images
|
@property
|
||||||
|
def image(self):
|
||||||
|
"""Return image name of add-on."""
|
||||||
|
if self.is_installed:
|
||||||
|
# NOTE: cleanup
|
||||||
|
if ATTR_IMAGE in self._data.user[self._id]:
|
||||||
|
return self._data.user[self._id][ATTR_IMAGE]
|
||||||
|
return self.image_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_name(self):
|
||||||
|
"""Return image name for install/update."""
|
||||||
|
if self.is_detached:
|
||||||
|
addon_data = self._data.system.get(self._id)
|
||||||
|
else:
|
||||||
|
addon_data = self._data.cache.get(self._id)
|
||||||
|
|
||||||
|
# Repository with Dockerhub images
|
||||||
if ATTR_IMAGE in addon_data:
|
if ATTR_IMAGE in addon_data:
|
||||||
return addon_data[ATTR_IMAGE].format(arch=self.sys_arch)
|
arch = self.sys_arch.match(addon_data[ATTR_ARCH])
|
||||||
|
return addon_data[ATTR_IMAGE].format(arch=arch)
|
||||||
|
|
||||||
# local build
|
# local build
|
||||||
return "{}/{}-addon-{}".format(
|
return (f"{addon_data[ATTR_REPOSITORY]}/"
|
||||||
addon_data[ATTR_REPOSITORY], self.sys_arch,
|
f"{self.sys_arch.default}-"
|
||||||
addon_data[ATTR_SLUG])
|
f"addon-{addon_data[ATTR_SLUG]}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def need_build(self):
|
def need_build(self):
|
||||||
"""Return True if this addon need a local build."""
|
"""Return True if this add-on need a local build."""
|
||||||
return ATTR_IMAGE not in self._mesh
|
if self.is_detached:
|
||||||
|
return ATTR_IMAGE not in self._data.system.get(self._id)
|
||||||
|
return ATTR_IMAGE not in self._data.cache.get(self._id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def map_volumes(self):
|
def map_volumes(self):
|
||||||
"""Return a dict of {volume: policy} from addon."""
|
"""Return a dict of {volume: policy} from add-on."""
|
||||||
volumes = {}
|
volumes = {}
|
||||||
for volume in self._mesh[ATTR_MAP]:
|
for volume in self._mesh[ATTR_MAP]:
|
||||||
result = RE_VOLUME.match(volume)
|
result = RE_VOLUME.match(volume)
|
||||||
@@ -481,37 +553,37 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_data(self):
|
def path_data(self):
|
||||||
"""Return addon data path inside supervisor."""
|
"""Return add-on data path inside Supervisor."""
|
||||||
return Path(self.sys_config.path_addons_data, self._id)
|
return Path(self.sys_config.path_addons_data, self._id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_data(self):
|
def path_extern_data(self):
|
||||||
"""Return addon data path external for docker."""
|
"""Return add-on data path external for Docker."""
|
||||||
return PurePath(self.sys_config.path_extern_addons_data, self._id)
|
return PurePath(self.sys_config.path_extern_addons_data, self._id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_options(self):
|
def path_options(self):
|
||||||
"""Return path to addons options."""
|
"""Return path to add-on options."""
|
||||||
return Path(self.path_data, "options.json")
|
return Path(self.path_data, "options.json")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_location(self):
|
def path_location(self):
|
||||||
"""Return path to this addon."""
|
"""Return path to this add-on."""
|
||||||
return Path(self._mesh[ATTR_LOCATON])
|
return Path(self._mesh[ATTR_LOCATON])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_icon(self):
|
def path_icon(self):
|
||||||
"""Return path to addon icon."""
|
"""Return path to add-on icon."""
|
||||||
return Path(self.path_location, 'icon.png')
|
return Path(self.path_location, 'icon.png')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_logo(self):
|
def path_logo(self):
|
||||||
"""Return path to addon logo."""
|
"""Return path to add-on logo."""
|
||||||
return Path(self.path_location, 'logo.png')
|
return Path(self.path_location, 'logo.png')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_changelog(self):
|
def path_changelog(self):
|
||||||
"""Return path to addon changelog."""
|
"""Return path to add-on changelog."""
|
||||||
return Path(self.path_location, 'CHANGELOG.md')
|
return Path(self.path_location, 'CHANGELOG.md')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -526,15 +598,15 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_asound(self):
|
def path_extern_asound(self):
|
||||||
"""Return path to asound config for docker."""
|
"""Return path to asound config for Docker."""
|
||||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound")
|
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_asound")
|
||||||
|
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
"""Save data of addon."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
self.sys_addons.data.save_data()
|
||||||
|
|
||||||
def write_options(self):
|
def write_options(self):
|
||||||
"""Return True if addon options is written to data."""
|
"""Return True if add-on options is written to data."""
|
||||||
schema = self.schema
|
schema = self.schema
|
||||||
options = self.options
|
options = self.options
|
||||||
|
|
||||||
@@ -542,15 +614,22 @@ class Addon(CoreSysAttributes):
|
|||||||
schema(options)
|
schema(options)
|
||||||
write_json_file(self.path_options, options)
|
write_json_file(self.path_options, options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.error("Addon %s have wrong options: %s", self._id,
|
_LOGGER.error("Add-on %s have wrong options: %s", self._id,
|
||||||
humanize_error(options, ex))
|
humanize_error(options, ex))
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
_LOGGER.error("Addon %s can't write options: %s", self._id, err)
|
_LOGGER.error("Add-on %s can't write options: %s", self._id, err)
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def remove_discovery(self):
|
||||||
|
"""Remove all discovery message from add-on."""
|
||||||
|
for message in self.sys_discovery.list_messages:
|
||||||
|
if message.addon != self.slug:
|
||||||
|
continue
|
||||||
|
self.sys_discovery.remove(message)
|
||||||
|
|
||||||
def write_asound(self):
|
def write_asound(self):
|
||||||
"""Write asound config to file and return True on success."""
|
"""Write asound config to file and return True on success."""
|
||||||
asound_config = self.sys_host.alsa.asound(
|
asound_config = self.sys_host.alsa.asound(
|
||||||
@@ -560,7 +639,7 @@ class Addon(CoreSysAttributes):
|
|||||||
with self.path_asound.open('w') as config_file:
|
with self.path_asound.open('w') as config_file:
|
||||||
config_file.write(asound_config)
|
config_file.write(asound_config)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error("Addon %s can't write asound: %s", self._id, err)
|
_LOGGER.error("Add-on %s can't write asound: %s", self._id, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -588,15 +667,15 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def schema(self):
|
def schema(self):
|
||||||
"""Create a schema for addon options."""
|
"""Create a schema for add-on options."""
|
||||||
raw_schema = self._mesh[ATTR_SCHEMA]
|
raw_schema = self._mesh[ATTR_SCHEMA]
|
||||||
|
|
||||||
if isinstance(raw_schema, bool):
|
if isinstance(raw_schema, bool):
|
||||||
return vol.Schema(dict)
|
return vol.Schema(dict)
|
||||||
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
||||||
|
|
||||||
def test_udpate_schema(self):
|
def test_update_schema(self):
|
||||||
"""Check if the exists config valid after update."""
|
"""Check if the existing configuration is valid after update."""
|
||||||
if not self.is_installed or self.is_detached:
|
if not self.is_installed or self.is_detached:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -626,39 +705,41 @@ class Addon(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def install(self):
|
async def install(self):
|
||||||
"""Install an addon."""
|
"""Install an add-on."""
|
||||||
if self.sys_arch not in self.supported_arch:
|
if not self.available:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Addon %s not supported on %s", self._id, self.sys_arch)
|
"Add-on %s not supported on %s with %s architecture",
|
||||||
|
self._id, self.sys_machine, self.sys_arch.supported)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.is_installed:
|
if self.is_installed:
|
||||||
_LOGGER.error("Addon %s is already installed", self._id)
|
_LOGGER.error("Add-on %s is already installed", self._id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Create Home-Assistant addon data folder %s", self.path_data)
|
"Create Home Assistant add-on data folder %s", self.path_data)
|
||||||
self.path_data.mkdir()
|
self.path_data.mkdir()
|
||||||
|
|
||||||
# Setup/Fix AppArmor profile
|
# Setup/Fix AppArmor profile
|
||||||
await self._install_apparmor()
|
await self._install_apparmor()
|
||||||
|
|
||||||
if not await self.instance.install(self.last_version):
|
if not await self.instance.install(
|
||||||
|
self.last_version, self.image_name):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._set_install(self.last_version)
|
self._set_install(self.image_name, self.last_version)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def uninstall(self):
|
async def uninstall(self):
|
||||||
"""Remove an addon."""
|
"""Remove an add-on."""
|
||||||
if not await self.instance.remove():
|
if not await self.instance.remove():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.path_data.is_dir():
|
if self.path_data.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Remove Home-Assistant addon data folder %s", self.path_data)
|
"Remove Home Assistant add-on data folder %s", self.path_data)
|
||||||
await remove_data(self.path_data)
|
await remove_data(self.path_data)
|
||||||
|
|
||||||
# Cleanup audio settings
|
# Cleanup audio settings
|
||||||
@@ -666,16 +747,19 @@ class Addon(CoreSysAttributes):
|
|||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
self.path_asound.unlink()
|
self.path_asound.unlink()
|
||||||
|
|
||||||
# Cleanup apparmor profile
|
# Cleanup AppArmor profile
|
||||||
if self.sys_host.apparmor.exists(self.slug):
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
with suppress(HostAppArmorError):
|
with suppress(HostAppArmorError):
|
||||||
await self.sys_host.apparmor.remove_profile(self.slug)
|
await self.sys_host.apparmor.remove_profile(self.slug)
|
||||||
|
|
||||||
|
# Remove discovery messages
|
||||||
|
self.remove_discovery()
|
||||||
|
|
||||||
self._set_uninstall()
|
self._set_uninstall()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def state(self):
|
async def state(self):
|
||||||
"""Return running state of addon."""
|
"""Return running state of add-on."""
|
||||||
if not self.is_installed:
|
if not self.is_installed:
|
||||||
return STATE_NONE
|
return STATE_NONE
|
||||||
|
|
||||||
@@ -685,7 +769,15 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Set options and start addon."""
|
"""Set options and start add-on."""
|
||||||
|
if await self.instance.is_running():
|
||||||
|
_LOGGER.warning("%s already running!", self.slug)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Access Token
|
||||||
|
self._data.user[self._id][ATTR_ACCESS_TOKEN] = create_token()
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
if not self.write_options():
|
if not self.write_options():
|
||||||
return False
|
return False
|
||||||
@@ -698,7 +790,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop addon.
|
"""Stop add-on.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
@@ -706,16 +798,17 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def update(self):
|
async def update(self):
|
||||||
"""Update addon."""
|
"""Update add-on."""
|
||||||
last_state = await self.state()
|
last_state = await self.state()
|
||||||
|
|
||||||
if self.last_version == self.version_installed:
|
if self.last_version == self.version_installed:
|
||||||
_LOGGER.warning("No update available for Addon %s", self._id)
|
_LOGGER.warning("No update available for add-on %s", self._id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not await self.instance.update(self.last_version):
|
if not await self.instance.update(
|
||||||
|
self.last_version, self.image_name):
|
||||||
return False
|
return False
|
||||||
self._set_update(self.last_version)
|
self._set_update(self.image_name, self.last_version)
|
||||||
|
|
||||||
# Setup/Fix AppArmor profile
|
# Setup/Fix AppArmor profile
|
||||||
await self._install_apparmor()
|
await self._install_apparmor()
|
||||||
@@ -727,13 +820,13 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
"""Restart addon."""
|
"""Restart add-on."""
|
||||||
await self.stop()
|
await self.stop()
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
def logs(self):
|
def logs(self):
|
||||||
"""Return addons log output.
|
"""Return add-ons log output.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
@@ -749,11 +842,11 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def rebuild(self):
|
async def rebuild(self):
|
||||||
"""Performe a rebuild of local build addon."""
|
"""Perform a rebuild of local build add-on."""
|
||||||
last_state = await self.state()
|
last_state = await self.state()
|
||||||
|
|
||||||
if not self.need_build:
|
if not self.need_build:
|
||||||
_LOGGER.error("Can't rebuild a none local build addon!")
|
_LOGGER.error("Can't rebuild a none local build add-on!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# remove docker container but not addon config
|
# remove docker container but not addon config
|
||||||
@@ -782,7 +875,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
@check_installed
|
@check_installed
|
||||||
async def snapshot(self, tar_file):
|
async def snapshot(self, tar_file):
|
||||||
"""Snapshot state of an addon."""
|
"""Snapshot state of an add-on."""
|
||||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
||||||
# store local image
|
# store local image
|
||||||
if self.need_build and not await \
|
if self.need_build and not await \
|
||||||
@@ -796,7 +889,7 @@ class Addon(CoreSysAttributes):
|
|||||||
ATTR_STATE: await self.state(),
|
ATTR_STATE: await self.state(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# store local configs/state
|
# Store local configs/state
|
||||||
try:
|
try:
|
||||||
write_json_file(Path(temp, 'addon.json'), data)
|
write_json_file(Path(temp, 'addon.json'), data)
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
@@ -820,7 +913,7 @@ class Addon(CoreSysAttributes):
|
|||||||
snapshot.add(self.path_data, arcname="data")
|
snapshot.add(self.path_data, arcname="data")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Build snapshot for addon %s", self._id)
|
_LOGGER.info("Build snapshot for add-on %s", self._id)
|
||||||
await self.sys_run_in_executor(_write_tarfile)
|
await self.sys_run_in_executor(_write_tarfile)
|
||||||
except (tarfile.TarError, OSError) as err:
|
except (tarfile.TarError, OSError) as err:
|
||||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||||
@@ -830,7 +923,7 @@ class Addon(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def restore(self, tar_file):
|
async def restore(self, tar_file):
|
||||||
"""Restore state of an addon."""
|
"""Restore state of an add-on."""
|
||||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
||||||
# extract snapshot
|
# extract snapshot
|
||||||
def _extract_tarfile():
|
def _extract_tarfile():
|
||||||
@@ -844,13 +937,13 @@ class Addon(CoreSysAttributes):
|
|||||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# read snapshot data
|
# Read snapshot data
|
||||||
try:
|
try:
|
||||||
data = read_json_file(Path(temp, 'addon.json'))
|
data = read_json_file(Path(temp, 'addon.json'))
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
_LOGGER.error("Can't read addon.json: %s", err)
|
_LOGGER.error("Can't read addon.json: %s", err)
|
||||||
|
|
||||||
# validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
data = SCHEMA_ADDON_SNAPSHOT(data)
|
data = SCHEMA_ADDON_SNAPSHOT(data)
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
@@ -858,11 +951,11 @@ class Addon(CoreSysAttributes):
|
|||||||
self._id, humanize_error(data, err))
|
self._id, humanize_error(data, err))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# restore data / reload addon
|
# Restore data or reload add-on
|
||||||
_LOGGER.info("Restore config for addon %s", self._id)
|
_LOGGER.info("Restore config for addon %s", self._id)
|
||||||
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM])
|
self._restore_data(data[ATTR_USER], data[ATTR_SYSTEM], self.image_name)
|
||||||
|
|
||||||
# check version / restore image
|
# Check version / restore image
|
||||||
version = data[ATTR_VERSION]
|
version = data[ATTR_VERSION]
|
||||||
if not await self.instance.exists():
|
if not await self.instance.exists():
|
||||||
_LOGGER.info("Restore image for addon %s", self._id)
|
_LOGGER.info("Restore image for addon %s", self._id)
|
||||||
@@ -871,12 +964,12 @@ class Addon(CoreSysAttributes):
|
|||||||
if image_file.is_file():
|
if image_file.is_file():
|
||||||
await self.instance.import_image(image_file, version)
|
await self.instance.import_image(image_file, version)
|
||||||
else:
|
else:
|
||||||
if await self.instance.install(version):
|
if await self.instance.install(version, self.image_name):
|
||||||
await self.instance.cleanup()
|
await self.instance.cleanup()
|
||||||
else:
|
else:
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
|
|
||||||
# restore data
|
# Restore data
|
||||||
def _restore_data():
|
def _restore_data():
|
||||||
"""Restore data."""
|
"""Restore data."""
|
||||||
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
|
shutil.copytree(str(Path(temp, "data")), str(self.path_data))
|
||||||
@@ -900,9 +993,9 @@ class Addon(CoreSysAttributes):
|
|||||||
_LOGGER.error("Can't restore AppArmor profile")
|
_LOGGER.error("Can't restore AppArmor profile")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# run addon
|
# Run add-on
|
||||||
if data[ATTR_STATE] == STATE_STARTED:
|
if data[ATTR_STATE] == STATE_STARTED:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
_LOGGER.info("Finish restore for addon %s", self._id)
|
_LOGGER.info("Finish restore for add-on %s", self._id)
|
||||||
return True
|
return True
|
||||||
|
@@ -1,50 +1,55 @@
|
|||||||
"""HassIO addons build environment."""
|
"""Hass.io add-on build environment."""
|
||||||
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from .validate import SCHEMA_BUILD_CONFIG, BASE_IMAGE
|
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
|
||||||
from ..const import ATTR_SQUASH, ATTR_BUILD_FROM, ATTR_ARGS, META_ADDON
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..coresys import CoreSysAttributes
|
|
||||||
from ..utils.json import JsonConfig
|
from ..utils.json import JsonConfig
|
||||||
|
from .validate import SCHEMA_BUILD_CONFIG
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .addon import Addon
|
||||||
|
|
||||||
|
|
||||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||||
"""Handle build options for addons."""
|
"""Handle build options for add-ons."""
|
||||||
|
|
||||||
def __init__(self, coresys, slug):
|
def __init__(self, coresys: CoreSys, slug: str) -> None:
|
||||||
"""Initialize addon builder."""
|
"""Initialize Hass.io add-on builder."""
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self._id = slug
|
self._id: str = slug
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
|
Path(self.addon.path_location, 'build.json'), SCHEMA_BUILD_CONFIG)
|
||||||
|
|
||||||
def save_data(self):
|
def save_data(self):
|
||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addon(self):
|
def addon(self) -> Addon:
|
||||||
"""Return addon of build data."""
|
"""Return add-on of build data."""
|
||||||
return self.sys_addons.get(self._id)
|
return self.sys_addons.get(self._id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_image(self):
|
def base_image(self) -> str:
|
||||||
"""Base images for this addon."""
|
"""Base images for this add-on."""
|
||||||
return self._data[ATTR_BUILD_FROM].get(
|
return self._data[ATTR_BUILD_FROM].get(
|
||||||
self.sys_arch, BASE_IMAGE[self.sys_arch])
|
self.sys_arch.default,
|
||||||
|
f"homeassistant/{self.sys_arch.default}-base:latest")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def squash(self):
|
def squash(self) -> bool:
|
||||||
"""Return True or False if squash is active."""
|
"""Return True or False if squash is active."""
|
||||||
return self._data[ATTR_SQUASH]
|
return self._data[ATTR_SQUASH]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def additional_args(self):
|
def additional_args(self) -> Dict[str, str]:
|
||||||
"""Return additional docker build arguments."""
|
"""Return additional Docker build arguments."""
|
||||||
return self._data[ATTR_ARGS]
|
return self._data[ATTR_ARGS]
|
||||||
|
|
||||||
def get_docker_args(self, version):
|
def get_docker_args(self, version):
|
||||||
"""Create a dict with docker build arguments."""
|
"""Create a dict with Docker build arguments."""
|
||||||
args = {
|
args = {
|
||||||
'path': str(self.addon.path_location),
|
'path': str(self.addon.path_location),
|
||||||
'tag': f"{self.addon.image}:{version}",
|
'tag': f"{self.addon.image}:{version}",
|
||||||
@@ -53,7 +58,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
'squash': self.squash,
|
'squash': self.squash,
|
||||||
'labels': {
|
'labels': {
|
||||||
'io.hass.version': version,
|
'io.hass.version': version,
|
||||||
'io.hass.arch': self.sys_arch,
|
'io.hass.arch': self.sys_arch.default,
|
||||||
'io.hass.type': META_ADDON,
|
'io.hass.type': META_ADDON,
|
||||||
'io.hass.name': self._fix_label('name'),
|
'io.hass.name': self._fix_label('name'),
|
||||||
'io.hass.description': self._fix_label('description'),
|
'io.hass.description': self._fix_label('description'),
|
||||||
@@ -61,7 +66,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
'buildargs': {
|
'buildargs': {
|
||||||
'BUILD_FROM': self.base_image,
|
'BUILD_FROM': self.base_image,
|
||||||
'BUILD_VERSION': version,
|
'BUILD_VERSION': version,
|
||||||
'BUILD_ARCH': self.sys_arch,
|
'BUILD_ARCH': self.sys_arch.default,
|
||||||
**self.additional_args,
|
**self.additional_args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _fix_label(self, label_name):
|
def _fix_label(self, label_name: str) -> str:
|
||||||
"""Remove characters they are not supported."""
|
"""Remove characters they are not supported."""
|
||||||
label = getattr(self.addon, label_name, "")
|
label = getattr(self.addon, label_name, "")
|
||||||
return label.replace("'", "")
|
return label.replace("'", "")
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for Hass.io add-on data."""
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
class AddonsData(JsonConfig, CoreSysAttributes):
|
||||||
"""Hold data for addons inside HassIO."""
|
"""Hold data for Add-ons inside Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
@@ -30,26 +30,26 @@ class AddonsData(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
"""Return local addon user data."""
|
"""Return local add-on user data."""
|
||||||
return self._data[ATTR_USER]
|
return self._data[ATTR_USER]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def system(self):
|
def system(self):
|
||||||
"""Return local addon data."""
|
"""Return local add-on data."""
|
||||||
return self._data[ATTR_SYSTEM]
|
return self._data[ATTR_SYSTEM]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cache(self):
|
def cache(self):
|
||||||
"""Return addon data from cache/repositories."""
|
"""Return add-on data from cache/repositories."""
|
||||||
return self._cache
|
return self._cache
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repositories(self):
|
def repositories(self):
|
||||||
"""Return addon data from repositories."""
|
"""Return add-on data from repositories."""
|
||||||
return self._repositories
|
return self._repositories
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""Read data from addons repository."""
|
"""Read data from add-on repository."""
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
self._repositories = {}
|
self._repositories = {}
|
||||||
|
|
||||||
@@ -94,14 +94,24 @@ class AddonsData(JsonConfig, CoreSysAttributes):
|
|||||||
self._read_addons_folder(path, slug)
|
self._read_addons_folder(path, slug)
|
||||||
|
|
||||||
def _read_addons_folder(self, path, repository):
|
def _read_addons_folder(self, path, repository):
|
||||||
"""Read data from addons folder."""
|
"""Read data from add-ons folder."""
|
||||||
for addon in path.glob("**/config.json"):
|
for addon in path.glob("**/config.json"):
|
||||||
try:
|
try:
|
||||||
addon_config = read_json_file(addon)
|
addon_config = read_json_file(addon)
|
||||||
|
|
||||||
|
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
||||||
|
_LOGGER.warning("Can't read %s", addon)
|
||||||
|
continue
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
|
try:
|
||||||
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
||||||
|
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
_LOGGER.warning("Can't read %s: %s", addon,
|
||||||
|
humanize_error(addon_config, ex))
|
||||||
|
continue
|
||||||
|
|
||||||
# Generate slug
|
# Generate slug
|
||||||
addon_slug = "{}_{}".format(
|
addon_slug = "{}_{}".format(
|
||||||
repository, addon_config[ATTR_SLUG])
|
repository, addon_config[ATTR_SLUG])
|
||||||
@@ -111,17 +121,10 @@ class AddonsData(JsonConfig, CoreSysAttributes):
|
|||||||
addon_config[ATTR_LOCATON] = str(addon.parent)
|
addon_config[ATTR_LOCATON] = str(addon.parent)
|
||||||
self._cache[addon_slug] = addon_config
|
self._cache[addon_slug] = addon_config
|
||||||
|
|
||||||
except (OSError, json.JSONDecodeError):
|
|
||||||
_LOGGER.warning("Can't read %s", addon)
|
|
||||||
|
|
||||||
except vol.Invalid as ex:
|
|
||||||
_LOGGER.warning("Can't read %s: %s", addon,
|
|
||||||
humanize_error(addon_config, ex))
|
|
||||||
|
|
||||||
def _set_builtin_repositories(self):
|
def _set_builtin_repositories(self):
|
||||||
"""Add local built-in repository into dataset."""
|
"""Add local built-in repository into dataset."""
|
||||||
try:
|
try:
|
||||||
builtin_file = Path(__file__).parent.joinpath('built-in.json')
|
builtin_file = Path(__file__).parent.joinpath("built-in.json")
|
||||||
builtin_data = read_json_file(builtin_file)
|
builtin_data = read_json_file(builtin_file)
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
_LOGGER.warning("Can't read built-in json: %s", err)
|
_LOGGER.warning("Can't read built-in json: %s", err)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO addons git."""
|
"""Init file for Hass.io add-on Git."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import functools as ft
|
import functools as ft
|
||||||
@@ -16,10 +16,10 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GitRepo(CoreSysAttributes):
|
class GitRepo(CoreSysAttributes):
|
||||||
"""Manage addons git repo."""
|
"""Manage Add-on Git repository."""
|
||||||
|
|
||||||
def __init__(self, coresys, path, url):
|
def __init__(self, coresys, path, url):
|
||||||
"""Initialize git base wrapper."""
|
"""Initialize Git base wrapper."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self.repo = None
|
self.repo = None
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -38,13 +38,13 @@ class GitRepo(CoreSysAttributes):
|
|||||||
return self._data[ATTR_BRANCH]
|
return self._data[ATTR_BRANCH]
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Init git addon repo."""
|
"""Init Git add-on repository."""
|
||||||
if not self.path.is_dir():
|
if not self.path.is_dir():
|
||||||
return await self.clone()
|
return await self.clone()
|
||||||
|
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Load addon %s repository", self.path)
|
_LOGGER.info("Load add-on %s repository", self.path)
|
||||||
self.repo = await self.sys_run_in_executor(
|
self.repo = await self.sys_run_in_executor(
|
||||||
git.Repo, str(self.path))
|
git.Repo, str(self.path))
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ class GitRepo(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def clone(self):
|
async def clone(self):
|
||||||
"""Clone git addon repo."""
|
"""Clone git add-on repository."""
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
git_args = {
|
git_args = {
|
||||||
attribute: value
|
attribute: value
|
||||||
@@ -70,7 +70,7 @@ class GitRepo(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Clone addon %s repository", self.url)
|
_LOGGER.info("Clone add-on %s repository", self.url)
|
||||||
self.repo = await self.sys_run_in_executor(ft.partial(
|
self.repo = await self.sys_run_in_executor(ft.partial(
|
||||||
git.Repo.clone_from, self.url, str(self.path),
|
git.Repo.clone_from, self.url, str(self.path),
|
||||||
**git_args
|
**git_args
|
||||||
@@ -78,20 +78,20 @@ class GitRepo(CoreSysAttributes):
|
|||||||
|
|
||||||
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
|
||||||
git.GitCommandError) as err:
|
git.GitCommandError) as err:
|
||||||
_LOGGER.error("Can't clone %s repo: %s.", self.url, err)
|
_LOGGER.error("Can't clone %s repository: %s.", self.url, err)
|
||||||
self._remove()
|
self._remove()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def pull(self):
|
async def pull(self):
|
||||||
"""Pull git addon repo."""
|
"""Pull Git add-on repo."""
|
||||||
if self.lock.locked():
|
if self.lock.locked():
|
||||||
_LOGGER.warning("It is already a task in progress.")
|
_LOGGER.warning("It is already a task in progress")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
_LOGGER.info("Update addon %s repository", self.url)
|
_LOGGER.info("Update add-on %s repository", self.url)
|
||||||
branch = self.repo.active_branch.name
|
branch = self.repo.active_branch.name
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -130,19 +130,19 @@ class GitRepo(CoreSysAttributes):
|
|||||||
|
|
||||||
|
|
||||||
class GitRepoHassIO(GitRepo):
|
class GitRepoHassIO(GitRepo):
|
||||||
"""HassIO addons repository."""
|
"""Hass.io add-ons repository."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize git hassio addon repository."""
|
"""Initialize Git Hass.io add-on repository."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS)
|
coresys, coresys.config.path_addons_core, URL_HASSIO_ADDONS)
|
||||||
|
|
||||||
|
|
||||||
class GitRepoCustom(GitRepo):
|
class GitRepoCustom(GitRepo):
|
||||||
"""Custom addons repository."""
|
"""Custom add-ons repository."""
|
||||||
|
|
||||||
def __init__(self, coresys, url):
|
def __init__(self, coresys, url):
|
||||||
"""Initialize git hassio addon repository."""
|
"""Initialize custom Git Hass.io addo-n repository."""
|
||||||
path = Path(
|
path = Path(
|
||||||
coresys.config.path_addons_git,
|
coresys.config.path_addons_git,
|
||||||
get_hash_from_repository(url))
|
get_hash_from_repository(url))
|
||||||
@@ -151,5 +151,5 @@ class GitRepoCustom(GitRepo):
|
|||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove a custom repository."""
|
"""Remove a custom repository."""
|
||||||
_LOGGER.info("Remove custom addon repository %s", self.url)
|
_LOGGER.info("Remove custom add-on repository %s", self.url)
|
||||||
self._remove()
|
self._remove()
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
"""Represent a HassIO repository."""
|
"""Represent a Hass.io repository."""
|
||||||
from .git import GitRepoHassIO, GitRepoCustom
|
from .git import GitRepoHassIO, GitRepoCustom
|
||||||
from .utils import get_hash_from_repository
|
from .utils import get_hash_from_repository
|
||||||
from ..const import (
|
from ..const import (
|
||||||
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
|
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_NAME, ATTR_URL, ATTR_MAINTAINER)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
UNKNOWN = 'unknown'
|
UNKNOWN = 'unknown'
|
||||||
|
|
||||||
|
|
||||||
class Repository(CoreSysAttributes):
|
class Repository(CoreSysAttributes):
|
||||||
"""Repository in HassIO."""
|
"""Repository in Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys, repository):
|
def __init__(self, coresys, repository):
|
||||||
"""Initialize repository object."""
|
"""Initialize repository object."""
|
||||||
@@ -44,7 +45,7 @@ class Repository(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
"""Return url of repository."""
|
"""Return URL of repository."""
|
||||||
return self._mesh.get(ATTR_URL, self.source)
|
return self._mesh.get(ATTR_URL, self.source)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -59,14 +60,14 @@ class Repository(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def update(self):
|
async def update(self):
|
||||||
"""Update addon repository."""
|
"""Update add-on repository."""
|
||||||
if self.git:
|
if self.git:
|
||||||
return await self.git.pull()
|
return await self.git.pull()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove addon repository."""
|
"""Remove add-on repository."""
|
||||||
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
if self._id in (REPOSITORY_CORE, REPOSITORY_LOCAL):
|
||||||
raise RuntimeError("Can't remove built-in repositories!")
|
raise APIError("Can't remove built-in repositories!")
|
||||||
|
|
||||||
self.git.remove()
|
self.git.remove()
|
||||||
|
@@ -1,23 +1,38 @@
|
|||||||
"""Util addons functions."""
|
"""Util add-ons functions."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
SECURITY_DISABLE, SECURITY_PROFILE, PRIVILEGED_NET_ADMIN,
|
PRIVILEGED_DAC_READ_SEARCH,
|
||||||
PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO)
|
PRIVILEGED_NET_ADMIN,
|
||||||
|
PRIVILEGED_SYS_ADMIN,
|
||||||
|
PRIVILEGED_SYS_MODULE,
|
||||||
|
PRIVILEGED_SYS_PTRACE,
|
||||||
|
PRIVILEGED_SYS_RAWIO,
|
||||||
|
ROLE_ADMIN,
|
||||||
|
ROLE_MANAGER,
|
||||||
|
SECURITY_DISABLE,
|
||||||
|
SECURITY_PROFILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .addon import Addon
|
||||||
|
|
||||||
RE_SHA1 = re.compile(r"[a-f0-9]{8}")
|
RE_SHA1 = re.compile(r"[a-f0-9]{8}")
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def rating_security(addon):
|
def rating_security(addon: Addon) -> int:
|
||||||
"""Return 1-5 for security rating.
|
"""Return 1-6 for security rating.
|
||||||
|
|
||||||
1 = not secure
|
1 = not secure
|
||||||
5 = high secure
|
6 = high secure
|
||||||
"""
|
"""
|
||||||
rating = 5
|
rating = 5
|
||||||
|
|
||||||
@@ -27,19 +42,38 @@ def rating_security(addon):
|
|||||||
elif addon.apparmor == SECURITY_PROFILE:
|
elif addon.apparmor == SECURITY_PROFILE:
|
||||||
rating += 1
|
rating += 1
|
||||||
|
|
||||||
# API Access
|
# Home Assistant Login
|
||||||
if addon.access_hassio_api or addon.access_homeassistant_api:
|
if addon.access_auth_api:
|
||||||
rating += -1
|
rating += 1
|
||||||
|
|
||||||
# Privileged options
|
# Privileged options
|
||||||
if addon.privileged in (PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN,
|
if any(
|
||||||
PRIVILEGED_SYS_RAWIO):
|
privilege in addon.privileged
|
||||||
|
for privilege in (
|
||||||
|
PRIVILEGED_NET_ADMIN,
|
||||||
|
PRIVILEGED_SYS_ADMIN,
|
||||||
|
PRIVILEGED_SYS_RAWIO,
|
||||||
|
PRIVILEGED_SYS_PTRACE,
|
||||||
|
PRIVILEGED_SYS_MODULE,
|
||||||
|
PRIVILEGED_DAC_READ_SEARCH,
|
||||||
|
)
|
||||||
|
):
|
||||||
rating += -1
|
rating += -1
|
||||||
|
|
||||||
|
# API Hass.io role
|
||||||
|
if addon.hassio_role == ROLE_MANAGER:
|
||||||
|
rating += -1
|
||||||
|
elif addon.hassio_role == ROLE_ADMIN:
|
||||||
|
rating += -2
|
||||||
|
|
||||||
# Not secure Networking
|
# Not secure Networking
|
||||||
if addon.host_network:
|
if addon.host_network:
|
||||||
rating += -1
|
rating += -1
|
||||||
|
|
||||||
|
# Insecure PID namespace
|
||||||
|
if addon.host_pid:
|
||||||
|
rating += -2
|
||||||
|
|
||||||
# Full Access
|
# Full Access
|
||||||
if addon.with_full_access:
|
if addon.with_full_access:
|
||||||
rating += -2
|
rating += -2
|
||||||
@@ -51,23 +85,24 @@ def rating_security(addon):
|
|||||||
return max(min(6, rating), 1)
|
return max(min(6, rating), 1)
|
||||||
|
|
||||||
|
|
||||||
def get_hash_from_repository(name):
|
def get_hash_from_repository(name: str) -> str:
|
||||||
"""Generate a hash from repository."""
|
"""Generate a hash from repository."""
|
||||||
key = name.lower().encode()
|
key = name.lower().encode()
|
||||||
return hashlib.sha1(key).hexdigest()[:8]
|
return hashlib.sha1(key).hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
def extract_hash_from_path(path):
|
def extract_hash_from_path(path: Path) -> str:
|
||||||
"""Extract repo id from path."""
|
"""Extract repo id from path."""
|
||||||
repo_dir = path.parts[-1]
|
repository_dir = path.parts[-1]
|
||||||
|
|
||||||
if not RE_SHA1.match(repo_dir):
|
if not RE_SHA1.match(repository_dir):
|
||||||
return get_hash_from_repository(repo_dir)
|
return get_hash_from_repository(repository_dir)
|
||||||
return repo_dir
|
return repository_dir
|
||||||
|
|
||||||
|
|
||||||
def check_installed(method):
|
def check_installed(method):
|
||||||
"""Wrap function with check if addon is installed."""
|
"""Wrap function with check if add-on is installed."""
|
||||||
|
|
||||||
async def wrap_check(addon, *args, **kwargs):
|
async def wrap_check(addon, *args, **kwargs):
|
||||||
"""Return False if not installed or the function."""
|
"""Return False if not installed or the function."""
|
||||||
if not addon.is_installed:
|
if not addon.is_installed:
|
||||||
@@ -78,18 +113,18 @@ def check_installed(method):
|
|||||||
return wrap_check
|
return wrap_check
|
||||||
|
|
||||||
|
|
||||||
async def remove_data(folder):
|
async def remove_data(folder: Path) -> None:
|
||||||
"""Remove folder and reset privileged."""
|
"""Remove folder and reset privileged."""
|
||||||
try:
|
try:
|
||||||
proc = await asyncio.create_subprocess_exec(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"rm", "-rf", str(folder),
|
"rm", "-rf", str(folder), stdout=asyncio.subprocess.DEVNULL
|
||||||
stdout=asyncio.subprocess.DEVNULL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_, error_msg = await proc.communicate()
|
_, error_msg = await proc.communicate()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
error_msg = str(err)
|
error_msg = str(err)
|
||||||
|
else:
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.error("Can't remove Add-on Data: %s", error_msg)
|
_LOGGER.error("Can't remove Add-on Data: %s", error_msg)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Validate addons options schema."""
|
"""Validate add-ons options schema."""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
@@ -6,31 +6,30 @@ import uuid
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
|
ARCH_ALL, ATTR_ACCESS_TOKEN, ATTR_APPARMOR, ATTR_ARCH, ATTR_ARGS,
|
||||||
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE,
|
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_AUTH_API,
|
||||||
STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE,
|
ATTR_AUTO_UART, ATTR_AUTO_UPDATE, ATTR_BOOT, ATTR_BUILD_FROM,
|
||||||
BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER,
|
ATTR_DESCRIPTON, ATTR_DEVICES, ATTR_DEVICETREE, ATTR_DISCOVERY,
|
||||||
ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF,
|
ATTR_DOCKER_API, ATTR_ENVIRONMENT, ATTR_FULL_ACCESS, ATTR_GPIO,
|
||||||
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
|
ATTR_HASSIO_API, ATTR_HASSIO_ROLE, ATTR_HOMEASSISTANT_API, ATTR_HOST_DBUS,
|
||||||
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
|
ATTR_HOST_IPC, ATTR_HOST_NETWORK, ATTR_HOST_PID, ATTR_IMAGE,
|
||||||
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK, ATTR_UUID,
|
ATTR_KERNEL_MODULES, ATTR_LEGACY, ATTR_LOCATON, ATTR_MACHINE,
|
||||||
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC,
|
ATTR_MAINTAINER, ATTR_MAP, ATTR_NAME, ATTR_NETWORK, ATTR_OPTIONS,
|
||||||
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
|
ATTR_PORTS, ATTR_PRIVILEGED, ATTR_PROTECTED, ATTR_REPOSITORY, ATTR_SCHEMA,
|
||||||
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
|
ATTR_SERVICES, ATTR_SLUG, ATTR_SQUASH, ATTR_STARTUP, ATTR_STATE,
|
||||||
ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY,
|
ATTR_STDIN, ATTR_SYSTEM, ATTR_TIMEOUT, ATTR_TMPFS, ATTR_URL, ATTR_USER,
|
||||||
ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API, ATTR_PROTECTED,
|
ATTR_UUID, ATTR_VERSION, ATTR_WEBUI, BOOT_AUTO, BOOT_MANUAL,
|
||||||
ATTR_FULL_ACCESS,
|
PRIVILEGED_ALL, ROLE_ALL, ROLE_DEFAULT, STARTUP_ALL, STARTUP_APPLICATION,
|
||||||
PRIVILEGED_NET_ADMIN, PRIVILEGED_SYS_ADMIN, PRIVILEGED_SYS_RAWIO,
|
STARTUP_SERVICES, STATE_STARTED, STATE_STOPPED)
|
||||||
PRIVILEGED_IPC_LOCK, PRIVILEGED_SYS_TIME, PRIVILEGED_SYS_NICE,
|
from ..services.validate import DISCOVERY_SERVICES
|
||||||
PRIVILEGED_SYS_RESOURCE)
|
from ..validate import (
|
||||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
ALSA_DEVICE, DOCKER_PORTS, NETWORK_PORT, SHA256, UUID_MATCH)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
|
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|ro))?$")
|
||||||
RE_SERVICE = re.compile(r"^(?P<service>mqtt)(?::(?P<rights>rw|:ro))?$")
|
RE_SERVICE = re.compile(r"^(?P<service>mqtt):(?P<rights>provide|want|need)$")
|
||||||
RE_DISCOVERY = re.compile(r"^(?P<component>\w*)(?:/(?P<platform>\w*>))?$")
|
|
||||||
|
|
||||||
V_STR = 'str'
|
V_STR = 'str'
|
||||||
V_INT = 'int'
|
V_INT = 'int'
|
||||||
@@ -50,34 +49,20 @@ RE_SCHEMA_ELEMENT = re.compile(
|
|||||||
r")\??$"
|
r")\??$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RE_DOCKER_IMAGE = re.compile(
|
||||||
|
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
|
||||||
|
RE_DOCKER_IMAGE_BUILD = re.compile(
|
||||||
|
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$")
|
||||||
|
|
||||||
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
||||||
|
|
||||||
ARCH_ALL = [
|
|
||||||
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
|
|
||||||
]
|
|
||||||
|
|
||||||
STARTUP_ALL = [
|
MACHINE_ALL = [
|
||||||
STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES,
|
'intel-nuc', 'odroid-c2', 'odroid-xu', 'orangepi-prime', 'qemux86',
|
||||||
STARTUP_APPLICATION
|
'qemux86-64', 'qemuarm', 'qemuarm-64', 'raspberrypi', 'raspberrypi2',
|
||||||
|
'raspberrypi3', 'raspberrypi3-64', 'tinker',
|
||||||
]
|
]
|
||||||
|
|
||||||
PRIVILEGED_ALL = [
|
|
||||||
PRIVILEGED_NET_ADMIN,
|
|
||||||
PRIVILEGED_SYS_ADMIN,
|
|
||||||
PRIVILEGED_SYS_RAWIO,
|
|
||||||
PRIVILEGED_IPC_LOCK,
|
|
||||||
PRIVILEGED_SYS_TIME,
|
|
||||||
PRIVILEGED_SYS_NICE,
|
|
||||||
PRIVILEGED_SYS_RESOURCE,
|
|
||||||
]
|
|
||||||
|
|
||||||
BASE_IMAGE = {
|
|
||||||
ARCH_ARMHF: "homeassistant/armhf-base:latest",
|
|
||||||
ARCH_AARCH64: "homeassistant/aarch64-base:latest",
|
|
||||||
ARCH_I386: "homeassistant/i386-base:latest",
|
|
||||||
ARCH_AMD64: "homeassistant/amd64-base:latest",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _simple_startup(value):
|
def _simple_startup(value):
|
||||||
"""Simple startup schema."""
|
"""Simple startup schema."""
|
||||||
@@ -96,6 +81,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_URL): vol.Url(),
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
|
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
|
||||||
|
vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)],
|
||||||
vol.Required(ATTR_STARTUP):
|
vol.Required(ATTR_STARTUP):
|
||||||
vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
||||||
vol.Required(ATTR_BOOT):
|
vol.Required(ATTR_BOOT):
|
||||||
@@ -104,6 +90,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_WEBUI):
|
vol.Optional(ATTR_WEBUI):
|
||||||
vol.Match(r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"),
|
vol.Match(r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"),
|
||||||
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_IPC, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
||||||
@@ -118,13 +105,16 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
||||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
vol.Optional(ATTR_STDIN, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
|
vol.Optional(ATTR_LEGACY, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DOCKER_API, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||||
vol.Optional(ATTR_DISCOVERY): [vol.Match(RE_DISCOVERY)],
|
vol.Optional(ATTR_DISCOVERY): [vol.In(DISCOVERY_SERVICES)],
|
||||||
vol.Required(ATTR_OPTIONS): dict,
|
vol.Required(ATTR_OPTIONS): dict,
|
||||||
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
|
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
|
||||||
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
|
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
|
||||||
@@ -137,7 +127,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
}))
|
}))
|
||||||
}), False),
|
}), False),
|
||||||
vol.Optional(ATTR_IMAGE):
|
vol.Optional(ATTR_IMAGE):
|
||||||
vol.Match(r"^([a-zA-Z.:\d{}]+/)*?([\w{}]+)/([\-\w{}]+)$"),
|
vol.Match(RE_DOCKER_IMAGE),
|
||||||
vol.Optional(ATTR_TIMEOUT, default=10):
|
vol.Optional(ATTR_TIMEOUT, default=10):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
|
vol.All(vol.Coerce(int), vol.Range(min=10, max=120)),
|
||||||
}, extra=vol.REMOVE_EXTRA)
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
@@ -153,8 +143,8 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
|
|||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_BUILD_CONFIG = vol.Schema({
|
SCHEMA_BUILD_CONFIG = vol.Schema({
|
||||||
vol.Optional(ATTR_BUILD_FROM, default=BASE_IMAGE): vol.Schema({
|
vol.Optional(ATTR_BUILD_FROM, default=dict): vol.Schema({
|
||||||
vol.In(ARCH_ALL): vol.Match(r"(?:^[\w{}]+/)?[\-\w{}]+:[\.\-\w{}]+$"),
|
vol.In(ARCH_ALL): vol.Match(RE_DOCKER_IMAGE_BUILD),
|
||||||
}),
|
}),
|
||||||
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
vol.Optional(ATTR_SQUASH, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({
|
vol.Optional(ATTR_ARGS, default=dict): vol.Schema({
|
||||||
@@ -166,8 +156,9 @@ SCHEMA_BUILD_CONFIG = vol.Schema({
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_ADDON_USER = vol.Schema({
|
SCHEMA_ADDON_USER = vol.Schema({
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex):
|
vol.Optional(ATTR_IMAGE): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Match(r"^[0-9a-f]{32}$"),
|
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): UUID_MATCH,
|
||||||
|
vol.Optional(ATTR_ACCESS_TOKEN): SHA256,
|
||||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_BOOT):
|
vol.Optional(ATTR_BOOT):
|
||||||
@@ -206,7 +197,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema({
|
|||||||
def validate_options(raw_schema):
|
def validate_options(raw_schema):
|
||||||
"""Validate schema."""
|
"""Validate schema."""
|
||||||
def validate(struct):
|
def validate(struct):
|
||||||
"""Create schema validator for addons options."""
|
"""Create schema validator for add-ons options."""
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
# read options
|
# read options
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
"""Init file for HassIO rest api."""
|
"""Init file for Hass.io RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
|
from .auth import APIAuth
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
from .hardware import APIHardware
|
from .hardware import APIHardware
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .hassos import APIHassOS
|
from .hassos import APIHassOS
|
||||||
|
from .info import APIInfo
|
||||||
from .proxy import APIProxy
|
from .proxy import APIProxy
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
from .snapshots import APISnapshots
|
from .snapshots import APISnapshots
|
||||||
@@ -21,14 +23,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RestAPI(CoreSysAttributes):
|
class RestAPI(CoreSysAttributes):
|
||||||
"""Handle rest api for hassio."""
|
"""Handle RESTful API for Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self.security = SecurityMiddleware(coresys)
|
self.security = SecurityMiddleware(coresys)
|
||||||
self.webapp = web.Application(
|
self.webapp = web.Application(
|
||||||
middlewares=[self.security.token_validation], loop=coresys.loop)
|
middlewares=[self.security.token_validation])
|
||||||
|
|
||||||
# service stuff
|
# service stuff
|
||||||
self._runner = web.AppRunner(self.webapp)
|
self._runner = web.AppRunner(self.webapp)
|
||||||
@@ -47,9 +49,11 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_snapshots()
|
self._register_snapshots()
|
||||||
self._register_discovery()
|
self._register_discovery()
|
||||||
self._register_services()
|
self._register_services()
|
||||||
|
self._register_info()
|
||||||
|
self._register_auth()
|
||||||
|
|
||||||
def _register_host(self):
|
def _register_host(self):
|
||||||
"""Register hostcontrol function."""
|
"""Register hostcontrol functions."""
|
||||||
api_host = APIHost()
|
api_host = APIHost()
|
||||||
api_host.coresys = self.coresys
|
api_host.coresys = self.coresys
|
||||||
|
|
||||||
@@ -62,14 +66,14 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get('/host/services', api_host.services),
|
web.get('/host/services', api_host.services),
|
||||||
web.post('/host/services/{service}/stop', api_host.service_stop),
|
web.post('/host/services/{service}/stop', api_host.service_stop),
|
||||||
web.post('/host/services/{service}/start', api_host.service_start),
|
web.post('/host/services/{service}/start', api_host.service_start),
|
||||||
web.post(
|
web.post('/host/services/{service}/restart',
|
||||||
'/host/services/{service}/restart', api_host.service_restart),
|
api_host.service_restart),
|
||||||
web.post(
|
web.post('/host/services/{service}/reload',
|
||||||
'/host/services/{service}/reload', api_host.service_reload),
|
api_host.service_reload),
|
||||||
])
|
])
|
||||||
|
|
||||||
def _register_hassos(self):
|
def _register_hassos(self):
|
||||||
"""Register hassos function."""
|
"""Register HassOS functions."""
|
||||||
api_hassos = APIHassOS()
|
api_hassos = APIHassOS()
|
||||||
api_hassos.coresys = self.coresys
|
api_hassos.coresys = self.coresys
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def _register_hardware(self):
|
def _register_hardware(self):
|
||||||
"""Register hardware function."""
|
"""Register hardware functions."""
|
||||||
api_hardware = APIHardware()
|
api_hardware = APIHardware()
|
||||||
api_hardware.coresys = self.coresys
|
api_hardware.coresys = self.coresys
|
||||||
|
|
||||||
@@ -90,8 +94,26 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get('/hardware/audio', api_hardware.audio),
|
web.get('/hardware/audio', api_hardware.audio),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def _register_info(self):
|
||||||
|
"""Register info functions."""
|
||||||
|
api_info = APIInfo()
|
||||||
|
api_info.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes([
|
||||||
|
web.get('/info', api_info.info),
|
||||||
|
])
|
||||||
|
|
||||||
|
def _register_auth(self):
|
||||||
|
"""Register auth functions."""
|
||||||
|
api_auth = APIAuth()
|
||||||
|
api_auth.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes([
|
||||||
|
web.post('/auth', api_auth.auth),
|
||||||
|
])
|
||||||
|
|
||||||
def _register_supervisor(self):
|
def _register_supervisor(self):
|
||||||
"""Register supervisor function."""
|
"""Register Supervisor functions."""
|
||||||
api_supervisor = APISupervisor()
|
api_supervisor = APISupervisor()
|
||||||
api_supervisor.coresys = self.coresys
|
api_supervisor.coresys = self.coresys
|
||||||
|
|
||||||
@@ -106,7 +128,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def _register_homeassistant(self):
|
def _register_homeassistant(self):
|
||||||
"""Register homeassistant function."""
|
"""Register Home Assistant functions."""
|
||||||
api_hass = APIHomeAssistant()
|
api_hass = APIHomeAssistant()
|
||||||
api_hass.coresys = self.coresys
|
api_hass.coresys = self.coresys
|
||||||
|
|
||||||
@@ -137,7 +159,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def _register_addons(self):
|
def _register_addons(self):
|
||||||
"""Register homeassistant function."""
|
"""Register Add-on functions."""
|
||||||
api_addons = APIAddons()
|
api_addons = APIAddons()
|
||||||
api_addons.coresys = self.coresys
|
api_addons.coresys = self.coresys
|
||||||
|
|
||||||
@@ -158,11 +180,12 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.get('/addons/{addon}/logo', api_addons.logo),
|
web.get('/addons/{addon}/logo', api_addons.logo),
|
||||||
web.get('/addons/{addon}/changelog', api_addons.changelog),
|
web.get('/addons/{addon}/changelog', api_addons.changelog),
|
||||||
web.post('/addons/{addon}/stdin', api_addons.stdin),
|
web.post('/addons/{addon}/stdin', api_addons.stdin),
|
||||||
|
web.post('/addons/{addon}/security', api_addons.security),
|
||||||
web.get('/addons/{addon}/stats', api_addons.stats),
|
web.get('/addons/{addon}/stats', api_addons.stats),
|
||||||
])
|
])
|
||||||
|
|
||||||
def _register_snapshots(self):
|
def _register_snapshots(self):
|
||||||
"""Register snapshots function."""
|
"""Register snapshots functions."""
|
||||||
api_snapshots = APISnapshots()
|
api_snapshots = APISnapshots()
|
||||||
api_snapshots.coresys = self.coresys
|
api_snapshots.coresys = self.coresys
|
||||||
|
|
||||||
@@ -182,6 +205,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def _register_services(self):
|
def _register_services(self):
|
||||||
|
"""Register services functions."""
|
||||||
api_services = APIServices()
|
api_services = APIServices()
|
||||||
api_services.coresys = self.coresys
|
api_services.coresys = self.coresys
|
||||||
|
|
||||||
@@ -193,19 +217,19 @@ class RestAPI(CoreSysAttributes):
|
|||||||
])
|
])
|
||||||
|
|
||||||
def _register_discovery(self):
|
def _register_discovery(self):
|
||||||
|
"""Register discovery functions."""
|
||||||
api_discovery = APIDiscovery()
|
api_discovery = APIDiscovery()
|
||||||
api_discovery.coresys = self.coresys
|
api_discovery.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes([
|
self.webapp.add_routes([
|
||||||
web.get('/services/discovery', api_discovery.list),
|
web.get('/discovery', api_discovery.list),
|
||||||
web.get('/services/discovery/{uuid}', api_discovery.get_discovery),
|
web.get('/discovery/{uuid}', api_discovery.get_discovery),
|
||||||
web.delete('/services/discovery/{uuid}',
|
web.delete('/discovery/{uuid}', api_discovery.del_discovery),
|
||||||
api_discovery.del_discovery),
|
web.post('/discovery', api_discovery.set_discovery),
|
||||||
web.post('/services/discovery', api_discovery.set_discovery),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def _register_panel(self):
|
def _register_panel(self):
|
||||||
"""Register panel for homeassistant."""
|
"""Register panel for Home Assistant."""
|
||||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||||
|
|
||||||
def create_response(panel_file):
|
def create_response(panel_file):
|
||||||
@@ -214,8 +238,8 @@ class RestAPI(CoreSysAttributes):
|
|||||||
return lambda request: web.FileResponse(path)
|
return lambda request: web.FileResponse(path)
|
||||||
|
|
||||||
# This route is for backwards compatibility with HA < 0.58
|
# This route is for backwards compatibility with HA < 0.58
|
||||||
self.webapp.add_routes([
|
self.webapp.add_routes(
|
||||||
web.get('/panel', create_response('hassio-main-es5'))])
|
[web.get('/panel', create_response('hassio-main-es5'))])
|
||||||
|
|
||||||
# This route is for backwards compatibility with HA 0.58 - 0.61
|
# This route is for backwards compatibility with HA 0.58 - 0.61
|
||||||
self.webapp.add_routes([
|
self.webapp.add_routes([
|
||||||
@@ -233,7 +257,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes([web.static('/app', panel_dir)])
|
self.webapp.add_routes([web.static('/app', panel_dir)])
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Run rest api webserver."""
|
"""Run RESTful API webserver."""
|
||||||
await self._runner.setup()
|
await self._runner.setup()
|
||||||
self._site = web.TCPSite(
|
self._site = web.TCPSite(
|
||||||
self._runner, host="0.0.0.0", port=80, shutdown_timeout=5)
|
self._runner, host="0.0.0.0", port=80, shutdown_timeout=5)
|
||||||
@@ -241,13 +265,13 @@ class RestAPI(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
await self._site.start()
|
await self._site.start()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.fatal(
|
_LOGGER.fatal("Failed to create HTTP server at 0.0.0.0:80 -> %s",
|
||||||
"Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
|
err)
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
|
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""Stop rest api webserver."""
|
"""Stop RESTful API webserver."""
|
||||||
if not self._site:
|
if not self._site:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO homeassistant rest api."""
|
"""Init file for Hass.io Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -19,11 +19,13 @@ from ..const import (
|
|||||||
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
|
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
|
||||||
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
|
ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
|
||||||
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
|
ATTR_DISCOVERY, ATTR_APPARMOR, ATTR_DEVICETREE, ATTR_DOCKER_API,
|
||||||
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING,
|
ATTR_FULL_ACCESS, ATTR_PROTECTED, ATTR_RATING, ATTR_HOST_PID,
|
||||||
|
ATTR_HASSIO_ROLE, ATTR_MACHINE, ATTR_AVAILABLE, ATTR_AUTH_API,
|
||||||
|
ATTR_KERNEL_MODULES,
|
||||||
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
|
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT, REQUEST_FROM)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
||||||
from ..exceptions import APINotSupportedError
|
from ..exceptions import APIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -38,35 +40,37 @@ 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,
|
||||||
|
})
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_SECURITY = vol.Schema({
|
||||||
vol.Optional(ATTR_PROTECTED): vol.Boolean(),
|
vol.Optional(ATTR_PROTECTED): vol.Boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class APIAddons(CoreSysAttributes):
|
class APIAddons(CoreSysAttributes):
|
||||||
"""Handle rest api for addons functions."""
|
"""Handle RESTful API for add-on functions."""
|
||||||
|
|
||||||
def _extract_addon(self, request, check_installed=True):
|
def _extract_addon(self, request, check_installed=True):
|
||||||
"""Return addon, throw an exception it it doesn't exist."""
|
"""Return addon, throw an exception it it doesn't exist."""
|
||||||
addon = self.sys_addons.get(request.match_info.get('addon'))
|
addon_slug = request.match_info.get('addon')
|
||||||
|
|
||||||
|
# Lookup itself
|
||||||
|
if addon_slug == 'self':
|
||||||
|
return request.get(REQUEST_FROM)
|
||||||
|
|
||||||
|
addon = self.sys_addons.get(addon_slug)
|
||||||
if not addon:
|
if not addon:
|
||||||
raise RuntimeError("Addon does not exist")
|
raise APIError("Addon does not exist")
|
||||||
|
|
||||||
if check_installed and not addon.is_installed:
|
if check_installed and not addon.is_installed:
|
||||||
raise RuntimeError("Addon is not installed")
|
raise APIError("Addon is not installed")
|
||||||
|
|
||||||
return addon
|
return addon
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _pretty_devices(addon):
|
|
||||||
"""Return a simplified device list."""
|
|
||||||
dev_list = addon.devices
|
|
||||||
if not dev_list:
|
|
||||||
return None
|
|
||||||
return [row.split(':')[0] for row in dev_list]
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Return all addons / repositories ."""
|
"""Return all add-ons or repositories."""
|
||||||
data_addons = []
|
data_addons = []
|
||||||
for addon in self.sys_addons.list_addons:
|
for addon in self.sys_addons.list_addons:
|
||||||
data_addons.append({
|
data_addons.append({
|
||||||
@@ -75,7 +79,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_VERSION: addon.last_version,
|
ATTR_VERSION: addon.last_version,
|
||||||
ATTR_INSTALLED: addon.version_installed,
|
ATTR_INSTALLED: addon.version_installed,
|
||||||
ATTR_ARCH: addon.supported_arch,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
@@ -101,13 +105,13 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload(self, request):
|
async def reload(self, request):
|
||||||
"""Reload all addons data."""
|
"""Reload all add-on data."""
|
||||||
await asyncio.shield(self.sys_addons.reload())
|
await asyncio.shield(self.sys_addons.reload())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return addon information."""
|
"""Return add-on information."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -124,44 +128,46 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_RATING: rating_security(addon),
|
ATTR_RATING: rating_security(addon),
|
||||||
ATTR_BOOT: addon.boot,
|
ATTR_BOOT: addon.boot,
|
||||||
ATTR_OPTIONS: addon.options,
|
ATTR_OPTIONS: addon.options,
|
||||||
|
ATTR_ARCH: addon.supported_arch,
|
||||||
|
ATTR_MACHINE: addon.supported_machine,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
ATTR_NETWORK: addon.ports,
|
ATTR_NETWORK: addon.ports,
|
||||||
ATTR_HOST_NETWORK: addon.host_network,
|
ATTR_HOST_NETWORK: addon.host_network,
|
||||||
|
ATTR_HOST_PID: addon.host_pid,
|
||||||
ATTR_HOST_IPC: addon.host_ipc,
|
ATTR_HOST_IPC: addon.host_ipc,
|
||||||
ATTR_HOST_DBUS: addon.host_dbus,
|
ATTR_HOST_DBUS: addon.host_dbus,
|
||||||
ATTR_PRIVILEGED: addon.privileged,
|
ATTR_PRIVILEGED: addon.privileged,
|
||||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||||
ATTR_APPARMOR: addon.apparmor,
|
ATTR_APPARMOR: addon.apparmor,
|
||||||
ATTR_DEVICES: self._pretty_devices(addon),
|
ATTR_DEVICES: _pretty_devices(addon),
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
ATTR_CHANGELOG: addon.with_changelog,
|
ATTR_CHANGELOG: addon.with_changelog,
|
||||||
ATTR_WEBUI: addon.webui,
|
ATTR_WEBUI: addon.webui,
|
||||||
ATTR_STDIN: addon.with_stdin,
|
ATTR_STDIN: addon.with_stdin,
|
||||||
ATTR_HASSIO_API: addon.access_hassio_api,
|
ATTR_HASSIO_API: addon.access_hassio_api,
|
||||||
|
ATTR_HASSIO_ROLE: addon.hassio_role,
|
||||||
|
ATTR_AUTH_API: addon.access_auth_api,
|
||||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||||
ATTR_GPIO: addon.with_gpio,
|
ATTR_GPIO: addon.with_gpio,
|
||||||
|
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||||
ATTR_DEVICETREE: addon.with_devicetree,
|
ATTR_DEVICETREE: addon.with_devicetree,
|
||||||
ATTR_DOCKER_API: addon.access_docker_api,
|
ATTR_DOCKER_API: addon.access_docker_api,
|
||||||
ATTR_AUDIO: addon.with_audio,
|
ATTR_AUDIO: addon.with_audio,
|
||||||
ATTR_AUDIO_INPUT: addon.audio_input,
|
ATTR_AUDIO_INPUT: addon.audio_input,
|
||||||
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
ATTR_AUDIO_OUTPUT: addon.audio_output,
|
||||||
ATTR_SERVICES: addon.services,
|
ATTR_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options(self, request):
|
async def options(self, request):
|
||||||
"""Store user options for addon."""
|
"""Store user options for add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
# Have Access
|
|
||||||
if addon.slug == request[REQUEST_FROM]:
|
|
||||||
_LOGGER.error("Add-on can't self modify his options!")
|
|
||||||
raise APINotSupportedError()
|
|
||||||
|
|
||||||
addon_schema = SCHEMA_OPTIONS.extend({
|
addon_schema = SCHEMA_OPTIONS.extend({
|
||||||
vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema),
|
vol.Optional(ATTR_OPTIONS): vol.Any(None, addon.schema),
|
||||||
})
|
})
|
||||||
@@ -180,6 +186,16 @@ 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]
|
||||||
|
|
||||||
|
addon.save_data()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def security(self, request):
|
||||||
|
"""Store security options for add-on."""
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
body = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
_LOGGER.warning("Protected flag changing for %s!", addon.slug)
|
_LOGGER.warning("Protected flag changing for %s!", addon.slug)
|
||||||
addon.protected = body[ATTR_PROTECTED]
|
addon.protected = body[ATTR_PROTECTED]
|
||||||
@@ -194,7 +210,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
stats = await addon.stats()
|
stats = await addon.stats()
|
||||||
|
|
||||||
if not stats:
|
if not stats:
|
||||||
raise RuntimeError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -208,19 +224,19 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def install(self, request):
|
def install(self, request):
|
||||||
"""Install addon."""
|
"""Install add-on."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
return asyncio.shield(addon.install())
|
return asyncio.shield(addon.install())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def uninstall(self, request):
|
def uninstall(self, request):
|
||||||
"""Uninstall addon."""
|
"""Uninstall add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
return asyncio.shield(addon.uninstall())
|
return asyncio.shield(addon.uninstall())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request):
|
def start(self, request):
|
||||||
"""Start addon."""
|
"""Start add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
# check options
|
# check options
|
||||||
@@ -228,83 +244,99 @@ class APIAddons(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
addon.schema(options)
|
addon.schema(options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
raise RuntimeError(humanize_error(options, ex)) from None
|
raise APIError(humanize_error(options, ex)) from None
|
||||||
|
|
||||||
return asyncio.shield(addon.start())
|
return asyncio.shield(addon.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request):
|
def stop(self, request):
|
||||||
"""Stop addon."""
|
"""Stop add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
return asyncio.shield(addon.stop())
|
return asyncio.shield(addon.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def update(self, request):
|
def update(self, request):
|
||||||
"""Update addon."""
|
"""Update add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
if addon.last_version == addon.version_installed:
|
if addon.last_version == addon.version_installed:
|
||||||
raise RuntimeError("No update available!")
|
raise APIError("No update available!")
|
||||||
|
|
||||||
return asyncio.shield(addon.update())
|
return asyncio.shield(addon.update())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request):
|
def restart(self, request):
|
||||||
"""Restart addon."""
|
"""Restart add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
return asyncio.shield(addon.restart())
|
return asyncio.shield(addon.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request):
|
def rebuild(self, request):
|
||||||
"""Rebuild local build addon."""
|
"""Rebuild local build add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
if not addon.need_build:
|
if not addon.need_build:
|
||||||
raise RuntimeError("Only local build addons are supported")
|
raise APIError("Only local build addons are supported")
|
||||||
|
|
||||||
return asyncio.shield(addon.rebuild())
|
return asyncio.shield(addon.rebuild())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request):
|
def logs(self, request):
|
||||||
"""Return logs from addon."""
|
"""Return logs from add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
return addon.logs()
|
return addon.logs()
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def icon(self, request):
|
async def icon(self, request):
|
||||||
"""Return icon from addon."""
|
"""Return icon from add-on."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
if not addon.with_icon:
|
if not addon.with_icon:
|
||||||
raise RuntimeError("No icon found!")
|
raise APIError("No icon found!")
|
||||||
|
|
||||||
with addon.path_icon.open('rb') as png:
|
with addon.path_icon.open('rb') as png:
|
||||||
return png.read()
|
return png.read()
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def logo(self, request):
|
async def logo(self, request):
|
||||||
"""Return logo from addon."""
|
"""Return logo from add-on."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
if not addon.with_logo:
|
if not addon.with_logo:
|
||||||
raise RuntimeError("No logo found!")
|
raise APIError("No logo found!")
|
||||||
|
|
||||||
with addon.path_logo.open('rb') as png:
|
with addon.path_logo.open('rb') as png:
|
||||||
return png.read()
|
return png.read()
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def changelog(self, request):
|
async def changelog(self, request):
|
||||||
"""Return changelog from addon."""
|
"""Return changelog from add-on."""
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
if not addon.with_changelog:
|
if not addon.with_changelog:
|
||||||
raise RuntimeError("No changelog found!")
|
raise APIError("No changelog found!")
|
||||||
|
|
||||||
with addon.path_changelog.open('r') as changelog:
|
with addon.path_changelog.open('r') as changelog:
|
||||||
return changelog.read()
|
return changelog.read()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stdin(self, request):
|
async def stdin(self, request):
|
||||||
"""Write to stdin of addon."""
|
"""Write to stdin of add-on."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_stdin:
|
if not addon.with_stdin:
|
||||||
raise RuntimeError("STDIN not supported by addon")
|
raise APIError("STDIN not supported by add-on")
|
||||||
|
|
||||||
data = await request.read()
|
data = await request.read()
|
||||||
return await asyncio.shield(addon.write_stdin(data))
|
return await asyncio.shield(addon.write_stdin(data))
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_devices(addon):
|
||||||
|
"""Return a simplified device list."""
|
||||||
|
dev_list = addon.devices
|
||||||
|
if not dev_list:
|
||||||
|
return None
|
||||||
|
return [row.split(':')[0] for row in dev_list]
|
||||||
|
|
||||||
|
|
||||||
|
def _pretty_services(addon):
|
||||||
|
"""Return a simplified services role list."""
|
||||||
|
services = []
|
||||||
|
for name, access in addon.services_role.items():
|
||||||
|
services.append(f"{name}:{access}")
|
||||||
|
return services
|
||||||
|
61
hassio/api/auth.py
Normal file
61
hassio/api/auth.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"""Init file for Hass.io auth/SSO RESTful API."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import BasicAuth
|
||||||
|
from aiohttp.web_exceptions import HTTPUnauthorized
|
||||||
|
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION, WWW_AUTHENTICATE
|
||||||
|
|
||||||
|
from .utils import api_process
|
||||||
|
from ..const import REQUEST_FROM, CONTENT_TYPE_JSON, CONTENT_TYPE_URL
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIForbidden
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIAuth(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for auth functions."""
|
||||||
|
|
||||||
|
def _process_basic(self, request, addon):
|
||||||
|
"""Process login request with basic auth.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
auth = BasicAuth.decode(request.headers[AUTHORIZATION])
|
||||||
|
return self.sys_auth.check_login(addon, auth.login, auth.password)
|
||||||
|
|
||||||
|
def _process_dict(self, request, addon, data):
|
||||||
|
"""Process login with dict data.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
username = data.get('username') or data.get('user')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
return self.sys_auth.check_login(addon, username, password)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def auth(self, request):
|
||||||
|
"""Process login request."""
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
if not addon.access_auth_api:
|
||||||
|
raise APIForbidden("Can't use Home Assistant auth!")
|
||||||
|
|
||||||
|
# BasicAuth
|
||||||
|
if AUTHORIZATION in request.headers:
|
||||||
|
return await self._process_basic(request, addon)
|
||||||
|
|
||||||
|
# Json
|
||||||
|
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
|
||||||
|
data = await request.json()
|
||||||
|
return await self._process_dict(request, addon, data)
|
||||||
|
|
||||||
|
# URL encoded
|
||||||
|
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL:
|
||||||
|
data = await request.post()
|
||||||
|
return await self._process_dict(request, addon, data)
|
||||||
|
|
||||||
|
raise HTTPUnauthorized(headers={
|
||||||
|
WWW_AUTHENTICATE: "Basic realm=\"Hass.io Authentication\""
|
||||||
|
})
|
@@ -1,41 +1,47 @@
|
|||||||
"""Init file for HassIO network rest api."""
|
"""Init file for Hass.io network RESTful API."""
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_PROVIDER, ATTR_UUID, ATTR_COMPONENT, ATTR_PLATFORM, ATTR_CONFIG,
|
ATTR_ADDON, ATTR_UUID, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_SERVICE,
|
||||||
ATTR_DISCOVERY, REQUEST_FROM)
|
REQUEST_FROM)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
from ..validate import SERVICE_ALL
|
||||||
|
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema({
|
SCHEMA_DISCOVERY = vol.Schema({
|
||||||
vol.Required(ATTR_COMPONENT): vol.Coerce(str),
|
vol.Required(ATTR_SERVICE): SERVICE_ALL,
|
||||||
vol.Optional(ATTR_PLATFORM): vol.Any(None, vol.Coerce(str)),
|
vol.Optional(ATTR_CONFIG): vol.Maybe(dict),
|
||||||
vol.Optional(ATTR_CONFIG): vol.Any(None, dict),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class APIDiscovery(CoreSysAttributes):
|
class APIDiscovery(CoreSysAttributes):
|
||||||
"""Handle rest api for discovery functions."""
|
"""Handle RESTful API for discovery functions."""
|
||||||
|
|
||||||
def _extract_message(self, request):
|
def _extract_message(self, request):
|
||||||
"""Extract discovery message from URL."""
|
"""Extract discovery message from URL."""
|
||||||
message = self.sys_discovery.get(request.match_info.get('uuid'))
|
message = self.sys_discovery.get(request.match_info.get('uuid'))
|
||||||
if not message:
|
if not message:
|
||||||
raise RuntimeError("Discovery message not found")
|
raise APIError("Discovery message not found")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
def _check_permission_ha(self, request):
|
||||||
|
"""Check permission for API call / Home Assistant."""
|
||||||
|
if request[REQUEST_FROM] != self.sys_homeassistant:
|
||||||
|
raise APIForbidden("Only HomeAssistant can use this API!")
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request):
|
async def list(self, request):
|
||||||
"""Show register services."""
|
"""Show register services."""
|
||||||
|
self._check_permission_ha(request)
|
||||||
|
|
||||||
discovery = []
|
discovery = []
|
||||||
for message in self.sys_discovery.list_messages:
|
for message in self.sys_discovery.list_messages:
|
||||||
discovery.append({
|
discovery.append({
|
||||||
ATTR_PROVIDER: message.provider,
|
ATTR_ADDON: message.addon,
|
||||||
|
ATTR_SERVICE: message.service,
|
||||||
ATTR_UUID: message.uuid,
|
ATTR_UUID: message.uuid,
|
||||||
ATTR_COMPONENT: message.component,
|
|
||||||
ATTR_PLATFORM: message.platform,
|
|
||||||
ATTR_CONFIG: message.config,
|
ATTR_CONFIG: message.config,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,8 +51,14 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
async def set_discovery(self, request):
|
async def set_discovery(self, request):
|
||||||
"""Write data into a discovery pipeline."""
|
"""Write data into a discovery pipeline."""
|
||||||
body = await api_validate(SCHEMA_DISCOVERY, request)
|
body = await api_validate(SCHEMA_DISCOVERY, request)
|
||||||
message = self.sys_discovery.send(
|
addon = request[REQUEST_FROM]
|
||||||
provider=request[REQUEST_FROM], **body)
|
|
||||||
|
# Access?
|
||||||
|
if body[ATTR_SERVICE] not in addon.discovery:
|
||||||
|
raise APIForbidden(f"Can't use discovery!")
|
||||||
|
|
||||||
|
# Process discovery message
|
||||||
|
message = self.sys_discovery.send(addon, **body)
|
||||||
|
|
||||||
return {ATTR_UUID: message.uuid}
|
return {ATTR_UUID: message.uuid}
|
||||||
|
|
||||||
@@ -55,11 +67,13 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
"""Read data into a discovery message."""
|
"""Read data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
|
||||||
|
# HomeAssistant?
|
||||||
|
self._check_permission_ha(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_PROVIDER: message.provider,
|
ATTR_ADDON: message.addon,
|
||||||
|
ATTR_SERVICE: message.service,
|
||||||
ATTR_UUID: message.uuid,
|
ATTR_UUID: message.uuid,
|
||||||
ATTR_COMPONENT: message.component,
|
|
||||||
ATTR_PLATFORM: message.platform,
|
|
||||||
ATTR_CONFIG: message.config,
|
ATTR_CONFIG: message.config,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +81,11 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
async def del_discovery(self, request):
|
async def del_discovery(self, request):
|
||||||
"""Delete data into a discovery message."""
|
"""Delete data into a discovery message."""
|
||||||
message = self._extract_message(request)
|
message = self._extract_message(request)
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
# Permission
|
||||||
|
if message.addon != addon.slug:
|
||||||
|
raise APIForbidden(f"Can't remove discovery message")
|
||||||
|
|
||||||
self.sys_discovery.remove(message)
|
self.sys_discovery.remove(message)
|
||||||
return True
|
return True
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO hardware rest api."""
|
"""Init file for Hass.io hardware RESTful API."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .utils import api_process
|
from .utils import api_process
|
||||||
@@ -10,7 +10,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APIHardware(CoreSysAttributes):
|
class APIHardware(CoreSysAttributes):
|
||||||
"""Handle rest api for hardware functions."""
|
"""Handle RESTful API for hardware functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for Hass.io hassos rest api."""
|
"""Init file for Hass.io HassOS RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
class APIHassOS(CoreSysAttributes):
|
class APIHassOS(CoreSysAttributes):
|
||||||
"""Handle rest api for hassos functions."""
|
"""Handle RESTful API for HassOS functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return hassos information."""
|
"""Return HassOS information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
ATTR_VERSION: self.sys_hassos.version,
|
||||||
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
|
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
|
||||||
|
@@ -1,36 +1,42 @@
|
|||||||
"""Init file for HassIO homeassistant rest api."""
|
"""Init file for Hass.io Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_IMAGE, ATTR_CUSTOM, ATTR_BOOT,
|
ATTR_ARCH, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_BOOT, ATTR_CPU_PERCENT,
|
||||||
ATTR_PORT, ATTR_PASSWORD, ATTR_SSL, ATTR_WATCHDOG, ATTR_CPU_PERCENT,
|
ATTR_CUSTOM, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_MACHINE, ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_USAGE, ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX,
|
ATTR_MEMORY_USAGE, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_PASSWORD,
|
||||||
ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_WAIT_BOOT, ATTR_MACHINE,
|
ATTR_PORT, ATTR_REFRESH_TOKEN, ATTR_SSL, ATTR_VERSION, ATTR_WAIT_BOOT,
|
||||||
ATTR_REFRESH_TOKEN, CONTENT_TYPE_BINARY)
|
ATTR_WATCHDOG, CONTENT_TYPE_BINARY)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import NETWORK_PORT, DOCKER_IMAGE
|
from ..exceptions import APIError
|
||||||
|
from ..validate import DOCKER_IMAGE, NETWORK_PORT
|
||||||
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_OPTIONS = vol.Schema({
|
SCHEMA_OPTIONS = vol.Schema({
|
||||||
vol.Optional(ATTR_BOOT): vol.Boolean(),
|
vol.Optional(ATTR_BOOT):
|
||||||
|
vol.Boolean(),
|
||||||
vol.Inclusive(ATTR_IMAGE, 'custom_hass'):
|
vol.Inclusive(ATTR_IMAGE, 'custom_hass'):
|
||||||
vol.Maybe(vol.Coerce(str)),
|
vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
|
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
|
||||||
vol.Any(None, DOCKER_IMAGE),
|
vol.Any(None, DOCKER_IMAGE),
|
||||||
vol.Optional(ATTR_PORT): NETWORK_PORT,
|
vol.Optional(ATTR_PORT):
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
|
NETWORK_PORT,
|
||||||
vol.Optional(ATTR_SSL): vol.Boolean(),
|
vol.Optional(ATTR_PASSWORD):
|
||||||
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
vol.Maybe(vol.Coerce(str)),
|
||||||
|
vol.Optional(ATTR_SSL):
|
||||||
|
vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_WATCHDOG):
|
||||||
|
vol.Boolean(),
|
||||||
vol.Optional(ATTR_WAIT_BOOT):
|
vol.Optional(ATTR_WAIT_BOOT):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=60)),
|
vol.All(vol.Coerce(int), vol.Range(min=60)),
|
||||||
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_REFRESH_TOKEN):
|
||||||
|
vol.Maybe(vol.Coerce(str)),
|
||||||
})
|
})
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({
|
SCHEMA_VERSION = vol.Schema({
|
||||||
@@ -39,7 +45,7 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
class APIHomeAssistant(CoreSysAttributes):
|
class APIHomeAssistant(CoreSysAttributes):
|
||||||
"""Handle rest api for homeassistant functions."""
|
"""Handle RESTful API for Home Assistant functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
@@ -48,6 +54,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
ATTR_VERSION: self.sys_homeassistant.version,
|
ATTR_VERSION: self.sys_homeassistant.version,
|
||||||
ATTR_LAST_VERSION: self.sys_homeassistant.last_version,
|
ATTR_LAST_VERSION: self.sys_homeassistant.last_version,
|
||||||
ATTR_MACHINE: self.sys_homeassistant.machine,
|
ATTR_MACHINE: self.sys_homeassistant.machine,
|
||||||
|
ATTR_ARCH: self.sys_homeassistant.arch,
|
||||||
ATTR_IMAGE: self.sys_homeassistant.image,
|
ATTR_IMAGE: self.sys_homeassistant.image,
|
||||||
ATTR_CUSTOM: self.sys_homeassistant.is_custom_image,
|
ATTR_CUSTOM: self.sys_homeassistant.is_custom_image,
|
||||||
ATTR_BOOT: self.sys_homeassistant.boot,
|
ATTR_BOOT: self.sys_homeassistant.boot,
|
||||||
@@ -59,7 +66,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options(self, request):
|
async def options(self, request):
|
||||||
"""Set homeassistant options."""
|
"""Set Home Assistant options."""
|
||||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
if ATTR_IMAGE in body and ATTR_LAST_VERSION in body:
|
if ATTR_IMAGE in body and ATTR_LAST_VERSION in body:
|
||||||
@@ -94,7 +101,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_homeassistant.stats()
|
stats = await self.sys_homeassistant.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise RuntimeError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -108,7 +115,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update(self, request):
|
async def update(self, request):
|
||||||
"""Update homeassistant."""
|
"""Update Home Assistant."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
|
version = body.get(ATTR_VERSION, self.sys_homeassistant.last_version)
|
||||||
|
|
||||||
@@ -116,29 +123,27 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request):
|
def stop(self, request):
|
||||||
"""Stop homeassistant."""
|
"""Stop Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.stop())
|
return asyncio.shield(self.sys_homeassistant.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request):
|
def start(self, request):
|
||||||
"""Start homeassistant."""
|
"""Start Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.start())
|
return asyncio.shield(self.sys_homeassistant.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request):
|
def restart(self, request):
|
||||||
"""Restart homeassistant."""
|
"""Restart Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.restart())
|
return asyncio.shield(self.sys_homeassistant.restart())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request):
|
def logs(self, request):
|
||||||
"""Return homeassistant docker logs."""
|
"""Return Home Assistant Docker logs."""
|
||||||
return self.sys_homeassistant.logs()
|
return self.sys_homeassistant.logs()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def check(self, request):
|
async def check(self, request):
|
||||||
"""Check config of homeassistant."""
|
"""Check configuration of Home Assistant."""
|
||||||
result = await self.sys_homeassistant.check_config()
|
result = await self.sys_homeassistant.check_config()
|
||||||
if not result.valid:
|
if not result.valid:
|
||||||
raise RuntimeError(result.log)
|
raise APIError(result.log)
|
||||||
|
|
||||||
return True
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO host rest api."""
|
"""Init file for Hass.io host RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ SCHEMA_OPTIONS = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
class APIHost(CoreSysAttributes):
|
class APIHost(CoreSysAttributes):
|
||||||
"""Handle rest api for host functions."""
|
"""Handle RESTful API for host functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
|
28
hassio/api/info.py
Normal file
28
hassio/api/info.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Init file for Hass.io info RESTful API."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..const import (ATTR_ARCH, ATTR_CHANNEL, ATTR_HASSOS, ATTR_HOMEASSISTANT,
|
||||||
|
ATTR_HOSTNAME, ATTR_MACHINE, ATTR_SUPERVISOR,
|
||||||
|
ATTR_SUPPORTED_ARCH)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .utils import api_process
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APIInfo(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for info functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request):
|
||||||
|
"""Show system info."""
|
||||||
|
return {
|
||||||
|
ATTR_SUPERVISOR: self.sys_supervisor.version,
|
||||||
|
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
|
||||||
|
ATTR_HASSOS: self.sys_hassos.version,
|
||||||
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
|
ATTR_MACHINE: self.sys_machine,
|
||||||
|
ATTR_ARCH: self.sys_arch.default,
|
||||||
|
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||||
|
ATTR_CHANNEL: self.sys_updater.channel,
|
||||||
|
}
|
2
hassio/api/panel/chunk.0853908528652fbc5d4f.js
Normal file
2
hassio/api/panel/chunk.0853908528652fbc5d4f.js
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/api/panel/chunk.0853908528652fbc5d4f.js.gz
Normal file
BIN
hassio/api/panel/chunk.0853908528652fbc5d4f.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.0853908528652fbc5d4f.js.map
Normal file
1
hassio/api/panel/chunk.0853908528652fbc5d4f.js.map
Normal file
File diff suppressed because one or more lines are too long
2
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js
Normal file
2
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js.gz
Normal file
BIN
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js.map
Normal file
1
hassio/api/panel/chunk.0cb8b788b03dcc48da14.js.map
Normal file
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.
2
hassio/api/panel/chunk.8c049a124b9397e54c16.js
Normal file
2
hassio/api/panel/chunk.8c049a124b9397e54c16.js
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/api/panel/chunk.8c049a124b9397e54c16.js.gz
Normal file
BIN
hassio/api/panel/chunk.8c049a124b9397e54c16.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.8c049a124b9397e54c16.js.map
Normal file
1
hassio/api/panel/chunk.8c049a124b9397e54c16.js.map
Normal file
File diff suppressed because one or more lines are too long
2
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js
Normal file
2
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(window.webpackJsonp=window.webpackJsonp||[]).push([[4],{100:function(n,r,t){"use strict";t.r(r),t.d(r,"marked",function(){return a}),t.d(r,"filterXSS",function(){return c});var e=t(89),i=t.n(e),o=t(91),u=t.n(o),a=i.a,c=u.a}}]);
|
||||||
|
//# sourceMappingURL=chunk.9e3883f96f68b3ce89f5.js.map
|
BIN
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js.gz
Normal file
BIN
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js.map
Normal file
1
hassio/api/panel/chunk.9e3883f96f68b3ce89f5.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["webpack:///../src/resources/load_markdown.js"],"names":["__webpack_require__","r","__webpack_exports__","d","marked","filterXSS","marked__WEBPACK_IMPORTED_MODULE_0__","marked__WEBPACK_IMPORTED_MODULE_0___default","n","xss__WEBPACK_IMPORTED_MODULE_1__","xss__WEBPACK_IMPORTED_MODULE_1___default","marked_","filterXSS_"],"mappings":"0FAAAA,EAAAC,EAAAC,GAAAF,EAAAG,EAAAD,EAAA,2BAAAE,IAAAJ,EAAAG,EAAAD,EAAA,8BAAAG,IAAA,IAAAC,EAAAN,EAAA,IAAAO,EAAAP,EAAAQ,EAAAF,GAAAG,EAAAT,EAAA,IAAAU,EAAAV,EAAAQ,EAAAC,GAGaL,EAASO,IACTN,EAAYO","file":"chunk.9e3883f96f68b3ce89f5.js","sourcesContent":["import marked_ from \"marked\";\nimport filterXSS_ from \"xss\";\n\nexport const marked = marked_;\nexport const filterXSS = filterXSS_;\n"],"sourceRoot":""}
|
File diff suppressed because one or more lines are too long
@@ -1,419 +0,0 @@
|
|||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
||||||
Code distributed by Google as part of the polymer project is also
|
|
||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
Binary file not shown.
3
hassio/api/panel/chunk.c1ac97370d72bce0a835.js
Normal file
3
hassio/api/panel/chunk.c1ac97370d72bce0a835.js
Normal file
File diff suppressed because one or more lines are too long
820
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.LICENSE
Normal file
820
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.LICENSE
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
* This code may only be used under the BSD style license found at
|
||||||
|
* http://polymer.github.io/LICENSE.txt
|
||||||
|
* The complete set of authors may be found at
|
||||||
|
* http://polymer.github.io/AUTHORS.txt
|
||||||
|
* The complete set of contributors may be found at
|
||||||
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
* Code distributed by Google as part of the polymer project is also
|
||||||
|
* subject to an additional IP rights grant found at
|
||||||
|
* http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2016 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
BIN
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.gz
Normal file
BIN
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.map
Normal file
1
hassio/api/panel/chunk.c1ac97370d72bce0a835.js.map
Normal file
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.
@@ -1 +0,0 @@
|
|||||||
(window.webpackJsonp=window.webpackJsonp||[]).push([[5],{104:function(n,r,t){"use strict";t.r(r),t.d(r,"marked",function(){return a}),t.d(r,"filterXSS",function(){return c});var e=t(99),i=t.n(e),o=t(97),u=t.n(o),a=i.a,c=u.a}}]);
|
|
Binary file not shown.
2
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js
Normal file
2
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js
Normal file
File diff suppressed because one or more lines are too long
BIN
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js.gz
Normal file
BIN
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js.map
Normal file
1
hassio/api/panel/chunk.d0eb7b86b775838caf5e.js.map
Normal file
File diff suppressed because one or more lines are too long
3
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js
Normal file
3
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js
Normal file
File diff suppressed because one or more lines are too long
@@ -218,9 +218,91 @@ Code distributed by Google as part of the polymer project is also
|
|||||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@license
|
@license
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
||||||
|
Code distributed by Google as part of the polymer project is also
|
||||||
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
||||||
|
This code may only be used under the BSD style license found at
|
||||||
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
||||||
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
||||||
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
||||||
|
part of the polymer project is also subject to an additional IP rights grant
|
||||||
|
found at http://polymer.github.io/PATENTS.txt
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
@license
|
||||||
|
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
||||||
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
||||||
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
||||||
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
BIN
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js.gz
Normal file
BIN
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js.gz
Normal file
Binary file not shown.
1
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js.map
Normal file
1
hassio/api/panel/chunk.f32f3c841cc3e1d081f7.js.map
Normal file
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +1,2 @@
|
|||||||
!function(e){function n(n){for(var t,o,i=n[0],u=n[1],a=0,l=[];a<i.length;a++)o=i[a],r[o]&&l.push(r[o][0]),r[o]=0;for(t in u)Object.prototype.hasOwnProperty.call(u,t)&&(e[t]=u[t]);for(f&&f(n);l.length;)l.shift()()}var t={},r={6: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 i=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=i);var u,a=document.getElementsByTagName("head")[0],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:"f3880aa331d3ef2ddf32",1:"a8e86d80be46b3b6e16d",2:"0ef4ef1053fe3d5107b5",3:"ff92199b0d422767d108",4:"c77b56beea1d4547ff5f",5:"c93f37c558ff32991708"}[e]+".js"}(e),u=function(n){f.onerror=f.onload=null,clearTimeout(l);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src,u=new Error("Loading chunk "+e+" failed.\n("+o+": "+i+")");u.type=o,u.request=i,t[1](u)}r[e]=void 0}};var l=setTimeout(function(){u({type:"timeout",target:f})},12e4);f.onerror=f.onload=u,a.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 i=window.webpackJsonp=window.webpackJsonp||[],u=i.push.bind(i);i.push=n,i=i.slice();for(var a=0;a<i.length;a++)n(i[a]);var f=u;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(3)]).then(t.bind(null,1)),Promise.all([t.e(0),t.e(1),t.e(2)]).then(t.bind(null,2))})}]);
|
!function(e){function n(n){for(var t,o,i=n[0],u=n[1],c=0,f=[];c<i.length;c++)o=i[c],r[o]&&f.push(r[o][0]),r[o]=0;for(t in u)Object.prototype.hasOwnProperty.call(u,t)&&(e[t]=u[t]);for(a&&a(n);f.length;)f.shift()()}var t={},r={1: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 i=new Promise(function(n,o){t=r[e]=[n,o]});n.push(t[2]=i);var u,c=document.getElementsByTagName("head")[0],a=document.createElement("script");a.charset="utf-8",a.timeout=120,o.nc&&a.setAttribute("nonce",o.nc),a.src=function(e){return o.p+"chunk."+{0:"f32f3c841cc3e1d081f7",2:"8c049a124b9397e54c16",3:"d0eb7b86b775838caf5e",4:"9e3883f96f68b3ce89f5",5:"0cb8b788b03dcc48da14",6:"c1ac97370d72bce0a835",7:"0853908528652fbc5d4f"}[e]+".js"}(e),u=function(n){a.onerror=a.onload=null,clearTimeout(f);var t=r[e];if(0!==t){if(t){var o=n&&("load"===n.type?"missing":n.type),i=n&&n.target&&n.target.src,u=new Error("Loading chunk "+e+" failed.\n("+o+": "+i+")");u.type=o,u.request=i,t[1](u)}r[e]=void 0}};var f=setTimeout(function(){u({type:"timeout",target:a})},12e4);a.onerror=a.onload=u,c.appendChild(a)}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 i=window.webpackJsonp=window.webpackJsonp||[],u=i.push.bind(i);i.push=n,i=i.slice();for(var c=0;c<i.length;c++)n(i[c]);var a=u;o(o.s=0)}([function(e,n,t){window.loadES5Adapter().then(function(){Promise.all([t.e(0),t.e(2)]).then(t.bind(null,2)),Promise.all([t.e(0),t.e(6),t.e(3)]).then(t.bind(null,1))})}]);
|
||||||
|
//# sourceMappingURL=entrypoint.js.map
|
Binary file not shown.
1
hassio/api/panel/entrypoint.js.map
Normal file
1
hassio/api/panel/entrypoint.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -5,26 +5,31 @@ import logging
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web_exceptions import (
|
from aiohttp.web_exceptions import HTTPBadGateway, HTTPUnauthorized
|
||||||
HTTPBadGateway, HTTPInternalServerError, HTTPUnauthorized)
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
from aiohttp.hdrs import CONTENT_TYPE
|
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from ..const import HEADER_HA_ACCESS
|
from ..const import HEADER_HA_ACCESS
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import HomeAssistantAuthError, HomeAssistantAPIError
|
from ..exceptions import (
|
||||||
|
HomeAssistantAuthError, HomeAssistantAPIError, APIError)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class APIProxy(CoreSysAttributes):
|
class APIProxy(CoreSysAttributes):
|
||||||
"""API Proxy for Home-Assistant."""
|
"""API Proxy for Home Assistant."""
|
||||||
|
|
||||||
def _check_access(self, request):
|
def _check_access(self, request):
|
||||||
"""Check the Hass.io token."""
|
"""Check the Hass.io token."""
|
||||||
|
if AUTHORIZATION in request.headers:
|
||||||
|
bearer = request.headers[AUTHORIZATION]
|
||||||
|
hassio_token = bearer.split(' ')[-1]
|
||||||
|
else:
|
||||||
hassio_token = request.headers.get(HEADER_HA_ACCESS)
|
hassio_token = request.headers.get(HEADER_HA_ACCESS)
|
||||||
addon = self.sys_addons.from_uuid(hassio_token)
|
|
||||||
|
|
||||||
|
addon = self.sys_addons.from_token(hassio_token)
|
||||||
if not addon:
|
if not addon:
|
||||||
_LOGGER.warning("Unknown Home Assistant API access!")
|
_LOGGER.warning("Unknown Home Assistant API access!")
|
||||||
elif not addon.access_homeassistant_api:
|
elif not addon.access_homeassistant_api:
|
||||||
@@ -37,7 +42,7 @@ class APIProxy(CoreSysAttributes):
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _api_client(self, request, path, timeout=300):
|
async def _api_client(self, request, path, timeout=300):
|
||||||
"""Return a client request with proxy origin for Home-Assistant."""
|
"""Return a client request with proxy origin for Home Assistant."""
|
||||||
try:
|
try:
|
||||||
# read data
|
# read data
|
||||||
with async_timeout.timeout(30):
|
with async_timeout.timeout(30):
|
||||||
@@ -72,25 +77,19 @@ class APIProxy(CoreSysAttributes):
|
|||||||
"""Proxy HomeAssistant EventStream Requests."""
|
"""Proxy HomeAssistant EventStream Requests."""
|
||||||
self._check_access(request)
|
self._check_access(request)
|
||||||
|
|
||||||
_LOGGER.info("Home-Assistant EventStream start")
|
_LOGGER.info("Home Assistant EventStream start")
|
||||||
async with self._api_client(request, 'stream', timeout=None) as client:
|
async with self._api_client(request, 'stream', timeout=None) as client:
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = request.headers.get(CONTENT_TYPE)
|
response.content_type = request.headers.get(CONTENT_TYPE)
|
||||||
try:
|
try:
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
while True:
|
async for data in client.content:
|
||||||
data = await client.content.read(10)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
await response.write(data)
|
await response.write(data)
|
||||||
|
|
||||||
except aiohttp.ClientError:
|
except (aiohttp.ClientError, aiohttp.ClientPayloadError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
finally:
|
_LOGGER.info("Home Assistant EventStream close")
|
||||||
client.close()
|
|
||||||
_LOGGER.info("Home-Assistant EventStream close")
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def api(self, request):
|
async def api(self, request):
|
||||||
@@ -108,14 +107,14 @@ class APIProxy(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def _websocket_client(self):
|
async def _websocket_client(self):
|
||||||
"""Initialize a websocket api connection."""
|
"""Initialize a WebSocket API connection."""
|
||||||
url = f"{self.sys_homeassistant.api_url}/api/websocket"
|
url = f"{self.sys_homeassistant.api_url}/api/websocket"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = await self.sys_websession_ssl.ws_connect(
|
client = await self.sys_websession_ssl.ws_connect(
|
||||||
url, heartbeat=60, verify_ssl=False)
|
url, heartbeat=30, verify_ssl=False)
|
||||||
|
|
||||||
# handle authentication
|
# Handle authentication
|
||||||
data = await client.receive_json()
|
data = await client.receive_json()
|
||||||
|
|
||||||
if data.get('type') == 'auth_ok':
|
if data.get('type') == 'auth_ok':
|
||||||
@@ -124,8 +123,8 @@ class APIProxy(CoreSysAttributes):
|
|||||||
if data.get('type') != 'auth_required':
|
if data.get('type') != 'auth_required':
|
||||||
# Invalid protocol
|
# Invalid protocol
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
'Got unexpected response from HA websocket: %s', data)
|
"Got unexpected response from HA WebSocket: %s", data)
|
||||||
raise HTTPBadGateway()
|
raise APIError()
|
||||||
|
|
||||||
if self.sys_homeassistant.refresh_token:
|
if self.sys_homeassistant.refresh_token:
|
||||||
await self.sys_homeassistant.ensure_access_token()
|
await self.sys_homeassistant.ensure_access_token()
|
||||||
@@ -145,26 +144,25 @@ class APIProxy(CoreSysAttributes):
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
# Renew the Token is invalid
|
# Renew the Token is invalid
|
||||||
if (data.get('type') == 'invalid_auth' and
|
if data.get('type') == 'invalid_auth' and self.sys_homeassistant.refresh_token:
|
||||||
self.sys_homeassistant.refresh_token):
|
|
||||||
self.sys_homeassistant.access_token = None
|
self.sys_homeassistant.access_token = None
|
||||||
return await self._websocket_client()
|
return await self._websocket_client()
|
||||||
|
|
||||||
raise HomeAssistantAuthError()
|
raise HomeAssistantAuthError()
|
||||||
|
|
||||||
except (RuntimeError, ValueError) as err:
|
except (RuntimeError, ValueError, ClientConnectorError) as err:
|
||||||
_LOGGER.error("Client error on websocket API %s.", err)
|
_LOGGER.error("Client error on WebSocket API %s.", err)
|
||||||
except HomeAssistantAuthError as err:
|
except HomeAssistantAuthError:
|
||||||
_LOGGER.error("Failed authentication to HomeAssistant websocket")
|
_LOGGER.error("Failed authentication to Home Assistant WebSocket")
|
||||||
|
|
||||||
raise HTTPBadGateway()
|
raise APIError()
|
||||||
|
|
||||||
async def websocket(self, request):
|
async def websocket(self, request):
|
||||||
"""Initialize a websocket api connection."""
|
"""Initialize a WebSocket API connection."""
|
||||||
_LOGGER.info("Home-Assistant Websocket API request initialze")
|
_LOGGER.info("Home Assistant WebSocket API request initialize")
|
||||||
|
|
||||||
# init server
|
# init server
|
||||||
server = web.WebSocketResponse(heartbeat=60)
|
server = web.WebSocketResponse(heartbeat=30)
|
||||||
await server.prepare(request)
|
await server.prepare(request)
|
||||||
|
|
||||||
# handle authentication
|
# handle authentication
|
||||||
@@ -176,19 +174,18 @@ class APIProxy(CoreSysAttributes):
|
|||||||
|
|
||||||
# Check API access
|
# Check API access
|
||||||
response = await server.receive_json()
|
response = await server.receive_json()
|
||||||
hassio_token = (response.get('api_password') or
|
hassio_token = response.get('api_password') or response.get('access_token')
|
||||||
response.get('access_token'))
|
addon = self.sys_addons.from_token(hassio_token)
|
||||||
addon = self.sys_addons.from_uuid(hassio_token)
|
|
||||||
|
|
||||||
if not addon or not addon.access_homeassistant_api:
|
if not addon or not addon.access_homeassistant_api:
|
||||||
_LOGGER.warning("Unauthorized websocket access!")
|
_LOGGER.warning("Unauthorized WebSocket access!")
|
||||||
await server.send_json({
|
await server.send_json({
|
||||||
'type': 'auth_invalid',
|
'type': 'auth_invalid',
|
||||||
'message': 'Invalid access',
|
'message': 'Invalid access',
|
||||||
})
|
})
|
||||||
return server
|
return server
|
||||||
|
|
||||||
_LOGGER.info("Websocket access from %s", addon.slug)
|
_LOGGER.info("WebSocket access from %s", addon.slug)
|
||||||
|
|
||||||
await server.send_json({
|
await server.send_json({
|
||||||
'type': 'auth_ok',
|
'type': 'auth_ok',
|
||||||
@@ -196,12 +193,15 @@ class APIProxy(CoreSysAttributes):
|
|||||||
})
|
})
|
||||||
except (RuntimeError, ValueError) as err:
|
except (RuntimeError, ValueError) as err:
|
||||||
_LOGGER.error("Can't initialize handshake: %s", err)
|
_LOGGER.error("Can't initialize handshake: %s", err)
|
||||||
raise HTTPInternalServerError() from None
|
return server
|
||||||
|
|
||||||
# init connection to hass
|
# init connection to hass
|
||||||
|
try:
|
||||||
client = await self._websocket_client()
|
client = await self._websocket_client()
|
||||||
|
except APIError:
|
||||||
|
return server
|
||||||
|
|
||||||
_LOGGER.info("Home-Assistant Websocket API request running")
|
_LOGGER.info("Home Assistant WebSocket API request running")
|
||||||
try:
|
try:
|
||||||
client_read = None
|
client_read = None
|
||||||
server_read = None
|
server_read = None
|
||||||
@@ -234,8 +234,8 @@ class APIProxy(CoreSysAttributes):
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except RuntimeError as err:
|
except (RuntimeError, ConnectionError, TypeError) as err:
|
||||||
_LOGGER.info("Home-Assistant Websocket API error: %s", err)
|
_LOGGER.info("Home Assistant WebSocket API error: %s", err)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if client_read:
|
if client_read:
|
||||||
@@ -244,8 +244,10 @@ class APIProxy(CoreSysAttributes):
|
|||||||
server_read.cancel()
|
server_read.cancel()
|
||||||
|
|
||||||
# close connections
|
# close connections
|
||||||
|
if not client.closed:
|
||||||
await client.close()
|
await client.close()
|
||||||
|
if not server.closed:
|
||||||
await server.close()
|
await server.close()
|
||||||
|
|
||||||
_LOGGER.info("Home-Assistant Websocket API connection is closed")
|
_LOGGER.info("Home Assistant WebSocket API connection is closed")
|
||||||
return server
|
return server
|
||||||
|
@@ -3,18 +3,76 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from aiohttp.web import middleware
|
from aiohttp.web import middleware
|
||||||
from aiohttp.web_exceptions import HTTPUnauthorized
|
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPForbidden
|
||||||
|
|
||||||
from ..const import HEADER_TOKEN, REQUEST_FROM
|
from ..const import (
|
||||||
|
HEADER_TOKEN, REQUEST_FROM, ROLE_ADMIN, ROLE_DEFAULT, ROLE_HOMEASSISTANT,
|
||||||
|
ROLE_MANAGER, ROLE_BACKUP)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
NO_SECURITY_CHECK = set((
|
|
||||||
re.compile(r"^/homeassistant/api/.*$"),
|
# Block Anytime
|
||||||
re.compile(r"^/homeassistant/websocket$"),
|
BLACKLIST = re.compile(
|
||||||
re.compile(r"^/supervisor/ping$"),
|
r"^(?:"
|
||||||
))
|
r"|/homeassistant/api/hassio/.*"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Free to call or have own security concepts
|
||||||
|
NO_SECURITY_CHECK = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/homeassistant/api/.*"
|
||||||
|
r"|/homeassistant/websocket"
|
||||||
|
r"|/supervisor/ping"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Can called by every add-on
|
||||||
|
ADDONS_API_BYPASS = re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/addons/self/(?!security|update)[^/]+"
|
||||||
|
r"|/info"
|
||||||
|
r"|/services.*"
|
||||||
|
r"|/discovery.*"
|
||||||
|
r"|/auth"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Policy role add-on API access
|
||||||
|
ADDONS_ROLE_ACCESS = {
|
||||||
|
ROLE_DEFAULT: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/[^/]+/info"
|
||||||
|
r"|/addons"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_HOMEASSISTANT: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/homeassistant/.+"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_BACKUP: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/snapshots.*"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_MANAGER: re.compile(
|
||||||
|
r"^(?:"
|
||||||
|
r"|/homeassistant/.+"
|
||||||
|
r"|/host/.+"
|
||||||
|
r"|/hardware/.+"
|
||||||
|
r"|/hassos/.+"
|
||||||
|
r"|/supervisor/.+"
|
||||||
|
r"|/addons(?:/[^/]+/(?!security).+)?"
|
||||||
|
r"|/snapshots.*"
|
||||||
|
r")$"
|
||||||
|
),
|
||||||
|
ROLE_ADMIN: re.compile(
|
||||||
|
r".*"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SecurityMiddleware(CoreSysAttributes):
|
class SecurityMiddleware(CoreSysAttributes):
|
||||||
@@ -27,33 +85,56 @@ class SecurityMiddleware(CoreSysAttributes):
|
|||||||
@middleware
|
@middleware
|
||||||
async def token_validation(self, request, handler):
|
async def token_validation(self, request, handler):
|
||||||
"""Check security access of this layer."""
|
"""Check security access of this layer."""
|
||||||
|
request_from = None
|
||||||
hassio_token = request.headers.get(HEADER_TOKEN)
|
hassio_token = request.headers.get(HEADER_TOKEN)
|
||||||
|
|
||||||
|
# Blacklist
|
||||||
|
if BLACKLIST.match(request.path):
|
||||||
|
_LOGGER.warning("%s is blacklisted!", request.path)
|
||||||
|
raise HTTPForbidden()
|
||||||
|
|
||||||
# Ignore security check
|
# Ignore security check
|
||||||
for rule in NO_SECURITY_CHECK:
|
if NO_SECURITY_CHECK.match(request.path):
|
||||||
if rule.match(request.path):
|
|
||||||
_LOGGER.debug("Passthrough %s", request.path)
|
_LOGGER.debug("Passthrough %s", request.path)
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
# Not token
|
||||||
|
if not hassio_token:
|
||||||
|
_LOGGER.warning("No API token provided for %s", request.path)
|
||||||
|
raise HTTPUnauthorized()
|
||||||
|
|
||||||
# Home-Assistant
|
# Home-Assistant
|
||||||
if hassio_token == self.sys_homeassistant.uuid:
|
# UUID check need removed with 131
|
||||||
_LOGGER.debug("%s access from Home-Assistant", request.path)
|
if hassio_token in (self.sys_homeassistant.uuid,
|
||||||
request[REQUEST_FROM] = 'homeassistant'
|
self.sys_homeassistant.hassio_token):
|
||||||
|
_LOGGER.debug("%s access from Home Assistant", request.path)
|
||||||
|
request_from = self.sys_homeassistant
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
if hassio_token == self.sys_machine_id:
|
if hassio_token == self.sys_machine_id:
|
||||||
_LOGGER.debug("%s access from Host", request.path)
|
_LOGGER.debug("%s access from Host", request.path)
|
||||||
request[REQUEST_FROM] = 'host'
|
request_from = self.sys_host
|
||||||
|
|
||||||
# Add-on
|
# Add-on
|
||||||
addon = self.sys_addons.from_uuid(hassio_token) \
|
addon = None
|
||||||
if hassio_token else None
|
if hassio_token and not request_from:
|
||||||
if addon:
|
addon = self.sys_addons.from_token(hassio_token)
|
||||||
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
|
||||||
request[REQUEST_FROM] = addon.slug
|
|
||||||
|
|
||||||
if request.get(REQUEST_FROM):
|
# Check Add-on API access
|
||||||
|
if addon and ADDONS_API_BYPASS.match(request.path):
|
||||||
|
_LOGGER.debug("Passthrough %s from %s", request.path, addon.slug)
|
||||||
|
request_from = addon
|
||||||
|
elif addon and addon.access_hassio_api:
|
||||||
|
# Check Role
|
||||||
|
if ADDONS_ROLE_ACCESS[addon.hassio_role].match(request.path):
|
||||||
|
_LOGGER.info("%s access from %s", request.path, addon.slug)
|
||||||
|
request_from = addon
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("%s no role for %s", request.path, addon.slug)
|
||||||
|
|
||||||
|
if request_from:
|
||||||
|
request[REQUEST_FROM] = request_from
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
_LOGGER.warning("Invalid token for access %s", request.path)
|
_LOGGER.error("Invalid token for access %s", request.path)
|
||||||
raise HTTPUnauthorized()
|
raise HTTPForbidden()
|
||||||
|
@@ -1,19 +1,21 @@
|
|||||||
"""Init file for HassIO network rest api."""
|
"""Init file for Hass.io network RESTful API."""
|
||||||
|
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_AVAILABLE, ATTR_PROVIDER, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM)
|
ATTR_AVAILABLE, ATTR_PROVIDERS, ATTR_SLUG, ATTR_SERVICES, REQUEST_FROM,
|
||||||
|
PROVIDE_SERVICE)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
|
||||||
|
|
||||||
class APIServices(CoreSysAttributes):
|
class APIServices(CoreSysAttributes):
|
||||||
"""Handle rest api for services functions."""
|
"""Handle RESTful API for services functions."""
|
||||||
|
|
||||||
def _extract_service(self, request):
|
def _extract_service(self, request):
|
||||||
"""Return service, throw an exception if it doesn't exist."""
|
"""Return service, throw an exception if it doesn't exist."""
|
||||||
service = self.sys_services.get(request.match_info.get('service'))
|
service = self.sys_services.get(request.match_info.get('service'))
|
||||||
if not service:
|
if not service:
|
||||||
raise RuntimeError("Service does not exist")
|
raise APIError("Service does not exist")
|
||||||
|
|
||||||
return service
|
return service
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ class APIServices(CoreSysAttributes):
|
|||||||
services.append({
|
services.append({
|
||||||
ATTR_SLUG: service.slug,
|
ATTR_SLUG: service.slug,
|
||||||
ATTR_AVAILABLE: service.enabled,
|
ATTR_AVAILABLE: service.enabled,
|
||||||
ATTR_PROVIDER: service.provider,
|
ATTR_PROVIDERS: service.providers,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {ATTR_SERVICES: services}
|
return {ATTR_SERVICES: services}
|
||||||
@@ -35,21 +37,39 @@ class APIServices(CoreSysAttributes):
|
|||||||
"""Write data into a service."""
|
"""Write data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
body = await api_validate(service.schema, request)
|
body = await api_validate(service.schema, request)
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
return service.set_service_data(request[REQUEST_FROM], body)
|
_check_access(request, service.slug)
|
||||||
|
service.set_service_data(addon, body)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def get_service(self, request):
|
async def get_service(self, request):
|
||||||
"""Read data into a service."""
|
"""Read data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
|
|
||||||
return {
|
# Access
|
||||||
ATTR_AVAILABLE: service.enabled,
|
_check_access(request, service.slug)
|
||||||
service.slug: service.get_service_data(),
|
|
||||||
}
|
if not service.enabled:
|
||||||
|
raise APIError("Service not enabled")
|
||||||
|
return service.get_service_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def del_service(self, request):
|
async def del_service(self, request):
|
||||||
"""Delete data into a service."""
|
"""Delete data into a service."""
|
||||||
service = self._extract_service(request)
|
service = self._extract_service(request)
|
||||||
return service.del_service_data(request[REQUEST_FROM])
|
addon = request[REQUEST_FROM]
|
||||||
|
|
||||||
|
# Access
|
||||||
|
_check_access(request, service.slug, True)
|
||||||
|
service.del_service_data(addon)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_access(request, service, provide=False):
|
||||||
|
"""Raise error if the rights are wrong."""
|
||||||
|
addon = request[REQUEST_FROM]
|
||||||
|
if not addon.services_role.get(service):
|
||||||
|
raise APIForbidden(f"No access to {service} service!")
|
||||||
|
|
||||||
|
if provide and addon.services_role.get(service) != PROVIDE_SERVICE:
|
||||||
|
raise APIForbidden(f"No access to write {service} service!")
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO snapshot rest api."""
|
"""Init file for Hass.io snapshot RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,6 +14,7 @@ from ..const import (
|
|||||||
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
|
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
|
||||||
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
|
ATTR_SNAPSHOTS, ATTR_PASSWORD, ATTR_PROTECTED, CONTENT_TYPE_TAR)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,13 +47,13 @@ SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
|
|||||||
|
|
||||||
|
|
||||||
class APISnapshots(CoreSysAttributes):
|
class APISnapshots(CoreSysAttributes):
|
||||||
"""Handle rest api for snapshot functions."""
|
"""Handle RESTful API for snapshot functions."""
|
||||||
|
|
||||||
def _extract_snapshot(self, request):
|
def _extract_snapshot(self, request):
|
||||||
"""Return snapshot, throw an exception if it doesn't exist."""
|
"""Return snapshot, throw an exception if it doesn't exist."""
|
||||||
snapshot = self.sys_snapshots.get(request.match_info.get('snapshot'))
|
snapshot = self.sys_snapshots.get(request.match_info.get('snapshot'))
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
raise RuntimeError("Snapshot does not exist")
|
raise APIError("Snapshot does not exist")
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO supervisor rest api."""
|
"""Init file for Hass.io Supervisor RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -13,7 +13,9 @@ from ..const import (
|
|||||||
ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ,
|
ATTR_MEMORY_LIMIT, ATTR_NETWORK_RX, ATTR_NETWORK_TX, ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON)
|
ATTR_BLK_WRITE, CONTENT_TYPE_BINARY, ATTR_ICON)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import validate_timezone, WAIT_BOOT, REPOSITORIES, CHANNELS
|
from ..validate import WAIT_BOOT, REPOSITORIES, CHANNELS
|
||||||
|
from ..exceptions import APIError
|
||||||
|
from ..utils.validate import validate_timezone
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -30,11 +32,11 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
class APISupervisor(CoreSysAttributes):
|
class APISupervisor(CoreSysAttributes):
|
||||||
"""Handle rest api for supervisor functions."""
|
"""Handle RESTful API for Supervisor functions."""
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def ping(self, request):
|
async def ping(self, request):
|
||||||
"""Return ok for signal that the api is ready."""
|
"""Return ok for signal that the API is ready."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -59,7 +61,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
ATTR_VERSION: HASSIO_VERSION,
|
ATTR_VERSION: HASSIO_VERSION,
|
||||||
ATTR_LAST_VERSION: self.sys_updater.version_hassio,
|
ATTR_LAST_VERSION: self.sys_updater.version_hassio,
|
||||||
ATTR_CHANNEL: self.sys_updater.channel,
|
ATTR_CHANNEL: self.sys_updater.channel,
|
||||||
ATTR_ARCH: self.sys_arch,
|
ATTR_ARCH: self.sys_supervisor.arch,
|
||||||
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
ATTR_WAIT_BOOT: self.sys_config.wait_boot,
|
||||||
ATTR_TIMEZONE: self.sys_config.timezone,
|
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||||
ATTR_ADDONS: list_addons,
|
ATTR_ADDONS: list_addons,
|
||||||
@@ -68,7 +70,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options(self, request):
|
async def options(self, request):
|
||||||
"""Set supervisor options."""
|
"""Set Supervisor options."""
|
||||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
if ATTR_CHANNEL in body:
|
if ATTR_CHANNEL in body:
|
||||||
@@ -93,7 +95,7 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_supervisor.stats()
|
stats = await self.sys_supervisor.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise RuntimeError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -107,32 +109,30 @@ class APISupervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update(self, request):
|
async def update(self, request):
|
||||||
"""Update supervisor OS."""
|
"""Update Supervisor OS."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_updater.version_hassio)
|
version = body.get(ATTR_VERSION, self.sys_updater.version_hassio)
|
||||||
|
|
||||||
if version == self.sys_supervisor.version:
|
if version == self.sys_supervisor.version:
|
||||||
raise RuntimeError("Version {} is already in use".format(version))
|
raise APIError("Version {} is already in use".format(version))
|
||||||
|
|
||||||
return await asyncio.shield(
|
return await asyncio.shield(self.sys_supervisor.update(version))
|
||||||
self.sys_supervisor.update(version))
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload(self, request):
|
async def reload(self, request):
|
||||||
"""Reload addons, config etc."""
|
"""Reload add-ons, configuration, etc."""
|
||||||
tasks = [
|
tasks = [
|
||||||
self.sys_updater.reload(),
|
self.sys_updater.reload(),
|
||||||
]
|
]
|
||||||
results, _ = await asyncio.shield(
|
results, _ = await asyncio.shield(asyncio.wait(tasks))
|
||||||
asyncio.wait(tasks))
|
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
if result.exception() is not None:
|
if result.exception() is not None:
|
||||||
raise RuntimeError("Some reload task fails!")
|
raise APIError("Some reload task fails!")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request):
|
def logs(self, request):
|
||||||
"""Return supervisor docker logs."""
|
"""Return supervisor Docker logs."""
|
||||||
return self.sys_supervisor.logs()
|
return self.sys_supervisor.logs()
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO util for rest api."""
|
"""Init file for Hass.io util for RESTful API."""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
|
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
|
||||||
CONTENT_TYPE_BINARY)
|
CONTENT_TYPE_BINARY)
|
||||||
from ..exceptions import HassioError
|
from ..exceptions import HassioError, APIError, APIForbidden
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -21,19 +21,19 @@ def json_loads(data):
|
|||||||
try:
|
try:
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise RuntimeError("Invalid json")
|
raise APIError("Invalid json")
|
||||||
|
|
||||||
|
|
||||||
def api_process(method):
|
def api_process(method):
|
||||||
"""Wrap function with true/false calls to rest api."""
|
"""Wrap function with true/false calls to rest api."""
|
||||||
async def wrap_api(api, *args, **kwargs):
|
async def wrap_api(api, *args, **kwargs):
|
||||||
"""Return api information."""
|
"""Return API information."""
|
||||||
try:
|
try:
|
||||||
answer = await method(api, *args, **kwargs)
|
answer = await method(api, *args, **kwargs)
|
||||||
except HassioError:
|
except (APIError, APIForbidden) as err:
|
||||||
return api_return_error()
|
|
||||||
except RuntimeError as err:
|
|
||||||
return api_return_error(message=str(err))
|
return api_return_error(message=str(err))
|
||||||
|
except HassioError:
|
||||||
|
return api_return_error(message="Unknown Error, see logs")
|
||||||
|
|
||||||
if isinstance(answer, dict):
|
if isinstance(answer, dict):
|
||||||
return api_return_ok(data=answer)
|
return api_return_ok(data=answer)
|
||||||
@@ -55,7 +55,7 @@ def api_process_raw(content):
|
|||||||
try:
|
try:
|
||||||
msg_data = await method(api, *args, **kwargs)
|
msg_data = await method(api, *args, **kwargs)
|
||||||
msg_type = content
|
msg_type = content
|
||||||
except RuntimeError as err:
|
except (APIError, APIForbidden) as err:
|
||||||
msg_data = str(err).encode()
|
msg_data = str(err).encode()
|
||||||
msg_type = CONTENT_TYPE_BINARY
|
msg_type = CONTENT_TYPE_BINARY
|
||||||
except HassioError:
|
except HassioError:
|
||||||
@@ -90,6 +90,6 @@ async def api_validate(schema, request):
|
|||||||
try:
|
try:
|
||||||
data = schema(data)
|
data = schema(data)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
raise RuntimeError(humanize_error(data, ex)) from None
|
raise APIError(humanize_error(data, ex)) from None
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
44
hassio/arch.json
Normal file
44
hassio/arch.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"raspberrypi": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"raspberrypi2": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"raspberrypi3": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"raspberrypi3-64": [
|
||||||
|
"aarch64",
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"tinker": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"odroid-c2": [
|
||||||
|
"aarch64"
|
||||||
|
],
|
||||||
|
"odroid-xu": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"orangepi-prime": [
|
||||||
|
"aarch64"
|
||||||
|
],
|
||||||
|
"qemux86": [
|
||||||
|
"i386"
|
||||||
|
],
|
||||||
|
"qemux86-64": [
|
||||||
|
"amd64",
|
||||||
|
"i386"
|
||||||
|
],
|
||||||
|
"qemuarm": [
|
||||||
|
"armhf"
|
||||||
|
],
|
||||||
|
"qemuarm-64": [
|
||||||
|
"aarch64"
|
||||||
|
],
|
||||||
|
"intel-nuc": [
|
||||||
|
"amd64",
|
||||||
|
"i386"
|
||||||
|
]
|
||||||
|
}
|
67
hassio/arch.py
Normal file
67
hassio/arch.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
"""Handle Arch for underlay maschine/platforms."""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .coresys import CoreSysAttributes, CoreSys
|
||||||
|
from .exceptions import HassioArchNotFound
|
||||||
|
from .utils.json import read_json_file
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CpuArch(CoreSysAttributes):
|
||||||
|
"""Manage available architectures."""
|
||||||
|
|
||||||
|
def __init__(self, coresys: CoreSys) -> None:
|
||||||
|
"""Initialize CPU Architecture handler."""
|
||||||
|
self.coresys = coresys
|
||||||
|
self._supported_arch: List[str] = []
|
||||||
|
self._default_arch: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default(self) -> str:
|
||||||
|
"""Return system default arch."""
|
||||||
|
return self._default_arch
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supervisor(self) -> str:
|
||||||
|
"""Return supervisor arch."""
|
||||||
|
return self.sys_supervisor.arch
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported(self) -> List[str]:
|
||||||
|
"""Return support arch by CPU/Machine."""
|
||||||
|
return self._supported_arch
|
||||||
|
|
||||||
|
async def load(self) -> None:
|
||||||
|
"""Load data and initialize default arch."""
|
||||||
|
try:
|
||||||
|
arch_file = Path(__file__).parent.joinpath("arch.json")
|
||||||
|
arch_data = read_json_file(arch_file)
|
||||||
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
|
_LOGGER.warning("Can't read arch json: %s", err)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Evaluate current CPU/Platform
|
||||||
|
if not self.sys_machine or self.sys_machine not in arch_data:
|
||||||
|
_LOGGER.warning("Can't detect underlay machine type!")
|
||||||
|
self._default_arch = self.sys_supervisor.arch
|
||||||
|
self._supported_arch.append(self.default)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use configs from arch.json
|
||||||
|
self._supported_arch.extend(arch_data[self.sys_machine])
|
||||||
|
self._default_arch = self.supported[0]
|
||||||
|
|
||||||
|
def is_supported(self, arch_list: List[str]) -> bool:
|
||||||
|
"""Return True if there is a supported arch by this platform."""
|
||||||
|
return not set(self.supported).isdisjoint(set(arch_list))
|
||||||
|
|
||||||
|
def match(self, arch_list: List[str]) -> str:
|
||||||
|
"""Return best match for this CPU/Platform."""
|
||||||
|
for self_arch in self.supported:
|
||||||
|
if self_arch in arch_list:
|
||||||
|
return self_arch
|
||||||
|
raise HassioArchNotFound()
|
95
hassio/auth.py
Normal file
95
hassio/auth.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
FILE_HASSIO_AUTH, ATTR_PASSWORD, ATTR_USERNAME, ATTR_ADDON)
|
||||||
|
from .coresys import CoreSysAttributes
|
||||||
|
from .utils.json import JsonConfig
|
||||||
|
from .validate import SCHEMA_AUTH_CONFIG
|
||||||
|
from .exceptions import AuthError, HomeAssistantAPIError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Auth(JsonConfig, CoreSysAttributes):
|
||||||
|
"""Manage SSO for Add-ons with Home Assistant user."""
|
||||||
|
|
||||||
|
def __init__(self, coresys):
|
||||||
|
"""Initialize updater."""
|
||||||
|
super().__init__(FILE_HASSIO_AUTH, SCHEMA_AUTH_CONFIG)
|
||||||
|
self.coresys = coresys
|
||||||
|
|
||||||
|
def _check_cache(self, username, password):
|
||||||
|
"""Check password in cache."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
password_h = _rehash(password, username)
|
||||||
|
|
||||||
|
if self._data.get(username_h) == password_h:
|
||||||
|
_LOGGER.info("Cache hit for %s", username)
|
||||||
|
return True
|
||||||
|
|
||||||
|
_LOGGER.warning("No cache hit for %s", username)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _update_cache(self, username, password):
|
||||||
|
"""Cache a username, password."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
password_h = _rehash(password, username)
|
||||||
|
|
||||||
|
if self._data.get(username_h) == password_h:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data[username_h] = password_h
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
def _dismatch_cache(self, username, password):
|
||||||
|
"""Remove user from cache."""
|
||||||
|
username_h = _rehash(username)
|
||||||
|
password_h = _rehash(password, username)
|
||||||
|
|
||||||
|
if self._data.get(username_h) != password_h:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data.pop(username_h, None)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
async def check_login(self, addon, username, password):
|
||||||
|
"""Check username login."""
|
||||||
|
if password is None:
|
||||||
|
_LOGGER.error("None as password is not supported!")
|
||||||
|
raise AuthError()
|
||||||
|
_LOGGER.info("Auth request from %s for %s", addon.slug, username)
|
||||||
|
|
||||||
|
# Check API state
|
||||||
|
if not await self.sys_homeassistant.check_api_state():
|
||||||
|
_LOGGER.info("Home Assistant not running, check cache")
|
||||||
|
return self._check_cache(username, password)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self.sys_homeassistant.make_request(
|
||||||
|
'post', 'api/hassio_auth', json={
|
||||||
|
ATTR_USERNAME: username,
|
||||||
|
ATTR_PASSWORD: password,
|
||||||
|
ATTR_ADDON: addon.slug,
|
||||||
|
}) as req:
|
||||||
|
|
||||||
|
if req.status == 200:
|
||||||
|
_LOGGER.info("Success login from %s", username)
|
||||||
|
self._update_cache(username, password)
|
||||||
|
return True
|
||||||
|
|
||||||
|
_LOGGER.warning("Wrong login from %s", username)
|
||||||
|
self._dismatch_cache(username, password)
|
||||||
|
return False
|
||||||
|
except HomeAssistantAPIError:
|
||||||
|
_LOGGER.error("Can't request auth on Home Assistant!")
|
||||||
|
|
||||||
|
raise AuthError()
|
||||||
|
|
||||||
|
|
||||||
|
def _rehash(value, salt2=""):
|
||||||
|
"""Rehash a value."""
|
||||||
|
for idx in range(1, 20):
|
||||||
|
value = hashlib.sha256(f"{value}{idx}{salt2}".encode()).hexdigest()
|
||||||
|
return value
|
@@ -1,43 +1,47 @@
|
|||||||
"""Bootstrap HassIO."""
|
"""Bootstrap Hass.io."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import signal
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
|
||||||
from colorlog import ColoredFormatter
|
from colorlog import ColoredFormatter
|
||||||
|
|
||||||
from .core import HassIO
|
|
||||||
from .addons import AddonManager
|
from .addons import AddonManager
|
||||||
from .api import RestAPI
|
from .api import RestAPI
|
||||||
|
from .arch import CpuArch
|
||||||
|
from .auth import Auth
|
||||||
from .const import SOCKET_DOCKER
|
from .const import SOCKET_DOCKER
|
||||||
|
from .core import HassIO
|
||||||
from .coresys import CoreSys
|
from .coresys import CoreSys
|
||||||
from .supervisor import Supervisor
|
from .dbus import DBusManager
|
||||||
|
from .discovery import Discovery
|
||||||
|
from .hassos import HassOS
|
||||||
from .homeassistant import HomeAssistant
|
from .homeassistant import HomeAssistant
|
||||||
|
from .host import HostManager
|
||||||
|
from .services import ServiceManager
|
||||||
from .snapshots import SnapshotManager
|
from .snapshots import SnapshotManager
|
||||||
|
from .supervisor import Supervisor
|
||||||
from .tasks import Tasks
|
from .tasks import Tasks
|
||||||
from .updater import Updater
|
from .updater import Updater
|
||||||
from .services import ServiceManager
|
|
||||||
from .services import Discovery
|
|
||||||
from .host import HostManager
|
|
||||||
from .dbus import DBusManager
|
|
||||||
from .hassos import HassOS
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ENV_SHARE = 'SUPERVISOR_SHARE'
|
ENV_SHARE = "SUPERVISOR_SHARE"
|
||||||
ENV_NAME = 'SUPERVISOR_NAME'
|
ENV_NAME = "SUPERVISOR_NAME"
|
||||||
ENV_REPO = 'HOMEASSISTANT_REPOSITORY'
|
ENV_REPO = "HOMEASSISTANT_REPOSITORY"
|
||||||
|
|
||||||
MACHINE_ID = Path('/etc/machine-id')
|
MACHINE_ID = Path("/etc/machine-id")
|
||||||
|
|
||||||
|
|
||||||
def initialize_coresys(loop):
|
async def initialize_coresys():
|
||||||
"""Initialize HassIO coresys/objects."""
|
"""Initialize HassIO coresys/objects."""
|
||||||
coresys = CoreSys(loop)
|
coresys = CoreSys()
|
||||||
|
|
||||||
# Initialize core objects
|
# Initialize core objects
|
||||||
coresys.core = HassIO(coresys)
|
coresys.core = HassIO(coresys)
|
||||||
|
coresys.arch = CpuArch(coresys)
|
||||||
|
coresys.auth = Auth(coresys)
|
||||||
coresys.updater = Updater(coresys)
|
coresys.updater = Updater(coresys)
|
||||||
coresys.api = RestAPI(coresys)
|
coresys.api = RestAPI(coresys)
|
||||||
coresys.supervisor = Supervisor(coresys)
|
coresys.supervisor = Supervisor(coresys)
|
||||||
@@ -62,55 +66,54 @@ def initialize_coresys(loop):
|
|||||||
|
|
||||||
|
|
||||||
def initialize_system_data(coresys):
|
def initialize_system_data(coresys):
|
||||||
"""Setup default config and create folders."""
|
"""Set up the default configuration and create folders."""
|
||||||
config = coresys.config
|
config = coresys.config
|
||||||
|
|
||||||
# homeassistant config folder
|
# Home Assistant configuration folder
|
||||||
if not config.path_homeassistant.is_dir():
|
if not config.path_homeassistant.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info("Create Home Assistant configuration folder %s",
|
||||||
"Create Home-Assistant config folder %s",
|
|
||||||
config.path_homeassistant)
|
config.path_homeassistant)
|
||||||
config.path_homeassistant.mkdir()
|
config.path_homeassistant.mkdir()
|
||||||
|
|
||||||
# hassio ssl folder
|
# hassio ssl folder
|
||||||
if not config.path_ssl.is_dir():
|
if not config.path_ssl.is_dir():
|
||||||
_LOGGER.info("Create hassio ssl folder %s", config.path_ssl)
|
_LOGGER.info("Create Hass.io SSL/TLS folder %s", config.path_ssl)
|
||||||
config.path_ssl.mkdir()
|
config.path_ssl.mkdir()
|
||||||
|
|
||||||
# hassio addon data folder
|
# hassio addon data folder
|
||||||
if not config.path_addons_data.is_dir():
|
if not config.path_addons_data.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info("Create Hass.io Add-on data folder %s",
|
||||||
"Create hassio addon data folder %s", config.path_addons_data)
|
config.path_addons_data)
|
||||||
config.path_addons_data.mkdir(parents=True)
|
config.path_addons_data.mkdir(parents=True)
|
||||||
|
|
||||||
if not config.path_addons_local.is_dir():
|
if not config.path_addons_local.is_dir():
|
||||||
_LOGGER.info("Create hassio addon local repository folder %s",
|
_LOGGER.info("Create Hass.io Add-on local repository folder %s",
|
||||||
config.path_addons_local)
|
config.path_addons_local)
|
||||||
config.path_addons_local.mkdir(parents=True)
|
config.path_addons_local.mkdir(parents=True)
|
||||||
|
|
||||||
if not config.path_addons_git.is_dir():
|
if not config.path_addons_git.is_dir():
|
||||||
_LOGGER.info("Create hassio addon git repositories folder %s",
|
_LOGGER.info("Create Hass.io Add-on git repositories folder %s",
|
||||||
config.path_addons_git)
|
config.path_addons_git)
|
||||||
config.path_addons_git.mkdir(parents=True)
|
config.path_addons_git.mkdir(parents=True)
|
||||||
|
|
||||||
# hassio tmp folder
|
# hassio tmp folder
|
||||||
if not config.path_tmp.is_dir():
|
if not config.path_tmp.is_dir():
|
||||||
_LOGGER.info("Create hassio temp folder %s", config.path_tmp)
|
_LOGGER.info("Create Hass.io temp folder %s", config.path_tmp)
|
||||||
config.path_tmp.mkdir(parents=True)
|
config.path_tmp.mkdir(parents=True)
|
||||||
|
|
||||||
# hassio backup folder
|
# hassio backup folder
|
||||||
if not config.path_backup.is_dir():
|
if not config.path_backup.is_dir():
|
||||||
_LOGGER.info("Create hassio backup folder %s", config.path_backup)
|
_LOGGER.info("Create Hass.io backup folder %s", config.path_backup)
|
||||||
config.path_backup.mkdir()
|
config.path_backup.mkdir()
|
||||||
|
|
||||||
# share folder
|
# share folder
|
||||||
if not config.path_share.is_dir():
|
if not config.path_share.is_dir():
|
||||||
_LOGGER.info("Create hassio share folder %s", config.path_share)
|
_LOGGER.info("Create Hass.io share folder %s", config.path_share)
|
||||||
config.path_share.mkdir()
|
config.path_share.mkdir()
|
||||||
|
|
||||||
# apparmor folder
|
# apparmor folder
|
||||||
if not config.path_apparmor.is_dir():
|
if not config.path_apparmor.is_dir():
|
||||||
_LOGGER.info("Create hassio apparmor folder %s", config.path_apparmor)
|
_LOGGER.info("Create Hass.io Apparmor folder %s", config.path_apparmor)
|
||||||
config.path_apparmor.mkdir()
|
config.path_apparmor.mkdir()
|
||||||
|
|
||||||
return config
|
return config
|
||||||
@@ -126,31 +129,31 @@ def migrate_system_env(coresys):
|
|||||||
try:
|
try:
|
||||||
old_build.rmdir()
|
old_build.rmdir()
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.warning("Can't cleanup old addons build dir.")
|
_LOGGER.warning("Can't cleanup old Add-on build directory")
|
||||||
|
|
||||||
|
|
||||||
def initialize_logging():
|
def initialize_logging():
|
||||||
"""Setup the logging."""
|
"""Setup the logging."""
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
|
||||||
"[%(name)s] %(message)s")
|
colorfmt = f"%(log_color)s{fmt}%(reset)s"
|
||||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
datefmt = "%y-%m-%d %H:%M:%S"
|
||||||
datefmt = '%y-%m-%d %H:%M:%S'
|
|
||||||
|
|
||||||
# suppress overly verbose logs from libraries that aren't helpful
|
# suppress overly verbose logs from libraries that aren't helpful
|
||||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
logging.getLogger().handlers[0].setFormatter(
|
||||||
|
ColoredFormatter(
|
||||||
colorfmt,
|
colorfmt,
|
||||||
datefmt=datefmt,
|
datefmt=datefmt,
|
||||||
reset=True,
|
reset=True,
|
||||||
log_colors={
|
log_colors={
|
||||||
'DEBUG': 'cyan',
|
"DEBUG": "cyan",
|
||||||
'INFO': 'green',
|
"INFO": "green",
|
||||||
'WARNING': 'yellow',
|
"WARNING": "yellow",
|
||||||
'ERROR': 'red',
|
"ERROR": "red",
|
||||||
'CRITICAL': 'red',
|
"CRITICAL": "red",
|
||||||
}
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
@@ -166,38 +169,38 @@ def check_environment():
|
|||||||
|
|
||||||
# check docker socket
|
# check docker socket
|
||||||
if not SOCKET_DOCKER.is_socket():
|
if not SOCKET_DOCKER.is_socket():
|
||||||
_LOGGER.fatal("Can't find docker socket!")
|
_LOGGER.fatal("Can't find Docker socket!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check socat exec
|
# check socat exec
|
||||||
if not shutil.which('socat'):
|
if not shutil.which("socat"):
|
||||||
_LOGGER.fatal("Can't find socat program!")
|
_LOGGER.fatal("Can't find socat!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check socat exec
|
# check socat exec
|
||||||
if not shutil.which('gdbus'):
|
if not shutil.which("gdbus"):
|
||||||
_LOGGER.fatal("Can't find gdbus program!")
|
_LOGGER.fatal("Can't find gdbus!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def reg_signal(loop):
|
def reg_signal(loop):
|
||||||
"""Register SIGTERM, SIGKILL to stop system."""
|
"""Register SIGTERM and SIGKILL to stop system."""
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(signal.SIGTERM,
|
||||||
signal.SIGTERM, lambda: loop.call_soon(loop.stop))
|
lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGTERM")
|
_LOGGER.warning("Could not bind to SIGTERM")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(signal.SIGHUP,
|
||||||
signal.SIGHUP, lambda: loop.call_soon(loop.stop))
|
lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGHUP")
|
_LOGGER.warning("Could not bind to SIGHUP")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(
|
loop.add_signal_handler(signal.SIGINT,
|
||||||
signal.SIGINT, lambda: loop.call_soon(loop.stop))
|
lambda: loop.call_soon(loop.stop))
|
||||||
except (ValueError, RuntimeError):
|
except (ValueError, RuntimeError):
|
||||||
_LOGGER.warning("Could not bind to SIGINT")
|
_LOGGER.warning("Could not bind to SIGINT")
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Bootstrap HassIO."""
|
"""Bootstrap Hass.io."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -16,7 +16,7 @@ from .validate import SCHEMA_HASSIO_CONFIG
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
|
HOMEASSISTANT_CONFIG = PurePath('homeassistant')
|
||||||
|
|
||||||
HASSIO_SSL = PurePath("ssl")
|
HASSIO_SSL = PurePath("ssl")
|
||||||
|
|
||||||
@@ -93,17 +93,17 @@ class CoreConfig(JsonConfig):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_hassio(self):
|
def path_hassio(self):
|
||||||
"""Return hassio data path."""
|
"""Return Hass.io data path."""
|
||||||
return HASSIO_DATA
|
return HASSIO_DATA
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_hassio(self):
|
def path_extern_hassio(self):
|
||||||
"""Return hassio data path extern for docker."""
|
"""Return Hass.io data path external for Docker."""
|
||||||
return PurePath(os.environ['SUPERVISOR_SHARE'])
|
return PurePath(os.environ['SUPERVISOR_SHARE'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_homeassistant(self):
|
def path_extern_homeassistant(self):
|
||||||
"""Return config path extern for docker."""
|
"""Return config path external for Docker."""
|
||||||
return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG))
|
return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -113,7 +113,7 @@ class CoreConfig(JsonConfig):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_ssl(self):
|
def path_extern_ssl(self):
|
||||||
"""Return SSL path extern for docker."""
|
"""Return SSL path external for Docker."""
|
||||||
return str(PurePath(self.path_extern_hassio, HASSIO_SSL))
|
return str(PurePath(self.path_extern_hassio, HASSIO_SSL))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -123,42 +123,42 @@ class CoreConfig(JsonConfig):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_core(self):
|
def path_addons_core(self):
|
||||||
"""Return git path for core addons."""
|
"""Return git path for core Add-ons."""
|
||||||
return Path(HASSIO_DATA, ADDONS_CORE)
|
return Path(HASSIO_DATA, ADDONS_CORE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_git(self):
|
def path_addons_git(self):
|
||||||
"""Return path for git addons."""
|
"""Return path for Git Add-on."""
|
||||||
return Path(HASSIO_DATA, ADDONS_GIT)
|
return Path(HASSIO_DATA, ADDONS_GIT)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_local(self):
|
def path_addons_local(self):
|
||||||
"""Return path for customs addons."""
|
"""Return path for custom Add-ons."""
|
||||||
return Path(HASSIO_DATA, ADDONS_LOCAL)
|
return Path(HASSIO_DATA, ADDONS_LOCAL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_addons_local(self):
|
def path_extern_addons_local(self):
|
||||||
"""Return path for customs addons."""
|
"""Return path for custom Add-ons."""
|
||||||
return PurePath(self.path_extern_hassio, ADDONS_LOCAL)
|
return PurePath(self.path_extern_hassio, ADDONS_LOCAL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_data(self):
|
def path_addons_data(self):
|
||||||
"""Return root addon data folder."""
|
"""Return root Add-on data folder."""
|
||||||
return Path(HASSIO_DATA, ADDONS_DATA)
|
return Path(HASSIO_DATA, ADDONS_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_addons_data(self):
|
def path_extern_addons_data(self):
|
||||||
"""Return root addon data folder extern for docker."""
|
"""Return root add-on data folder external for Docker."""
|
||||||
return PurePath(self.path_extern_hassio, ADDONS_DATA)
|
return PurePath(self.path_extern_hassio, ADDONS_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_tmp(self):
|
def path_tmp(self):
|
||||||
"""Return hass.io temp folder."""
|
"""Return Hass.io temp folder."""
|
||||||
return Path(HASSIO_DATA, TMP_DATA)
|
return Path(HASSIO_DATA, TMP_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_tmp(self):
|
def path_extern_tmp(self):
|
||||||
"""Return hass.io temp folder for docker."""
|
"""Return Hass.io temp folder for Docker."""
|
||||||
return PurePath(self.path_extern_hassio, TMP_DATA)
|
return PurePath(self.path_extern_hassio, TMP_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -168,7 +168,7 @@ class CoreConfig(JsonConfig):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_backup(self):
|
def path_extern_backup(self):
|
||||||
"""Return root backup data folder extern for docker."""
|
"""Return root backup data folder external for Docker."""
|
||||||
return PurePath(self.path_extern_hassio, BACKUP_DATA)
|
return PurePath(self.path_extern_hassio, BACKUP_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -178,17 +178,17 @@ class CoreConfig(JsonConfig):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def path_apparmor(self):
|
def path_apparmor(self):
|
||||||
"""Return root apparmor profile folder."""
|
"""Return root Apparmor profile folder."""
|
||||||
return Path(HASSIO_DATA, APPARMOR_DATA)
|
return Path(HASSIO_DATA, APPARMOR_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_share(self):
|
def path_extern_share(self):
|
||||||
"""Return root share data folder extern for docker."""
|
"""Return root share data folder external for Docker."""
|
||||||
return PurePath(self.path_extern_hassio, SHARE_DATA)
|
return PurePath(self.path_extern_hassio, SHARE_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addons_repositories(self):
|
def addons_repositories(self):
|
||||||
"""Return list of addons custom repositories."""
|
"""Return list of custom Add-on repositories."""
|
||||||
return self._data[ATTR_ADDONS_CUSTOM_LIST]
|
return self._data[ATTR_ADDONS_CUSTOM_LIST]
|
||||||
|
|
||||||
def add_addon_repository(self, repo):
|
def add_addon_repository(self, repo):
|
||||||
|
458
hassio/const.py
458
hassio/const.py
@@ -1,245 +1,293 @@
|
|||||||
"""Const file for HassIO."""
|
"""Constants file for Hass.io."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
|
|
||||||
HASSIO_VERSION = '128'
|
HASSIO_VERSION = "145"
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
URL_HASSIO_VERSION = \
|
URL_HASSIO_VERSION = "https://s3.amazonaws.com/hassio-version/{channel}.json"
|
||||||
"https://s3.amazonaws.com/hassio-version/{channel}.json"
|
URL_HASSIO_APPARMOR = "https://s3.amazonaws.com/hassio-version/apparmor.txt"
|
||||||
URL_HASSIO_APPARMOR = \
|
|
||||||
"https://s3.amazonaws.com/hassio-version/apparmor.txt"
|
|
||||||
|
|
||||||
URL_HASSOS_OTA = (
|
URL_HASSOS_OTA = ("https://github.com/home-assistant/hassos/releases/download/"
|
||||||
"https://github.com/home-assistant/hassos/releases/download/"
|
|
||||||
"{version}/hassos_{board}-{version}.raucb")
|
"{version}/hassos_{board}-{version}.raucb")
|
||||||
|
|
||||||
HASSIO_DATA = Path("/data")
|
HASSIO_DATA = Path("/data")
|
||||||
|
|
||||||
|
FILE_HASSIO_AUTH = Path(HASSIO_DATA, "auth.json")
|
||||||
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
|
||||||
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
|
||||||
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
|
||||||
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
|
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
|
||||||
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
|
FILE_HASSIO_SERVICES = Path(HASSIO_DATA, "services.json")
|
||||||
|
FILE_HASSIO_DISCOVERY = Path(HASSIO_DATA, "discovery.json")
|
||||||
|
|
||||||
SOCKET_DOCKER = Path("/var/run/docker.sock")
|
SOCKET_DOCKER = Path("/var/run/docker.sock")
|
||||||
|
|
||||||
DOCKER_NETWORK = 'hassio'
|
DOCKER_NETWORK = "hassio"
|
||||||
DOCKER_NETWORK_MASK = ip_network('172.30.32.0/23')
|
DOCKER_NETWORK_MASK = ip_network("172.30.32.0/23")
|
||||||
DOCKER_NETWORK_RANGE = ip_network('172.30.33.0/24')
|
DOCKER_NETWORK_RANGE = ip_network("172.30.33.0/24")
|
||||||
|
|
||||||
LABEL_VERSION = 'io.hass.version'
|
LABEL_VERSION = "io.hass.version"
|
||||||
LABEL_ARCH = 'io.hass.arch'
|
LABEL_ARCH = "io.hass.arch"
|
||||||
LABEL_TYPE = 'io.hass.type'
|
LABEL_TYPE = "io.hass.type"
|
||||||
LABEL_MACHINE = 'io.hass.machine'
|
LABEL_MACHINE = "io.hass.machine"
|
||||||
|
|
||||||
META_ADDON = 'addon'
|
META_ADDON = "addon"
|
||||||
META_SUPERVISOR = 'supervisor'
|
META_SUPERVISOR = "supervisor"
|
||||||
META_HOMEASSISTANT = 'homeassistant'
|
META_HOMEASSISTANT = "homeassistant"
|
||||||
|
|
||||||
JSON_RESULT = 'result'
|
JSON_RESULT = "result"
|
||||||
JSON_DATA = 'data'
|
JSON_DATA = "data"
|
||||||
JSON_MESSAGE = 'message'
|
JSON_MESSAGE = "message"
|
||||||
|
|
||||||
RESULT_ERROR = 'error'
|
RESULT_ERROR = "error"
|
||||||
RESULT_OK = 'ok'
|
RESULT_OK = "ok"
|
||||||
|
|
||||||
CONTENT_TYPE_BINARY = 'application/octet-stream'
|
CONTENT_TYPE_BINARY = "application/octet-stream"
|
||||||
CONTENT_TYPE_PNG = 'image/png'
|
CONTENT_TYPE_PNG = "image/png"
|
||||||
CONTENT_TYPE_JSON = 'application/json'
|
CONTENT_TYPE_JSON = "application/json"
|
||||||
CONTENT_TYPE_TEXT = 'text/plain'
|
CONTENT_TYPE_TEXT = "text/plain"
|
||||||
CONTENT_TYPE_TAR = 'application/tar'
|
CONTENT_TYPE_TAR = "application/tar"
|
||||||
HEADER_HA_ACCESS = 'x-ha-access'
|
CONTENT_TYPE_URL = "application/x-www-form-urlencoded"
|
||||||
HEADER_TOKEN = 'x-hassio-key'
|
HEADER_HA_ACCESS = "x-ha-access"
|
||||||
|
HEADER_TOKEN = "x-hassio-key"
|
||||||
|
|
||||||
ENV_TOKEN = 'HASSIO_TOKEN'
|
ENV_TOKEN = "HASSIO_TOKEN"
|
||||||
ENV_TIME = 'TZ'
|
ENV_TIME = "TZ"
|
||||||
|
|
||||||
REQUEST_FROM = 'HASSIO_FROM'
|
REQUEST_FROM = "HASSIO_FROM"
|
||||||
|
|
||||||
ATTR_MACHINE = 'machine'
|
ATTR_MACHINE = "machine"
|
||||||
ATTR_WAIT_BOOT = 'wait_boot'
|
ATTR_WAIT_BOOT = "wait_boot"
|
||||||
ATTR_DEPLOYMENT = 'deployment'
|
ATTR_DEPLOYMENT = "deployment"
|
||||||
ATTR_WATCHDOG = 'watchdog'
|
ATTR_WATCHDOG = "watchdog"
|
||||||
ATTR_CHANGELOG = 'changelog'
|
ATTR_CHANGELOG = "changelog"
|
||||||
ATTR_DATE = 'date'
|
ATTR_DATE = "date"
|
||||||
ATTR_ARCH = 'arch'
|
ATTR_ARCH = "arch"
|
||||||
ATTR_LONG_DESCRIPTION = 'long_description'
|
ATTR_LONG_DESCRIPTION = "long_description"
|
||||||
ATTR_HOSTNAME = 'hostname'
|
ATTR_HOSTNAME = "hostname"
|
||||||
ATTR_TIMEZONE = 'timezone'
|
ATTR_TIMEZONE = "timezone"
|
||||||
ATTR_ARGS = 'args'
|
ATTR_ARGS = "args"
|
||||||
ATTR_OPERATING_SYSTEM = 'operating_system'
|
ATTR_OPERATING_SYSTEM = "operating_system"
|
||||||
ATTR_CHASSIS = 'chassis'
|
ATTR_CHASSIS = "chassis"
|
||||||
ATTR_TYPE = 'type'
|
ATTR_TYPE = "type"
|
||||||
ATTR_SOURCE = 'source'
|
ATTR_SOURCE = "source"
|
||||||
ATTR_FEATURES = 'features'
|
ATTR_FEATURES = "features"
|
||||||
ATTR_ADDONS = 'addons'
|
ATTR_ADDONS = "addons"
|
||||||
ATTR_VERSION = 'version'
|
ATTR_PROVIDERS = "providers"
|
||||||
ATTR_VERSION_LATEST = 'version_latest'
|
ATTR_VERSION = "version"
|
||||||
ATTR_AUTO_UART = 'auto_uart'
|
ATTR_VERSION_LATEST = "version_latest"
|
||||||
ATTR_LAST_BOOT = 'last_boot'
|
ATTR_AUTO_UART = "auto_uart"
|
||||||
ATTR_LAST_VERSION = 'last_version'
|
ATTR_LAST_BOOT = "last_boot"
|
||||||
ATTR_CHANNEL = 'channel'
|
ATTR_LAST_VERSION = "last_version"
|
||||||
ATTR_NAME = 'name'
|
ATTR_CHANNEL = "channel"
|
||||||
ATTR_SLUG = 'slug'
|
ATTR_NAME = "name"
|
||||||
ATTR_DESCRIPTON = 'description'
|
ATTR_SLUG = "slug"
|
||||||
ATTR_STARTUP = 'startup'
|
ATTR_DESCRIPTON = "description"
|
||||||
ATTR_BOOT = 'boot'
|
ATTR_STARTUP = "startup"
|
||||||
ATTR_PORTS = 'ports'
|
ATTR_BOOT = "boot"
|
||||||
ATTR_PORT = 'port'
|
ATTR_PORTS = "ports"
|
||||||
ATTR_SSL = 'ssl'
|
ATTR_PORT = "port"
|
||||||
ATTR_MAP = 'map'
|
ATTR_SSL = "ssl"
|
||||||
ATTR_WEBUI = 'webui'
|
ATTR_MAP = "map"
|
||||||
ATTR_OPTIONS = 'options'
|
ATTR_WEBUI = "webui"
|
||||||
ATTR_INSTALLED = 'installed'
|
ATTR_OPTIONS = "options"
|
||||||
ATTR_DETACHED = 'detached'
|
ATTR_INSTALLED = "installed"
|
||||||
ATTR_STATE = 'state'
|
ATTR_DETACHED = "detached"
|
||||||
ATTR_SCHEMA = 'schema'
|
ATTR_STATE = "state"
|
||||||
ATTR_IMAGE = 'image'
|
ATTR_SCHEMA = "schema"
|
||||||
ATTR_ICON = 'icon'
|
ATTR_IMAGE = "image"
|
||||||
ATTR_LOGO = 'logo'
|
ATTR_ICON = "icon"
|
||||||
ATTR_STDIN = 'stdin'
|
ATTR_LOGO = "logo"
|
||||||
ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
|
ATTR_STDIN = "stdin"
|
||||||
ATTR_REPOSITORY = 'repository'
|
ATTR_ADDONS_REPOSITORIES = "addons_repositories"
|
||||||
ATTR_REPOSITORIES = 'repositories'
|
ATTR_REPOSITORY = "repository"
|
||||||
ATTR_URL = 'url'
|
ATTR_REPOSITORIES = "repositories"
|
||||||
ATTR_MAINTAINER = 'maintainer'
|
ATTR_URL = "url"
|
||||||
ATTR_PASSWORD = 'password'
|
ATTR_MAINTAINER = "maintainer"
|
||||||
ATTR_TOTP = 'totp'
|
ATTR_PASSWORD = "password"
|
||||||
ATTR_INITIALIZE = 'initialize'
|
ATTR_TOTP = "totp"
|
||||||
ATTR_SESSION = 'session'
|
ATTR_INITIALIZE = "initialize"
|
||||||
ATTR_SESSIONS = 'sessions'
|
ATTR_LOCATON = "location"
|
||||||
ATTR_LOCATON = 'location'
|
ATTR_BUILD = "build"
|
||||||
ATTR_BUILD = 'build'
|
ATTR_DEVICES = "devices"
|
||||||
ATTR_DEVICES = 'devices'
|
ATTR_ENVIRONMENT = "environment"
|
||||||
ATTR_ENVIRONMENT = 'environment'
|
ATTR_HOST_NETWORK = "host_network"
|
||||||
ATTR_HOST_NETWORK = 'host_network'
|
ATTR_HOST_PID = "host_pid"
|
||||||
ATTR_HOST_IPC = 'host_ipc'
|
ATTR_HOST_IPC = "host_ipc"
|
||||||
ATTR_HOST_DBUS = 'host_dbus'
|
ATTR_HOST_DBUS = "host_dbus"
|
||||||
ATTR_NETWORK = 'network'
|
ATTR_NETWORK = "network"
|
||||||
ATTR_TMPFS = 'tmpfs'
|
ATTR_TMPFS = "tmpfs"
|
||||||
ATTR_PRIVILEGED = 'privileged'
|
ATTR_PRIVILEGED = "privileged"
|
||||||
ATTR_USER = 'user'
|
ATTR_USER = "user"
|
||||||
ATTR_SYSTEM = 'system'
|
ATTR_SYSTEM = "system"
|
||||||
ATTR_SNAPSHOTS = 'snapshots'
|
ATTR_SNAPSHOTS = "snapshots"
|
||||||
ATTR_HOMEASSISTANT = 'homeassistant'
|
ATTR_HOMEASSISTANT = "homeassistant"
|
||||||
ATTR_HASSIO = 'hassio'
|
ATTR_HASSIO = "hassio"
|
||||||
ATTR_HASSIO_API = 'hassio_api'
|
ATTR_HASSIO_API = "hassio_api"
|
||||||
ATTR_HOMEASSISTANT_API = 'homeassistant_api'
|
ATTR_HOMEASSISTANT_API = "homeassistant_api"
|
||||||
ATTR_UUID = 'uuid'
|
ATTR_UUID = "uuid"
|
||||||
ATTR_FOLDERS = 'folders'
|
ATTR_FOLDERS = "folders"
|
||||||
ATTR_SIZE = 'size'
|
ATTR_SIZE = "size"
|
||||||
ATTR_TYPE = 'type'
|
ATTR_TYPE = "type"
|
||||||
ATTR_TIMEOUT = 'timeout'
|
ATTR_TIMEOUT = "timeout"
|
||||||
ATTR_AUTO_UPDATE = 'auto_update'
|
ATTR_AUTO_UPDATE = "auto_update"
|
||||||
ATTR_CUSTOM = 'custom'
|
ATTR_CUSTOM = "custom"
|
||||||
ATTR_AUDIO = 'audio'
|
ATTR_AUDIO = "audio"
|
||||||
ATTR_AUDIO_INPUT = 'audio_input'
|
ATTR_AUDIO_INPUT = "audio_input"
|
||||||
ATTR_AUDIO_OUTPUT = 'audio_output'
|
ATTR_AUDIO_OUTPUT = "audio_output"
|
||||||
ATTR_INPUT = 'input'
|
ATTR_INPUT = "input"
|
||||||
ATTR_OUTPUT = 'output'
|
ATTR_OUTPUT = "output"
|
||||||
ATTR_DISK = 'disk'
|
ATTR_DISK = "disk"
|
||||||
ATTR_SERIAL = 'serial'
|
ATTR_SERIAL = "serial"
|
||||||
ATTR_SECURITY = 'security'
|
ATTR_SECURITY = "security"
|
||||||
ATTR_BUILD_FROM = 'build_from'
|
ATTR_BUILD_FROM = "build_from"
|
||||||
ATTR_SQUASH = 'squash'
|
ATTR_SQUASH = "squash"
|
||||||
ATTR_GPIO = 'gpio'
|
ATTR_GPIO = "gpio"
|
||||||
ATTR_LEGACY = 'legacy'
|
ATTR_LEGACY = "legacy"
|
||||||
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list"
|
||||||
ATTR_CPU_PERCENT = 'cpu_percent'
|
ATTR_CPU_PERCENT = "cpu_percent"
|
||||||
ATTR_NETWORK_RX = 'network_rx'
|
ATTR_NETWORK_RX = "network_rx"
|
||||||
ATTR_NETWORK_TX = 'network_tx'
|
ATTR_NETWORK_TX = "network_tx"
|
||||||
ATTR_MEMORY_LIMIT = 'memory_limit'
|
ATTR_MEMORY_LIMIT = "memory_limit"
|
||||||
ATTR_MEMORY_USAGE = 'memory_usage'
|
ATTR_MEMORY_USAGE = "memory_usage"
|
||||||
ATTR_BLK_READ = 'blk_read'
|
ATTR_BLK_READ = "blk_read"
|
||||||
ATTR_BLK_WRITE = 'blk_write'
|
ATTR_BLK_WRITE = "blk_write"
|
||||||
ATTR_PROVIDER = 'provider'
|
ATTR_ADDON = "addon"
|
||||||
ATTR_AVAILABLE = 'available'
|
ATTR_AVAILABLE = "available"
|
||||||
ATTR_HOST = 'host'
|
ATTR_HOST = "host"
|
||||||
ATTR_USERNAME = 'username'
|
ATTR_USERNAME = "username"
|
||||||
ATTR_PROTOCOL = 'protocol'
|
ATTR_PROTOCOL = "protocol"
|
||||||
ATTR_DISCOVERY = 'discovery'
|
ATTR_DISCOVERY = "discovery"
|
||||||
ATTR_PLATFORM = 'platform'
|
ATTR_CONFIG = "config"
|
||||||
ATTR_COMPONENT = 'component'
|
ATTR_SERVICES = "services"
|
||||||
ATTR_CONFIG = 'config'
|
ATTR_SERVICE = "service"
|
||||||
ATTR_DISCOVERY_ID = 'discovery_id'
|
ATTR_DISCOVERY = "discovery"
|
||||||
ATTR_SERVICES = 'services'
|
ATTR_PROTECTED = "protected"
|
||||||
ATTR_DISCOVERY = 'discovery'
|
ATTR_CRYPTO = "crypto"
|
||||||
ATTR_PROTECTED = 'protected'
|
ATTR_BRANCH = "branch"
|
||||||
ATTR_CRYPTO = 'crypto'
|
ATTR_KERNEL = "kernel"
|
||||||
ATTR_BRANCH = 'branch'
|
ATTR_APPARMOR = "apparmor"
|
||||||
ATTR_KERNEL = 'kernel'
|
ATTR_DEVICETREE = "devicetree"
|
||||||
ATTR_APPARMOR = 'apparmor'
|
ATTR_CPE = "cpe"
|
||||||
ATTR_DEVICETREE = 'devicetree'
|
ATTR_BOARD = "board"
|
||||||
ATTR_CPE = 'cpe'
|
ATTR_HASSOS = "hassos"
|
||||||
ATTR_BOARD = 'board'
|
ATTR_HASSOS_CLI = "hassos_cli"
|
||||||
ATTR_HASSOS = 'hassos'
|
ATTR_VERSION_CLI = "version_cli"
|
||||||
ATTR_HASSOS_CLI = 'hassos_cli'
|
ATTR_VERSION_CLI_LATEST = "version_cli_latest"
|
||||||
ATTR_VERSION_CLI = 'version_cli'
|
ATTR_REFRESH_TOKEN = "refresh_token"
|
||||||
ATTR_VERSION_CLI_LATEST = 'version_cli_latest'
|
ATTR_ACCESS_TOKEN = "access_token"
|
||||||
ATTR_REFRESH_TOKEN = 'refresh_token'
|
ATTR_DOCKER_API = "docker_api"
|
||||||
ATTR_DOCKER_API = 'docker_api'
|
ATTR_FULL_ACCESS = "full_access"
|
||||||
ATTR_FULL_ACCESS = 'full_access'
|
ATTR_PROTECTED = "protected"
|
||||||
ATTR_PROTECTED = 'protected'
|
ATTR_RATING = "rating"
|
||||||
ATTR_RATING = 'rating'
|
ATTR_HASSIO_ROLE = "hassio_role"
|
||||||
|
ATTR_SUPERVISOR = "supervisor"
|
||||||
|
ATTR_AUTH_API = "auth_api"
|
||||||
|
ATTR_KERNEL_MODULES = "kernel_modules"
|
||||||
|
ATTR_SUPPORTED_ARCH = "supported_arch"
|
||||||
|
|
||||||
SERVICE_MQTT = 'mqtt'
|
SERVICE_MQTT = "mqtt"
|
||||||
|
PROVIDE_SERVICE = "provide"
|
||||||
|
NEED_SERVICE = "need"
|
||||||
|
WANT_SERVICE = "want"
|
||||||
|
|
||||||
STARTUP_INITIALIZE = 'initialize'
|
STARTUP_INITIALIZE = "initialize"
|
||||||
STARTUP_SYSTEM = 'system'
|
STARTUP_SYSTEM = "system"
|
||||||
STARTUP_SERVICES = 'services'
|
STARTUP_SERVICES = "services"
|
||||||
STARTUP_APPLICATION = 'application'
|
STARTUP_APPLICATION = "application"
|
||||||
STARTUP_ONCE = 'once'
|
STARTUP_ONCE = "once"
|
||||||
|
|
||||||
BOOT_AUTO = 'auto'
|
STARTUP_ALL = [
|
||||||
BOOT_MANUAL = 'manual'
|
STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES,
|
||||||
|
STARTUP_APPLICATION
|
||||||
|
]
|
||||||
|
|
||||||
STATE_STARTED = 'started'
|
BOOT_AUTO = "auto"
|
||||||
STATE_STOPPED = 'stopped'
|
BOOT_MANUAL = "manual"
|
||||||
STATE_NONE = 'none'
|
|
||||||
|
|
||||||
MAP_CONFIG = 'config'
|
STATE_STARTED = "started"
|
||||||
MAP_SSL = 'ssl'
|
STATE_STOPPED = "stopped"
|
||||||
MAP_ADDONS = 'addons'
|
STATE_NONE = "none"
|
||||||
MAP_BACKUP = 'backup'
|
|
||||||
MAP_SHARE = 'share'
|
|
||||||
|
|
||||||
ARCH_ARMHF = 'armhf'
|
MAP_CONFIG = "config"
|
||||||
ARCH_AARCH64 = 'aarch64'
|
MAP_SSL = "ssl"
|
||||||
ARCH_AMD64 = 'amd64'
|
MAP_ADDONS = "addons"
|
||||||
ARCH_I386 = 'i386'
|
MAP_BACKUP = "backup"
|
||||||
|
MAP_SHARE = "share"
|
||||||
|
|
||||||
CHANNEL_STABLE = 'stable'
|
ARCH_ARMHF = "armhf"
|
||||||
CHANNEL_BETA = 'beta'
|
ARCH_ARMV7 = "armv7"
|
||||||
CHANNEL_DEV = 'dev'
|
ARCH_AARCH64 = "aarch64"
|
||||||
|
ARCH_AMD64 = "amd64"
|
||||||
|
ARCH_I386 = "i386"
|
||||||
|
|
||||||
REPOSITORY_CORE = 'core'
|
ARCH_ALL = [ARCH_ARMHF, ARCH_ARMV7, ARCH_AARCH64, ARCH_AMD64, ARCH_I386]
|
||||||
REPOSITORY_LOCAL = 'local'
|
|
||||||
|
|
||||||
FOLDER_HOMEASSISTANT = 'homeassistant'
|
CHANNEL_STABLE = "stable"
|
||||||
FOLDER_SHARE = 'share'
|
CHANNEL_BETA = "beta"
|
||||||
FOLDER_ADDONS = 'addons/local'
|
CHANNEL_DEV = "dev"
|
||||||
FOLDER_SSL = 'ssl'
|
|
||||||
|
|
||||||
SNAPSHOT_FULL = 'full'
|
REPOSITORY_CORE = "core"
|
||||||
SNAPSHOT_PARTIAL = 'partial'
|
REPOSITORY_LOCAL = "local"
|
||||||
|
|
||||||
CRYPTO_AES128 = 'aes128'
|
FOLDER_HOMEASSISTANT = "homeassistant"
|
||||||
|
FOLDER_SHARE = "share"
|
||||||
|
FOLDER_ADDONS = "addons/local"
|
||||||
|
FOLDER_SSL = "ssl"
|
||||||
|
|
||||||
SECURITY_PROFILE = 'profile'
|
SNAPSHOT_FULL = "full"
|
||||||
SECURITY_DEFAULT = 'default'
|
SNAPSHOT_PARTIAL = "partial"
|
||||||
SECURITY_DISABLE = 'disable'
|
|
||||||
|
|
||||||
PRIVILEGED_NET_ADMIN = 'NET_ADMIN'
|
CRYPTO_AES128 = "aes128"
|
||||||
PRIVILEGED_SYS_ADMIN = 'SYS_ADMIN'
|
|
||||||
PRIVILEGED_SYS_RAWIO = 'SYS_RAWIO'
|
|
||||||
PRIVILEGED_IPC_LOCK = 'IPC_LOCK'
|
|
||||||
PRIVILEGED_SYS_TIME = 'SYS_TIME'
|
|
||||||
PRIVILEGED_SYS_NICE = 'SYS_NICE'
|
|
||||||
PRIVILEGED_SYS_RESOURCE = 'SYS_RESOURCE'
|
|
||||||
|
|
||||||
FEATURES_SHUTDOWN = 'shutdown'
|
SECURITY_PROFILE = "profile"
|
||||||
FEATURES_REBOOT = 'reboot'
|
SECURITY_DEFAULT = "default"
|
||||||
FEATURES_HASSOS = 'hassos'
|
SECURITY_DISABLE = "disable"
|
||||||
FEATURES_HOSTNAME = 'hostname'
|
|
||||||
FEATURES_SERVICES = 'services'
|
PRIVILEGED_NET_ADMIN = "NET_ADMIN"
|
||||||
|
PRIVILEGED_SYS_ADMIN = "SYS_ADMIN"
|
||||||
|
PRIVILEGED_SYS_RAWIO = "SYS_RAWIO"
|
||||||
|
PRIVILEGED_IPC_LOCK = "IPC_LOCK"
|
||||||
|
PRIVILEGED_SYS_TIME = "SYS_TIME"
|
||||||
|
PRIVILEGED_SYS_NICE = "SYS_NICE"
|
||||||
|
PRIVILEGED_SYS_MODULE = "SYS_MODULE"
|
||||||
|
PRIVILEGED_SYS_RESOURCE = "SYS_RESOURCE"
|
||||||
|
PRIVILEGED_SYS_PTRACE = "SYS_PTRACE"
|
||||||
|
PRIVILEGED_DAC_READ_SEARCH = "DAC_READ_SEARCH"
|
||||||
|
|
||||||
|
PRIVILEGED_ALL = [
|
||||||
|
PRIVILEGED_NET_ADMIN,
|
||||||
|
PRIVILEGED_SYS_ADMIN,
|
||||||
|
PRIVILEGED_SYS_RAWIO,
|
||||||
|
PRIVILEGED_IPC_LOCK,
|
||||||
|
PRIVILEGED_SYS_TIME,
|
||||||
|
PRIVILEGED_SYS_NICE,
|
||||||
|
PRIVILEGED_SYS_RESOURCE,
|
||||||
|
PRIVILEGED_SYS_PTRACE,
|
||||||
|
PRIVILEGED_SYS_MODULE,
|
||||||
|
PRIVILEGED_DAC_READ_SEARCH,
|
||||||
|
]
|
||||||
|
|
||||||
|
FEATURES_SHUTDOWN = "shutdown"
|
||||||
|
FEATURES_REBOOT = "reboot"
|
||||||
|
FEATURES_HASSOS = "hassos"
|
||||||
|
FEATURES_HOSTNAME = "hostname"
|
||||||
|
FEATURES_SERVICES = "services"
|
||||||
|
|
||||||
|
ROLE_DEFAULT = "default"
|
||||||
|
ROLE_HOMEASSISTANT = "homeassistant"
|
||||||
|
ROLE_BACKUP = "backup"
|
||||||
|
ROLE_MANAGER = "manager"
|
||||||
|
ROLE_ADMIN = "admin"
|
||||||
|
|
||||||
|
ROLE_ALL = [
|
||||||
|
ROLE_DEFAULT,
|
||||||
|
ROLE_HOMEASSISTANT,
|
||||||
|
ROLE_BACKUP,
|
||||||
|
ROLE_MANAGER,
|
||||||
|
ROLE_ADMIN,
|
||||||
|
]
|
||||||
|
|
||||||
|
CHAN_ID = "chan_id"
|
||||||
|
CHAN_TYPE = "chan_type"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Main file for HassIO."""
|
"""Main file for Hass.io."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
@@ -6,18 +6,18 @@ import logging
|
|||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .const import (
|
from .const import (STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION,
|
||||||
STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE)
|
STARTUP_INITIALIZE)
|
||||||
from .exceptions import HassioError, HomeAssistantError
|
from .exceptions import HassioError, HomeAssistantError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HassIO(CoreSysAttributes):
|
class HassIO(CoreSysAttributes):
|
||||||
"""Main object of hassio."""
|
"""Main object of Hass.io."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize hassio object."""
|
"""Initialize Hass.io object."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
|
|
||||||
async def setup(self):
|
async def setup(self):
|
||||||
@@ -31,12 +31,15 @@ class HassIO(CoreSysAttributes):
|
|||||||
# Load Host
|
# Load Host
|
||||||
await self.sys_host.load()
|
await self.sys_host.load()
|
||||||
|
|
||||||
# Load HassOS
|
|
||||||
await self.sys_hassos.load()
|
|
||||||
|
|
||||||
# Load Home Assistant
|
# Load Home Assistant
|
||||||
await self.sys_homeassistant.load()
|
await self.sys_homeassistant.load()
|
||||||
|
|
||||||
|
# Load CPU/Arch
|
||||||
|
await self.sys_arch.load()
|
||||||
|
|
||||||
|
# Load HassOS
|
||||||
|
await self.sys_hassos.load()
|
||||||
|
|
||||||
# Load Add-ons
|
# Load Add-ons
|
||||||
await self.sys_addons.load()
|
await self.sys_addons.load()
|
||||||
|
|
||||||
@@ -52,18 +55,20 @@ class HassIO(CoreSysAttributes):
|
|||||||
# load services
|
# load services
|
||||||
await self.sys_services.load()
|
await self.sys_services.load()
|
||||||
|
|
||||||
|
# Load discovery
|
||||||
|
await self.sys_discovery.load()
|
||||||
|
|
||||||
# start dns forwarding
|
# start dns forwarding
|
||||||
self.sys_create_task(self.sys_dns.start())
|
self.sys_create_task(self.sys_dns.start())
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Start HassIO orchestration."""
|
"""Start Hass.io orchestration."""
|
||||||
# on release channel, try update itself
|
# on release channel, try update itself
|
||||||
# on dev mode, only read new versions
|
if self.sys_supervisor.need_update:
|
||||||
if not self.sys_dev and self.sys_supervisor.need_update:
|
if self.sys_dev:
|
||||||
if await self.sys_supervisor.update():
|
_LOGGER.warning("Ignore Hass.io updates on dev!")
|
||||||
|
elif await self.sys_supervisor.update():
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
_LOGGER.info("Ignore Hass.io auto updates on dev channel")
|
|
||||||
|
|
||||||
# start api
|
# start api
|
||||||
await self.sys_api.start()
|
await self.sys_api.start()
|
||||||
|
@@ -1,288 +1,455 @@
|
|||||||
"""Handle core shared data."""
|
"""Handle core shared data."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from .const import CHANNEL_DEV
|
|
||||||
from .config import CoreConfig
|
from .config import CoreConfig
|
||||||
|
from .const import CHANNEL_DEV
|
||||||
from .docker import DockerAPI
|
from .docker import DockerAPI
|
||||||
from .misc.dns import DNSForward
|
from .misc.dns import DNSForward
|
||||||
from .misc.hardware import Hardware
|
from .misc.hardware import Hardware
|
||||||
from .misc.scheduler import Scheduler
|
from .misc.scheduler import Scheduler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .addons import AddonManager
|
||||||
|
from .api import RestAPI
|
||||||
|
from .arch import CpuArch
|
||||||
|
from .auth import Auth
|
||||||
|
from .core import HassIO
|
||||||
|
from .dbus import DBusManager
|
||||||
|
from .discovery import Discovery
|
||||||
|
from .hassos import HassOS
|
||||||
|
from .homeassistant import HomeAssistant
|
||||||
|
from .host import HostManager
|
||||||
|
from .services import ServiceManager
|
||||||
|
from .snapshots import SnapshotManager
|
||||||
|
from .supervisor import Supervisor
|
||||||
|
from .tasks import Tasks
|
||||||
|
from .updater import Updater
|
||||||
|
|
||||||
|
|
||||||
class CoreSys:
|
class CoreSys:
|
||||||
"""Class that handle all shared data."""
|
"""Class that handle all shared data."""
|
||||||
|
|
||||||
def __init__(self, loop):
|
def __init__(self):
|
||||||
"""Initialize coresys."""
|
"""Initialize coresys."""
|
||||||
# Static attributes
|
# Static attributes
|
||||||
self.exit_code = 0
|
self.machine_id: str = None
|
||||||
self.machine_id = None
|
|
||||||
|
|
||||||
# External objects
|
# External objects
|
||||||
self._loop = loop
|
self._loop: asyncio.BaseEventLoop = asyncio.get_running_loop()
|
||||||
self._websession = aiohttp.ClientSession(loop=loop)
|
self._websession: aiohttp.ClientSession = aiohttp.ClientSession()
|
||||||
self._websession_ssl = aiohttp.ClientSession(
|
self._websession_ssl: aiohttp.ClientSession = aiohttp.ClientSession(
|
||||||
connector=aiohttp.TCPConnector(verify_ssl=False), loop=loop)
|
connector=aiohttp.TCPConnector(ssl=False))
|
||||||
|
|
||||||
# Global objects
|
# Global objects
|
||||||
self._config = CoreConfig()
|
self._config: CoreConfig = CoreConfig()
|
||||||
self._hardware = Hardware()
|
self._hardware: Hardware = Hardware()
|
||||||
self._docker = DockerAPI()
|
self._docker: DockerAPI = DockerAPI()
|
||||||
self._scheduler = Scheduler(loop=loop)
|
self._scheduler: Scheduler = Scheduler()
|
||||||
self._dns = DNSForward(loop=loop)
|
self._dns: DNSForward = DNSForward()
|
||||||
|
|
||||||
# Internal objects pointers
|
# Internal objects pointers
|
||||||
self._core = None
|
self._core: HassIO = None
|
||||||
self._homeassistant = None
|
self._arch: CpuArch = None
|
||||||
self._supervisor = None
|
self._auth: Auth = None
|
||||||
self._addons = None
|
self._homeassistant: HomeAssistant = None
|
||||||
self._api = None
|
self._supervisor: Supervisor = None
|
||||||
self._updater = None
|
self._addons: AddonManager = None
|
||||||
self._snapshots = None
|
self._api: RestAPI = None
|
||||||
self._tasks = None
|
self._updater: Updater = None
|
||||||
self._host = None
|
self._snapshots: SnapshotManager = None
|
||||||
self._dbus = None
|
self._tasks: Tasks = None
|
||||||
self._hassos = None
|
self._host: HostManager = None
|
||||||
self._services = None
|
self._dbus: DBusManager = None
|
||||||
self._discovery = None
|
self._hassos: HassOS = None
|
||||||
|
self._services: ServiceManager = None
|
||||||
|
self._discovery: Discovery = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self):
|
def machine(self) -> str:
|
||||||
"""Return running arch of hass.io system."""
|
"""Return running machine type of the Hass.io system."""
|
||||||
if self._supervisor:
|
|
||||||
return self._supervisor.arch
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def machine(self):
|
|
||||||
"""Return running machine type of hass.io system."""
|
|
||||||
if self._homeassistant:
|
if self._homeassistant:
|
||||||
return self._homeassistant.machine
|
return self._homeassistant.machine
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dev(self):
|
def dev(self) -> str:
|
||||||
"""Return True if we run dev modus."""
|
"""Return True if we run dev mode."""
|
||||||
return self._updater.channel == CHANNEL_DEV
|
return self._updater.channel == CHANNEL_DEV
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timezone(self):
|
def timezone(self) -> str:
|
||||||
"""Return timezone."""
|
"""Return timezone."""
|
||||||
return self._config.timezone
|
return self._config.timezone
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loop(self):
|
def loop(self) -> asyncio.BaseEventLoop:
|
||||||
"""Return loop object."""
|
"""Return loop object."""
|
||||||
return self._loop
|
return self._loop
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def websession(self):
|
def websession(self) -> aiohttp.ClientSession:
|
||||||
"""Return websession object."""
|
"""Return websession object."""
|
||||||
return self._websession
|
return self._websession
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def websession_ssl(self):
|
def websession_ssl(self) -> aiohttp.ClientSession:
|
||||||
"""Return websession object with disabled SSL."""
|
"""Return websession object with disabled SSL."""
|
||||||
return self._websession_ssl
|
return self._websession_ssl
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self) -> CoreConfig:
|
||||||
"""Return CoreConfig object."""
|
"""Return CoreConfig object."""
|
||||||
return self._config
|
return self._config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hardware(self):
|
def hardware(self) -> Hardware:
|
||||||
"""Return Hardware object."""
|
"""Return Hardware object."""
|
||||||
return self._hardware
|
return self._hardware
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def docker(self):
|
def docker(self) -> DockerAPI:
|
||||||
"""Return DockerAPI object."""
|
"""Return DockerAPI object."""
|
||||||
return self._docker
|
return self._docker
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scheduler(self):
|
def scheduler(self) -> Scheduler:
|
||||||
"""Return Scheduler object."""
|
"""Return Scheduler object."""
|
||||||
return self._scheduler
|
return self._scheduler
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dns(self):
|
def dns(self) -> DNSForward:
|
||||||
"""Return DNSForward object."""
|
"""Return DNSForward object."""
|
||||||
return self._dns
|
return self._dns
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def core(self):
|
def core(self) -> HassIO:
|
||||||
"""Return HassIO object."""
|
"""Return HassIO object."""
|
||||||
return self._core
|
return self._core
|
||||||
|
|
||||||
@core.setter
|
@core.setter
|
||||||
def core(self, value):
|
def core(self, value: HassIO):
|
||||||
"""Set a HassIO object."""
|
"""Set a Hass.io object."""
|
||||||
if self._core:
|
if self._core:
|
||||||
raise RuntimeError("HassIO already set!")
|
raise RuntimeError("Hass.io already set!")
|
||||||
self._core = value
|
self._core = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def homeassistant(self):
|
def arch(self) -> CpuArch:
|
||||||
|
"""Return CpuArch object."""
|
||||||
|
return self._arch
|
||||||
|
|
||||||
|
@arch.setter
|
||||||
|
def arch(self, value: CpuArch):
|
||||||
|
"""Set a CpuArch object."""
|
||||||
|
if self._arch:
|
||||||
|
raise RuntimeError("CpuArch already set!")
|
||||||
|
self._arch = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth(self) -> Auth:
|
||||||
|
"""Return Auth object."""
|
||||||
|
return self._auth
|
||||||
|
|
||||||
|
@auth.setter
|
||||||
|
def auth(self, value: Auth):
|
||||||
|
"""Set a Auth object."""
|
||||||
|
if self._auth:
|
||||||
|
raise RuntimeError("Auth already set!")
|
||||||
|
self._auth = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def homeassistant(self) -> HomeAssistant:
|
||||||
"""Return Home Assistant object."""
|
"""Return Home Assistant object."""
|
||||||
return self._homeassistant
|
return self._homeassistant
|
||||||
|
|
||||||
@homeassistant.setter
|
@homeassistant.setter
|
||||||
def homeassistant(self, value):
|
def homeassistant(self, value: HomeAssistant):
|
||||||
"""Set a HomeAssistant object."""
|
"""Set a HomeAssistant object."""
|
||||||
if self._homeassistant:
|
if self._homeassistant:
|
||||||
raise RuntimeError("Home Assistant already set!")
|
raise RuntimeError("Home Assistant already set!")
|
||||||
self._homeassistant = value
|
self._homeassistant = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supervisor(self):
|
def supervisor(self) -> Supervisor:
|
||||||
"""Return Supervisor object."""
|
"""Return Supervisor object."""
|
||||||
return self._supervisor
|
return self._supervisor
|
||||||
|
|
||||||
@supervisor.setter
|
@supervisor.setter
|
||||||
def supervisor(self, value):
|
def supervisor(self, value: Supervisor):
|
||||||
"""Set a Supervisor object."""
|
"""Set a Supervisor object."""
|
||||||
if self._supervisor:
|
if self._supervisor:
|
||||||
raise RuntimeError("Supervisor already set!")
|
raise RuntimeError("Supervisor already set!")
|
||||||
self._supervisor = value
|
self._supervisor = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api(self):
|
def api(self) -> RestAPI:
|
||||||
"""Return API object."""
|
"""Return API object."""
|
||||||
return self._api
|
return self._api
|
||||||
|
|
||||||
@api.setter
|
@api.setter
|
||||||
def api(self, value):
|
def api(self, value: RestAPI):
|
||||||
"""Set an API object."""
|
"""Set an API object."""
|
||||||
if self._api:
|
if self._api:
|
||||||
raise RuntimeError("API already set!")
|
raise RuntimeError("API already set!")
|
||||||
self._api = value
|
self._api = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def updater(self):
|
def updater(self) -> Updater:
|
||||||
"""Return Updater object."""
|
"""Return Updater object."""
|
||||||
return self._updater
|
return self._updater
|
||||||
|
|
||||||
@updater.setter
|
@updater.setter
|
||||||
def updater(self, value):
|
def updater(self, value: Updater):
|
||||||
"""Set a Updater object."""
|
"""Set a Updater object."""
|
||||||
if self._updater:
|
if self._updater:
|
||||||
raise RuntimeError("Updater already set!")
|
raise RuntimeError("Updater already set!")
|
||||||
self._updater = value
|
self._updater = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addons(self):
|
def addons(self) -> AddonManager:
|
||||||
"""Return AddonManager object."""
|
"""Return AddonManager object."""
|
||||||
return self._addons
|
return self._addons
|
||||||
|
|
||||||
@addons.setter
|
@addons.setter
|
||||||
def addons(self, value):
|
def addons(self, value: AddonManager):
|
||||||
"""Set a AddonManager object."""
|
"""Set a AddonManager object."""
|
||||||
if self._addons:
|
if self._addons:
|
||||||
raise RuntimeError("AddonManager already set!")
|
raise RuntimeError("AddonManager already set!")
|
||||||
self._addons = value
|
self._addons = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def snapshots(self):
|
def snapshots(self) -> SnapshotManager:
|
||||||
"""Return SnapshotManager object."""
|
"""Return SnapshotManager object."""
|
||||||
return self._snapshots
|
return self._snapshots
|
||||||
|
|
||||||
@snapshots.setter
|
@snapshots.setter
|
||||||
def snapshots(self, value):
|
def snapshots(self, value: SnapshotManager):
|
||||||
"""Set a SnapshotManager object."""
|
"""Set a SnapshotManager object."""
|
||||||
if self._snapshots:
|
if self._snapshots:
|
||||||
raise RuntimeError("SnapshotsManager already set!")
|
raise RuntimeError("SnapshotsManager already set!")
|
||||||
self._snapshots = value
|
self._snapshots = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tasks(self):
|
def tasks(self) -> Tasks:
|
||||||
"""Return Tasks object."""
|
"""Return Tasks object."""
|
||||||
return self._tasks
|
return self._tasks
|
||||||
|
|
||||||
@tasks.setter
|
@tasks.setter
|
||||||
def tasks(self, value):
|
def tasks(self, value: Tasks):
|
||||||
"""Set a Tasks object."""
|
"""Set a Tasks object."""
|
||||||
if self._tasks:
|
if self._tasks:
|
||||||
raise RuntimeError("Tasks already set!")
|
raise RuntimeError("Tasks already set!")
|
||||||
self._tasks = value
|
self._tasks = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def services(self):
|
def services(self) -> ServiceManager:
|
||||||
"""Return ServiceManager object."""
|
"""Return ServiceManager object."""
|
||||||
return self._services
|
return self._services
|
||||||
|
|
||||||
@services.setter
|
@services.setter
|
||||||
def services(self, value):
|
def services(self, value: ServiceManager):
|
||||||
"""Set a ServiceManager object."""
|
"""Set a ServiceManager object."""
|
||||||
if self._services:
|
if self._services:
|
||||||
raise RuntimeError("Services already set!")
|
raise RuntimeError("Services already set!")
|
||||||
self._services = value
|
self._services = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def discovery(self):
|
def discovery(self) -> Discovery:
|
||||||
"""Return ServiceManager object."""
|
"""Return ServiceManager object."""
|
||||||
return self._discovery
|
return self._discovery
|
||||||
|
|
||||||
@discovery.setter
|
@discovery.setter
|
||||||
def discovery(self, value):
|
def discovery(self, value: Discovery):
|
||||||
"""Set a Discovery object."""
|
"""Set a Discovery object."""
|
||||||
if self._discovery:
|
if self._discovery:
|
||||||
raise RuntimeError("Discovery already set!")
|
raise RuntimeError("Discovery already set!")
|
||||||
self._discovery = value
|
self._discovery = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dbus(self):
|
def dbus(self) -> DBusManager:
|
||||||
"""Return DBusManager object."""
|
"""Return DBusManager object."""
|
||||||
return self._dbus
|
return self._dbus
|
||||||
|
|
||||||
@dbus.setter
|
@dbus.setter
|
||||||
def dbus(self, value):
|
def dbus(self, value: DBusManager):
|
||||||
"""Set a DBusManager object."""
|
"""Set a DBusManager object."""
|
||||||
if self._dbus:
|
if self._dbus:
|
||||||
raise RuntimeError("DBusManager already set!")
|
raise RuntimeError("DBusManager already set!")
|
||||||
self._dbus = value
|
self._dbus = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def host(self):
|
def host(self) -> HostManager:
|
||||||
"""Return HostManager object."""
|
"""Return HostManager object."""
|
||||||
return self._host
|
return self._host
|
||||||
|
|
||||||
@host.setter
|
@host.setter
|
||||||
def host(self, value):
|
def host(self, value: HostManager):
|
||||||
"""Set a HostManager object."""
|
"""Set a HostManager object."""
|
||||||
if self._host:
|
if self._host:
|
||||||
raise RuntimeError("HostManager already set!")
|
raise RuntimeError("HostManager already set!")
|
||||||
self._host = value
|
self._host = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassos(self):
|
def hassos(self) -> HassOS:
|
||||||
"""Return HassOS object."""
|
"""Return HassOS object."""
|
||||||
return self._hassos
|
return self._hassos
|
||||||
|
|
||||||
@hassos.setter
|
@hassos.setter
|
||||||
def hassos(self, value):
|
def hassos(self, value: HassOS):
|
||||||
"""Set a HassOS object."""
|
"""Set a HassOS object."""
|
||||||
if self._hassos:
|
if self._hassos:
|
||||||
raise RuntimeError("HassOS already set!")
|
raise RuntimeError("HassOS already set!")
|
||||||
self._hassos = value
|
self._hassos = value
|
||||||
|
|
||||||
def run_in_executor(self, funct, *args):
|
|
||||||
"""Wrapper for executor pool."""
|
|
||||||
return self._loop.run_in_executor(None, funct, *args)
|
|
||||||
|
|
||||||
def create_task(self, coroutine):
|
|
||||||
"""Wrapper for async task."""
|
|
||||||
return self._loop.create_task(coroutine)
|
|
||||||
|
|
||||||
|
|
||||||
class CoreSysAttributes:
|
class CoreSysAttributes:
|
||||||
"""Inheret basic CoreSysAttributes."""
|
"""Inheret basic CoreSysAttributes."""
|
||||||
|
|
||||||
coresys = None
|
coresys = None
|
||||||
|
|
||||||
def __getattr__(self, name):
|
@property
|
||||||
"""Mapping to coresys."""
|
def sys_machine(self) -> str:
|
||||||
if name.startswith("sys_") and hasattr(self.coresys, name[4:]):
|
"""Return running machine type of the Hass.io system."""
|
||||||
return getattr(self.coresys, name[4:])
|
return self.coresys.machine
|
||||||
raise AttributeError(f"Can't resolve {name} on {self}")
|
|
||||||
|
@property
|
||||||
|
def sys_dev(self) -> str:
|
||||||
|
"""Return True if we run dev mode."""
|
||||||
|
return self.coresys.dev
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_timezone(self) -> str:
|
||||||
|
"""Return timezone."""
|
||||||
|
return self.coresys.timezone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_machine_id(self) -> str:
|
||||||
|
"""Return timezone."""
|
||||||
|
return self.coresys.machine_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_loop(self) -> asyncio.BaseEventLoop:
|
||||||
|
"""Return loop object."""
|
||||||
|
return self.coresys.loop
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_websession(self) -> aiohttp.ClientSession:
|
||||||
|
"""Return websession object."""
|
||||||
|
return self.coresys.websession
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_websession_ssl(self) -> aiohttp.ClientSession:
|
||||||
|
"""Return websession object with disabled SSL."""
|
||||||
|
return self.coresys.websession_ssl
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_config(self) -> CoreConfig:
|
||||||
|
"""Return CoreConfig object."""
|
||||||
|
return self.coresys.config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_hardware(self) -> Hardware:
|
||||||
|
"""Return Hardware object."""
|
||||||
|
return self.coresys.hardware
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_docker(self) -> DockerAPI:
|
||||||
|
"""Return DockerAPI object."""
|
||||||
|
return self.coresys.docker
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_scheduler(self) -> Scheduler:
|
||||||
|
"""Return Scheduler object."""
|
||||||
|
return self.coresys.scheduler
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_dns(self) -> DNSForward:
|
||||||
|
"""Return DNSForward object."""
|
||||||
|
return self.coresys.dns
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_core(self) -> HassIO:
|
||||||
|
"""Return HassIO object."""
|
||||||
|
return self.coresys.core
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_arch(self) -> CpuArch:
|
||||||
|
"""Return CpuArch object."""
|
||||||
|
return self.coresys.arch
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_auth(self) -> Auth:
|
||||||
|
"""Return Auth object."""
|
||||||
|
return self.coresys.auth
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_homeassistant(self) -> HomeAssistant:
|
||||||
|
"""Return Home Assistant object."""
|
||||||
|
return self.coresys.homeassistant
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_supervisor(self) -> Supervisor:
|
||||||
|
"""Return Supervisor object."""
|
||||||
|
return self.coresys.supervisor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_api(self) -> RestAPI:
|
||||||
|
"""Return API object."""
|
||||||
|
return self.coresys.api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_updater(self) -> Updater:
|
||||||
|
"""Return Updater object."""
|
||||||
|
return self.coresys.updater
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_addons(self) -> AddonManager:
|
||||||
|
"""Return AddonManager object."""
|
||||||
|
return self.coresys.addons
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_snapshots(self) -> SnapshotManager:
|
||||||
|
"""Return SnapshotManager object."""
|
||||||
|
return self.coresys.snapshots
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_tasks(self) -> Tasks:
|
||||||
|
"""Return Tasks object."""
|
||||||
|
return self.coresys.tasks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_services(self) -> ServiceManager:
|
||||||
|
"""Return ServiceManager object."""
|
||||||
|
return self.coresys.services
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_discovery(self) -> Discovery:
|
||||||
|
"""Return ServiceManager object."""
|
||||||
|
return self.coresys.discovery
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_dbus(self) -> DBusManager:
|
||||||
|
"""Return DBusManager object."""
|
||||||
|
return self.coresys.dbus
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_host(self) -> HostManager:
|
||||||
|
"""Return HostManager object."""
|
||||||
|
return self.coresys.host
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sys_hassos(self) -> HassOS:
|
||||||
|
"""Return HassOS object."""
|
||||||
|
return self.coresys.hassos
|
||||||
|
|
||||||
|
def sys_run_in_executor(self, funct, *args) -> asyncio.Future:
|
||||||
|
"""Wrapper for executor pool."""
|
||||||
|
return self.sys_loop.run_in_executor(None, funct, *args)
|
||||||
|
|
||||||
|
def sys_create_task(self, coroutine) -> asyncio.Task:
|
||||||
|
"""Wrapper for async task."""
|
||||||
|
return self.sys_loop.create_task(coroutine)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""DBus interface objects."""
|
"""D-Bus interface objects."""
|
||||||
|
|
||||||
from .systemd import Systemd
|
from .systemd import Systemd
|
||||||
from .hostname import Hostname
|
from .hostname import Hostname
|
||||||
@@ -7,10 +7,10 @@ from ..coresys import CoreSysAttributes
|
|||||||
|
|
||||||
|
|
||||||
class DBusManager(CoreSysAttributes):
|
class DBusManager(CoreSysAttributes):
|
||||||
"""DBus Interface handler."""
|
"""A DBus Interface handler."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize DBus Interface."""
|
"""Initialize D-Bus interface."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
|
|
||||||
self._systemd = Systemd()
|
self._systemd = Systemd()
|
||||||
@@ -19,21 +19,21 @@ class DBusManager(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def systemd(self):
|
def systemd(self):
|
||||||
"""Return Systemd Interface."""
|
"""Return the systemd interface."""
|
||||||
return self._systemd
|
return self._systemd
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self):
|
def hostname(self):
|
||||||
"""Return hostname Interface."""
|
"""Return the hostname interface."""
|
||||||
return self._hostname
|
return self._hostname
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rauc(self):
|
def rauc(self):
|
||||||
"""Return rauc Interface."""
|
"""Return the rauc interface."""
|
||||||
return self._rauc
|
return self._rauc
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Connect interfaces to dbus."""
|
"""Connect interfaces to D-Bus."""
|
||||||
await self.systemd.connect()
|
await self.systemd.connect()
|
||||||
await self.hostname.connect()
|
await self.hostname.connect()
|
||||||
await self.rauc.connect()
|
await self.rauc.connect()
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""DBus interface for hostname."""
|
"""D-Bus interface for hostname."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .interface import DBusInterface
|
from .interface import DBusInterface
|
||||||
@@ -13,10 +13,10 @@ DBUS_OBJECT = '/org/freedesktop/hostname1'
|
|||||||
|
|
||||||
|
|
||||||
class Hostname(DBusInterface):
|
class Hostname(DBusInterface):
|
||||||
"""Handle DBus interface for hostname/system."""
|
"""Handle D-Bus interface for hostname/system."""
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect do bus."""
|
"""Connect to system's D-Bus."""
|
||||||
try:
|
try:
|
||||||
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
||||||
except DBusError:
|
except DBusError:
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
"""Interface class for dbus wrappers."""
|
"""Interface class for D-Bus wrappers."""
|
||||||
|
|
||||||
|
|
||||||
class DBusInterface:
|
class DBusInterface:
|
||||||
"""Handle DBus interface for hostname/system."""
|
"""Handle D-Bus interface for hostname/system."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize systemd."""
|
"""Initialize systemd."""
|
||||||
@@ -10,9 +10,9 @@ class DBusInterface:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
"""Return True, if they is connected to dbus."""
|
"""Return True, if they is connected to D-Bus."""
|
||||||
return self.dbus is not None
|
return self.dbus is not None
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect do bus."""
|
"""Connect to D-Bus."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""DBus interface for rauc."""
|
"""D-Bus interface for rauc."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .interface import DBusInterface
|
from .interface import DBusInterface
|
||||||
@@ -13,10 +13,10 @@ DBUS_OBJECT = '/'
|
|||||||
|
|
||||||
|
|
||||||
class Rauc(DBusInterface):
|
class Rauc(DBusInterface):
|
||||||
"""Handle DBus interface for rauc."""
|
"""Handle D-Bus interface for rauc."""
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect do bus."""
|
"""Connect to D-Bus."""
|
||||||
try:
|
try:
|
||||||
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
||||||
except DBusError:
|
except DBusError:
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Interface to Systemd over dbus."""
|
"""Interface to Systemd over D-Bus."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .interface import DBusInterface
|
from .interface import DBusInterface
|
||||||
@@ -16,7 +16,7 @@ class Systemd(DBusInterface):
|
|||||||
"""Systemd function handler."""
|
"""Systemd function handler."""
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect do bus."""
|
"""Connect to D-Bus."""
|
||||||
try:
|
try:
|
||||||
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
self.dbus = await DBus.connect(DBUS_NAME, DBUS_OBJECT)
|
||||||
except DBusError:
|
except DBusError:
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
"""Utils for dbus."""
|
"""Utils for D-Bus."""
|
||||||
|
|
||||||
from ..exceptions import DBusNotConnectedError
|
from ..exceptions import DBusNotConnectedError
|
||||||
|
|
||||||
|
|
||||||
def dbus_connected(method):
|
def dbus_connected(method):
|
||||||
"""Wrapper for check if dbus is connected."""
|
"""Wrapper for check if D-Bus is connected."""
|
||||||
def wrap_dbus(api, *args, **kwargs):
|
def wrap_dbus(api, *args, **kwargs):
|
||||||
"""Check if dbus is connected before call a method."""
|
"""Check if D-Bus is connected before call a method."""
|
||||||
if api.dbus is None:
|
if api.dbus is None:
|
||||||
raise DBusNotConnectedError()
|
raise DBusNotConnectedError()
|
||||||
return method(api, *args, **kwargs)
|
return method(api, *args, **kwargs)
|
||||||
|
122
hassio/discovery.py
Normal file
122
hassio/discovery.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
"""Handle discover message for Home Assistant."""
|
||||||
|
import logging
|
||||||
|
from contextlib import suppress
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
|
from .const import FILE_HASSIO_DISCOVERY, ATTR_CONFIG, ATTR_DISCOVERY
|
||||||
|
from .coresys import CoreSysAttributes
|
||||||
|
from .exceptions import DiscoveryError, HomeAssistantAPIError
|
||||||
|
from .validate import SCHEMA_DISCOVERY_CONFIG
|
||||||
|
from .utils.json import JsonConfig
|
||||||
|
from .services.validate import DISCOVERY_SERVICES
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CMD_NEW = 'post'
|
||||||
|
CMD_DEL = 'delete'
|
||||||
|
|
||||||
|
|
||||||
|
class Discovery(CoreSysAttributes, JsonConfig):
|
||||||
|
"""Home Assistant Discovery handler."""
|
||||||
|
|
||||||
|
def __init__(self, coresys):
|
||||||
|
"""Initialize discovery handler."""
|
||||||
|
super().__init__(FILE_HASSIO_DISCOVERY, SCHEMA_DISCOVERY_CONFIG)
|
||||||
|
self.coresys = coresys
|
||||||
|
self.message_obj = {}
|
||||||
|
|
||||||
|
async def load(self):
|
||||||
|
"""Load exists discovery message into storage."""
|
||||||
|
messages = {}
|
||||||
|
for message in self._data[ATTR_DISCOVERY]:
|
||||||
|
discovery = Message(**message)
|
||||||
|
messages[discovery.uuid] = discovery
|
||||||
|
|
||||||
|
_LOGGER.info("Load %d messages", len(messages))
|
||||||
|
self.message_obj = messages
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Write discovery message into data file."""
|
||||||
|
messages = []
|
||||||
|
for message in self.list_messages:
|
||||||
|
messages.append(attr.asdict(message))
|
||||||
|
|
||||||
|
self._data[ATTR_DISCOVERY].clear()
|
||||||
|
self._data[ATTR_DISCOVERY].extend(messages)
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
|
def get(self, uuid):
|
||||||
|
"""Return discovery message."""
|
||||||
|
return self.message_obj.get(uuid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_messages(self):
|
||||||
|
"""Return list of available discovery messages."""
|
||||||
|
return list(self.message_obj.values())
|
||||||
|
|
||||||
|
def send(self, addon, service, config):
|
||||||
|
"""Send a discovery message to Home Assistant."""
|
||||||
|
try:
|
||||||
|
config = DISCOVERY_SERVICES[service](config)
|
||||||
|
except vol.Invalid as err:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Invalid discovery %s config", humanize_error(config, err))
|
||||||
|
raise DiscoveryError() from None
|
||||||
|
|
||||||
|
# Create message
|
||||||
|
message = Message(addon.slug, service, config)
|
||||||
|
|
||||||
|
# Already exists?
|
||||||
|
for old_message in self.list_messages:
|
||||||
|
if old_message != message:
|
||||||
|
continue
|
||||||
|
_LOGGER.info("Duplicate discovery message from %s", addon.slug)
|
||||||
|
return old_message
|
||||||
|
|
||||||
|
_LOGGER.info("Send discovery to Home Assistant %s from %s",
|
||||||
|
service, addon.slug)
|
||||||
|
self.message_obj[message.uuid] = message
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
self.sys_create_task(self._push_discovery(message, CMD_NEW))
|
||||||
|
return message
|
||||||
|
|
||||||
|
def remove(self, message):
|
||||||
|
"""Remove a discovery message from Home Assistant."""
|
||||||
|
self.message_obj.pop(message.uuid, None)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
_LOGGER.info("Delete discovery to Home Assistant %s from %s",
|
||||||
|
message.service, message.addon)
|
||||||
|
self.sys_create_task(self._push_discovery(message, CMD_DEL))
|
||||||
|
|
||||||
|
async def _push_discovery(self, message, command):
|
||||||
|
"""Send a discovery request."""
|
||||||
|
if not await self.sys_homeassistant.check_api_state():
|
||||||
|
_LOGGER.info("Discovery %s mesage ignore", message.uuid)
|
||||||
|
return
|
||||||
|
|
||||||
|
data = attr.asdict(message)
|
||||||
|
data.pop(ATTR_CONFIG)
|
||||||
|
|
||||||
|
with suppress(HomeAssistantAPIError):
|
||||||
|
async with self.sys_homeassistant.make_request(
|
||||||
|
command, f"api/hassio_push/discovery/{message.uuid}",
|
||||||
|
json=data, timeout=10):
|
||||||
|
_LOGGER.info("Discovery %s message send", message.uuid)
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.warning("Discovery %s message fail", message.uuid)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Message:
|
||||||
|
"""Represent a single Discovery message."""
|
||||||
|
addon = attr.ib()
|
||||||
|
service = attr.ib()
|
||||||
|
config = attr.ib(cmp=False)
|
||||||
|
uuid = attr.ib(factory=lambda: uuid4().hex, cmp=False)
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO docker object."""
|
"""Init file for Hass.io Docker object."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -10,18 +10,22 @@ from ..const import SOCKET_DOCKER
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
CommandReturn = attr.make_class('CommandReturn', ['exit_code', 'output'])
|
@attr.s(frozen=True)
|
||||||
|
class CommandReturn:
|
||||||
|
"""Return object from command run."""
|
||||||
|
exit_code = attr.ib()
|
||||||
|
output = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
class DockerAPI:
|
class DockerAPI:
|
||||||
"""Docker hassio wrapper.
|
"""Docker Hass.io wrapper.
|
||||||
|
|
||||||
This class is not AsyncIO safe!
|
This class is not AsyncIO safe!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.docker = docker.DockerClient(
|
self.docker = docker.DockerClient(
|
||||||
base_url="unix:/{}".format(str(SOCKET_DOCKER)),
|
base_url="unix:/{}".format(str(SOCKET_DOCKER)),
|
||||||
version='auto', timeout=900)
|
version='auto', timeout=900)
|
||||||
@@ -29,21 +33,21 @@ class DockerAPI:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def images(self):
|
def images(self):
|
||||||
"""Return api images."""
|
"""Return API images."""
|
||||||
return self.docker.images
|
return self.docker.images
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def containers(self):
|
def containers(self):
|
||||||
"""Return api containers."""
|
"""Return API containers."""
|
||||||
return self.docker.containers
|
return self.docker.containers
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api(self):
|
def api(self):
|
||||||
"""Return api containers."""
|
"""Return API containers."""
|
||||||
return self.docker.api
|
return self.docker.api
|
||||||
|
|
||||||
def run(self, image, **kwargs):
|
def run(self, image, **kwargs):
|
||||||
""""Create a docker and run it.
|
""""Create a Docker container and run it.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -51,7 +55,7 @@ class DockerAPI:
|
|||||||
network_mode = kwargs.get('network_mode')
|
network_mode = kwargs.get('network_mode')
|
||||||
hostname = kwargs.get('hostname')
|
hostname = kwargs.get('hostname')
|
||||||
|
|
||||||
# setup network
|
# Setup network
|
||||||
kwargs['dns_search'] = ["."]
|
kwargs['dns_search'] = ["."]
|
||||||
if network_mode:
|
if network_mode:
|
||||||
kwargs['dns'] = [str(self.network.supervisor)]
|
kwargs['dns'] = [str(self.network.supervisor)]
|
||||||
@@ -59,9 +63,10 @@ class DockerAPI:
|
|||||||
else:
|
else:
|
||||||
kwargs['network'] = None
|
kwargs['network'] = None
|
||||||
|
|
||||||
# create container
|
# Create container
|
||||||
try:
|
try:
|
||||||
container = self.docker.containers.create(image, **kwargs)
|
container = self.docker.containers.create(
|
||||||
|
image, use_config_proxy=False, **kwargs)
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.error("Can't create container from %s: %s", name, err)
|
_LOGGER.error("Can't create container from %s: %s", name, err)
|
||||||
return False
|
return False
|
||||||
@@ -97,6 +102,7 @@ class DockerAPI:
|
|||||||
image,
|
image,
|
||||||
command=command,
|
command=command,
|
||||||
network=self.network.name,
|
network=self.network.name,
|
||||||
|
use_config_proxy=False,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +114,7 @@ class DockerAPI:
|
|||||||
_LOGGER.error("Can't execute command: %s", err)
|
_LOGGER.error("Can't execute command: %s", err)
|
||||||
return CommandReturn(None, b"")
|
return CommandReturn(None, b"")
|
||||||
|
|
||||||
|
finally:
|
||||||
# cleanup container
|
# cleanup container
|
||||||
with suppress(docker.errors.DockerException):
|
with suppress(docker.errors.DockerException):
|
||||||
container.remove(force=True)
|
container.remove(force=True)
|
||||||
|
@@ -1,16 +1,14 @@
|
|||||||
"""Init file for HassIO addon docker object."""
|
"""Init file for Hass.io add-on Docker object."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .interface import DockerInterface
|
from .interface import DockerInterface
|
||||||
from ..addons.build import AddonBuild
|
from ..addons.build import AddonBuild
|
||||||
from ..const import (
|
from ..const import (MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE,
|
||||||
MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE, ENV_TOKEN,
|
ENV_TOKEN, ENV_TIME, SECURITY_PROFILE, SECURITY_DISABLE)
|
||||||
ENV_TIME, SECURITY_PROFILE, SECURITY_DISABLE)
|
|
||||||
from ..utils import process_lock
|
from ..utils import process_lock
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -19,45 +17,45 @@ AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
|
|||||||
|
|
||||||
|
|
||||||
class DockerAddon(DockerInterface):
|
class DockerAddon(DockerInterface):
|
||||||
"""Docker hassio wrapper for HomeAssistant."""
|
"""Docker Hass.io wrapper for Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, coresys, slug):
|
def __init__(self, coresys, slug):
|
||||||
"""Initialize docker homeassistant wrapper."""
|
"""Initialize Docker Home Assistant wrapper."""
|
||||||
super().__init__(coresys)
|
super().__init__(coresys)
|
||||||
self._id = slug
|
self._id = slug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addon(self):
|
def addon(self):
|
||||||
"""Return addon of docker image."""
|
"""Return add-on of Docker image."""
|
||||||
return self.sys_addons.get(self._id)
|
return self.sys_addons.get(self._id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return name of docker image."""
|
"""Return name of Docker image."""
|
||||||
return self.addon.image
|
return self.addon.image
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeout(self):
|
def timeout(self):
|
||||||
"""Return timeout for docker actions."""
|
"""Return timeout for Docker actions."""
|
||||||
return self.addon.timeout
|
return self.addon.timeout
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return version of docker image."""
|
"""Return version of Docker image."""
|
||||||
if not self.addon.legacy:
|
if self.addon.legacy:
|
||||||
return super().version
|
|
||||||
return self.addon.version_installed
|
return self.addon.version_installed
|
||||||
|
return super().version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
"""Return arch of docker image."""
|
"""Return arch of Docker image."""
|
||||||
if not self.addon.legacy:
|
if self.addon.legacy:
|
||||||
|
return self.sys_arch.default
|
||||||
return super().arch
|
return super().arch
|
||||||
return self.sys_arch
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of Docker container."""
|
||||||
return "addon_{}".format(self.addon.slug)
|
return "addon_{}".format(self.addon.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -74,25 +72,27 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self):
|
def hostname(self):
|
||||||
"""Return slug/id of addon."""
|
"""Return slug/id of add-on."""
|
||||||
return self.addon.slug.replace('_', '-')
|
return self.addon.slug.replace('_', '-')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def environment(self):
|
def environment(self):
|
||||||
"""Return environment for docker add-on."""
|
"""Return environment for Docker add-on."""
|
||||||
addon_env = self.addon.environment or {}
|
addon_env = self.addon.environment or {}
|
||||||
|
|
||||||
# Need audio settings
|
# Provide options for legacy add-ons
|
||||||
if self.addon.with_audio:
|
if self.addon.legacy:
|
||||||
addon_env.update({
|
for key, value in self.addon.options.items():
|
||||||
'ALSA_OUTPUT': self.addon.audio_output,
|
if isinstance(value, (int, str)):
|
||||||
'ALSA_INPUT': self.addon.audio_input,
|
addon_env[key] = value
|
||||||
})
|
else:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Can not set nested option %s as Docker env", key)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**addon_env,
|
**addon_env,
|
||||||
ENV_TIME: self.sys_timezone,
|
ENV_TIME: self.sys_timezone,
|
||||||
ENV_TOKEN: self.addon.uuid,
|
ENV_TOKEN: self.addon.hassio_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -101,7 +101,7 @@ class DockerAddon(DockerInterface):
|
|||||||
devices = self.addon.devices or []
|
devices = self.addon.devices or []
|
||||||
|
|
||||||
# Use audio devices
|
# Use audio devices
|
||||||
if self.addon.with_audio and AUDIO_DEVICE not in devices:
|
if self.addon.with_audio and self.sys_hardware.support_audio:
|
||||||
devices.append(AUDIO_DEVICE)
|
devices.append(AUDIO_DEVICE)
|
||||||
|
|
||||||
# Auto mapping UART devices
|
# Auto mapping UART devices
|
||||||
@@ -114,7 +114,7 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
"""Filter None from addon ports."""
|
"""Filter None from add-on ports."""
|
||||||
if not self.addon.ports:
|
if not self.addon.ports:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def security_opt(self):
|
def security_opt(self):
|
||||||
"""Controlling security opt."""
|
"""Controlling security options."""
|
||||||
security = []
|
security = []
|
||||||
|
|
||||||
# AppArmor
|
# AppArmor
|
||||||
@@ -144,7 +144,7 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def tmpfs(self):
|
def tmpfs(self):
|
||||||
"""Return tmpfs for docker add-on."""
|
"""Return tmpfs for Docker add-on."""
|
||||||
options = self.addon.tmpfs
|
options = self.addon.tmpfs
|
||||||
if options:
|
if options:
|
||||||
return {"/tmpfs": f"{options}"}
|
return {"/tmpfs": f"{options}"}
|
||||||
@@ -160,18 +160,27 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def network_mode(self):
|
def network_mode(self):
|
||||||
"""Return network mode for addon."""
|
"""Return network mode for add-on."""
|
||||||
if self.addon.host_network:
|
if self.addon.host_network:
|
||||||
return 'host'
|
return 'host'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pid_mode(self):
|
||||||
|
"""Return PID mode for add-on."""
|
||||||
|
if not self.addon.protected and self.addon.host_pid:
|
||||||
|
return 'host'
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def volumes(self):
|
def volumes(self):
|
||||||
"""Generate volumes for mappings."""
|
"""Generate volumes for mappings."""
|
||||||
volumes = {
|
volumes = {
|
||||||
str(self.addon.path_extern_data): {
|
str(self.addon.path_extern_data): {
|
||||||
'bind': "/data", 'mode': 'rw'
|
'bind': "/data",
|
||||||
}}
|
'mode': 'rw'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addon_mapping = self.addon.map_volumes
|
addon_mapping = self.addon.map_volumes
|
||||||
|
|
||||||
@@ -179,43 +188,52 @@ class DockerAddon(DockerInterface):
|
|||||||
if MAP_CONFIG in addon_mapping:
|
if MAP_CONFIG in addon_mapping:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.sys_config.path_extern_homeassistant): {
|
str(self.sys_config.path_extern_homeassistant): {
|
||||||
'bind': "/config", 'mode': addon_mapping[MAP_CONFIG]
|
'bind': "/config",
|
||||||
}})
|
'mode': addon_mapping[MAP_CONFIG]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if MAP_SSL in addon_mapping:
|
if MAP_SSL in addon_mapping:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.sys_config.path_extern_ssl): {
|
str(self.sys_config.path_extern_ssl): {
|
||||||
'bind': "/ssl", 'mode': addon_mapping[MAP_SSL]
|
'bind': "/ssl",
|
||||||
}})
|
'mode': addon_mapping[MAP_SSL]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if MAP_ADDONS in addon_mapping:
|
if MAP_ADDONS in addon_mapping:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.sys_config.path_extern_addons_local): {
|
str(self.sys_config.path_extern_addons_local): {
|
||||||
'bind': "/addons", 'mode': addon_mapping[MAP_ADDONS]
|
'bind': "/addons",
|
||||||
}})
|
'mode': addon_mapping[MAP_ADDONS]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if MAP_BACKUP in addon_mapping:
|
if MAP_BACKUP in addon_mapping:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.sys_config.path_extern_backup): {
|
str(self.sys_config.path_extern_backup): {
|
||||||
'bind': "/backup", 'mode': addon_mapping[MAP_BACKUP]
|
'bind': "/backup",
|
||||||
}})
|
'mode': addon_mapping[MAP_BACKUP]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
if MAP_SHARE in addon_mapping:
|
if MAP_SHARE in addon_mapping:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.sys_config.path_extern_share): {
|
str(self.sys_config.path_extern_share): {
|
||||||
'bind': "/share", 'mode': addon_mapping[MAP_SHARE]
|
'bind': "/share",
|
||||||
}})
|
'mode': addon_mapping[MAP_SHARE]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
# Init other hardware mappings
|
# Init other hardware mappings
|
||||||
|
|
||||||
# GPIO support
|
# GPIO support
|
||||||
if self.addon.with_gpio:
|
if self.addon.with_gpio and self.sys_hardware.support_gpio:
|
||||||
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
|
for gpio_path in ("/sys/class/gpio", "/sys/devices/platform/soc"):
|
||||||
if not Path(gpio_path).exists():
|
|
||||||
continue
|
|
||||||
volumes.update({
|
volumes.update({
|
||||||
gpio_path: {
|
gpio_path: {
|
||||||
'bind': gpio_path, 'mode': 'rw'
|
'bind': gpio_path,
|
||||||
|
'mode': 'rw'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -223,7 +241,17 @@ class DockerAddon(DockerInterface):
|
|||||||
if self.addon.with_devicetree:
|
if self.addon.with_devicetree:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
"/sys/firmware/devicetree/base": {
|
"/sys/firmware/devicetree/base": {
|
||||||
'bind': "/device-tree", 'mode': 'ro'
|
'bind': "/device-tree",
|
||||||
|
'mode': 'ro'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Kernel Modules support
|
||||||
|
if self.addon.with_kernel_modules:
|
||||||
|
volumes.update({
|
||||||
|
"/lib/modules": {
|
||||||
|
'bind': "/lib/modules",
|
||||||
|
'mode': 'ro'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -231,28 +259,33 @@ class DockerAddon(DockerInterface):
|
|||||||
if not self.addon.protected and self.addon.access_docker_api:
|
if not self.addon.protected and self.addon.access_docker_api:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
"/var/run/docker.sock": {
|
"/var/run/docker.sock": {
|
||||||
'bind': "/var/run/docker.sock", 'mode': 'ro'
|
'bind': "/var/run/docker.sock",
|
||||||
|
'mode': 'ro'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Host dbus system
|
# Host D-Bus system
|
||||||
if self.addon.host_dbus:
|
if self.addon.host_dbus:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
"/var/run/dbus": {
|
"/var/run/dbus": {
|
||||||
'bind': "/var/run/dbus", 'mode': 'rw'
|
'bind': "/var/run/dbus",
|
||||||
}})
|
'mode': 'rw'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
# ALSA configuration
|
# ALSA configuration
|
||||||
if self.addon.with_audio:
|
if self.addon.with_audio:
|
||||||
volumes.update({
|
volumes.update({
|
||||||
str(self.addon.path_extern_asound): {
|
str(self.addon.path_extern_asound): {
|
||||||
'bind': "/etc/asound.conf", 'mode': 'ro'
|
'bind': "/etc/asound.conf",
|
||||||
}})
|
'mode': 'ro'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return volumes
|
return volumes
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""Run docker image.
|
"""Run Docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -261,8 +294,8 @@ class DockerAddon(DockerInterface):
|
|||||||
|
|
||||||
# Security check
|
# Security check
|
||||||
if not self.addon.protected:
|
if not self.addon.protected:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning("%s run with disabled protected mode!",
|
||||||
"%s run with disabled proteced mode!", self.addon.name)
|
self.addon.name)
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
self._stop()
|
self._stop()
|
||||||
@@ -277,6 +310,7 @@ class DockerAddon(DockerInterface):
|
|||||||
ipc_mode=self.ipc,
|
ipc_mode=self.ipc,
|
||||||
stdin_open=self.addon.with_stdin,
|
stdin_open=self.addon.with_stdin,
|
||||||
network_mode=self.network_mode,
|
network_mode=self.network_mode,
|
||||||
|
pid_mode=self.pid_mode,
|
||||||
ports=self.ports,
|
ports=self.ports,
|
||||||
extra_hosts=self.network_mapping,
|
extra_hosts=self.network_mapping,
|
||||||
devices=self.devices,
|
devices=self.devices,
|
||||||
@@ -284,27 +318,26 @@ class DockerAddon(DockerInterface):
|
|||||||
security_opt=self.security_opt,
|
security_opt=self.security_opt,
|
||||||
environment=self.environment,
|
environment=self.environment,
|
||||||
volumes=self.volumes,
|
volumes=self.volumes,
|
||||||
tmpfs=self.tmpfs
|
tmpfs=self.tmpfs)
|
||||||
)
|
|
||||||
|
|
||||||
if ret:
|
if ret:
|
||||||
_LOGGER.info("Start docker addon %s with version %s",
|
_LOGGER.info("Start Docker add-on %s with version %s", self.image,
|
||||||
self.image, self.version)
|
self.version)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _install(self, tag):
|
def _install(self, tag, image=None):
|
||||||
"""Pull docker image or build it.
|
"""Pull Docker image or build it.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
if self.addon.need_build:
|
if self.addon.need_build:
|
||||||
return self._build(tag)
|
return self._build(tag)
|
||||||
|
|
||||||
return super()._install(tag)
|
return super()._install(tag, image)
|
||||||
|
|
||||||
def _build(self, tag):
|
def _build(self, tag):
|
||||||
"""Build a docker container.
|
"""Build a Docker container.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -313,7 +346,7 @@ class DockerAddon(DockerInterface):
|
|||||||
_LOGGER.info("Start build %s:%s", self.image, tag)
|
_LOGGER.info("Start build %s:%s", self.image, tag)
|
||||||
try:
|
try:
|
||||||
image, log = self.sys_docker.images.build(
|
image, log = self.sys_docker.images.build(
|
||||||
**build_env.get_docker_args(tag))
|
use_config_proxy=False, **build_env.get_docker_args(tag))
|
||||||
|
|
||||||
_LOGGER.debug("Build %s:%s done: %s", self.image, tag, log)
|
_LOGGER.debug("Build %s:%s done: %s", self.image, tag, log)
|
||||||
image.tag(self.image, tag='latest')
|
image.tag(self.image, tag='latest')
|
||||||
@@ -321,7 +354,7 @@ class DockerAddon(DockerInterface):
|
|||||||
# Update meta data
|
# Update meta data
|
||||||
self._meta = image.attrs
|
self._meta = image.attrs
|
||||||
|
|
||||||
except (docker.errors.DockerException) as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.error("Can't build %s:%s: %s", self.image, tag, err)
|
_LOGGER.error("Can't build %s:%s: %s", self.image, tag, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -395,7 +428,7 @@ class DockerAddon(DockerInterface):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# load needed docker objects
|
# Load needed docker objects
|
||||||
container = self.sys_docker.containers.get(self.name)
|
container = self.sys_docker.containers.get(self.name)
|
||||||
socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
|
socket = container.attach_socket(params={'stdin': 1, 'stream': 1})
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
@@ -403,7 +436,7 @@ class DockerAddon(DockerInterface):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# write to stdin
|
# Write to stdin
|
||||||
data += b"\n"
|
data += b"\n"
|
||||||
os.write(socket.fileno(), data)
|
os.write(socket.fileno(), data)
|
||||||
socket.close()
|
socket.close()
|
||||||
|
@@ -3,35 +3,35 @@ import logging
|
|||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
|
||||||
from .interface import DockerInterface
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .interface import DockerInterface
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
|
class DockerHassOSCli(DockerInterface, CoreSysAttributes):
|
||||||
"""Docker hassio wrapper for HassOS Cli."""
|
"""Docker Hass.io wrapper for HassOS Cli."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return name of HassOS cli image."""
|
"""Return name of HassOS CLI image."""
|
||||||
return f"homeassistant/{self.sys_arch}-hassio-cli"
|
return f"homeassistant/{self.sys_arch.supervisor}-hassio-cli"
|
||||||
|
|
||||||
def _stop(self):
|
def _stop(self):
|
||||||
"""Don't need stop."""
|
"""Don't need stop."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _attach(self):
|
def _attach(self):
|
||||||
"""Attach to running docker container.
|
"""Attach to running Docker container.
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
image = self.sys_docker.images.get(self.image)
|
image = self.sys_docker.images.get(self.image)
|
||||||
|
|
||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
_LOGGER.warning("Can't find a HassOS cli %s", self.image)
|
_LOGGER.warning("Can't find a HassOS CLI %s", self.image)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._meta = image.attrs
|
self._meta = image.attrs
|
||||||
_LOGGER.info("Found HassOS cli %s with version %s",
|
_LOGGER.info("Found HassOS CLI %s with version %s", self.image,
|
||||||
self.image, self.version)
|
self.version)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO docker object."""
|
"""Init file for Hass.io Docker object."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
@@ -12,35 +12,35 @@ HASS_DOCKER_NAME = 'homeassistant'
|
|||||||
|
|
||||||
|
|
||||||
class DockerHomeAssistant(DockerInterface):
|
class DockerHomeAssistant(DockerInterface):
|
||||||
"""Docker hassio wrapper for HomeAssistant."""
|
"""Docker Hass.io wrapper for Home Assistant."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def machine(self):
|
def machine(self):
|
||||||
"""Return machine of Home-Assistant docker image."""
|
"""Return machine of Home Assistant Docker image."""
|
||||||
if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']:
|
if self._meta and LABEL_MACHINE in self._meta['Config']['Labels']:
|
||||||
return self._meta['Config']['Labels'][LABEL_MACHINE]
|
return self._meta['Config']['Labels'][LABEL_MACHINE]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return name of docker image."""
|
"""Return name of Docker image."""
|
||||||
return self.sys_homeassistant.image
|
return self.sys_homeassistant.image
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of Docker container."""
|
||||||
return HASS_DOCKER_NAME
|
return HASS_DOCKER_NAME
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
"""Create list of special device to map into docker."""
|
"""Create list of special device to map into Docker."""
|
||||||
devices = []
|
devices = []
|
||||||
for device in self.sys_hardware.serial_devices:
|
for device in self.sys_hardware.serial_devices:
|
||||||
devices.append(f"{device}:{device}:rwm")
|
devices.append(f"{device}:{device}:rwm")
|
||||||
return devices or None
|
return devices or None
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""Run docker image.
|
"""Run Docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +62,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
environment={
|
environment={
|
||||||
'HASSIO': self.sys_docker.network.supervisor,
|
'HASSIO': self.sys_docker.network.supervisor,
|
||||||
ENV_TIME: self.sys_timezone,
|
ENV_TIME: self.sys_timezone,
|
||||||
ENV_TOKEN: self.sys_homeassistant.uuid,
|
ENV_TOKEN: self.sys_homeassistant.hassio_token,
|
||||||
},
|
},
|
||||||
volumes={
|
volumes={
|
||||||
str(self.sys_config.path_extern_homeassistant):
|
str(self.sys_config.path_extern_homeassistant):
|
||||||
@@ -108,7 +108,7 @@ class DockerHomeAssistant(DockerInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def is_initialize(self):
|
def is_initialize(self):
|
||||||
"""Return True if docker container exists."""
|
"""Return True if Docker container exists."""
|
||||||
return self.sys_run_in_executor(self._is_initialize)
|
return self.sys_run_in_executor(self._is_initialize)
|
||||||
|
|
||||||
def _is_initialize(self):
|
def _is_initialize(self):
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Interface class for HassIO docker object."""
|
"""Interface class for Hass.io Docker object."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
@@ -14,44 +14,50 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DockerInterface(CoreSysAttributes):
|
class DockerInterface(CoreSysAttributes):
|
||||||
"""Docker hassio interface."""
|
"""Docker Hass.io interface."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize docker base wrapper."""
|
"""Initialize Docker base wrapper."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self._meta = None
|
self._meta = None
|
||||||
self.lock = asyncio.Lock(loop=coresys.loop)
|
self.lock = asyncio.Lock(loop=coresys.loop)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timeout(self):
|
def timeout(self):
|
||||||
"""Return timeout for docker actions."""
|
"""Return timeout for Docker actions."""
|
||||||
return 30
|
return 30
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of Docker container."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta_config(self):
|
||||||
|
"""Return meta data of configuration for container/image."""
|
||||||
|
if not self._meta:
|
||||||
|
return {}
|
||||||
|
return self._meta.get('Config', {})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def meta_labels(self):
|
||||||
|
"""Return meta data of labels for container/image."""
|
||||||
|
return self.meta_config.get('Labels') or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return name of docker image."""
|
"""Return name of Docker image."""
|
||||||
if not self._meta:
|
return self.meta_config.get('Image')
|
||||||
return None
|
|
||||||
return self._meta['Config']['Image']
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return version of docker image."""
|
"""Return version of Docker image."""
|
||||||
if self._meta and LABEL_VERSION in self._meta['Config']['Labels']:
|
return self.meta_labels.get(LABEL_VERSION)
|
||||||
return self._meta['Config']['Labels'][LABEL_VERSION]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def arch(self):
|
def arch(self):
|
||||||
"""Return arch of docker image."""
|
"""Return arch of Docker image."""
|
||||||
if self._meta and LABEL_ARCH in self._meta['Config']['Labels']:
|
return self.meta_labels.get(LABEL_ARCH)
|
||||||
return self._meta['Config']['Labels'][LABEL_ARCH]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def in_progress(self):
|
def in_progress(self):
|
||||||
@@ -59,76 +65,78 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return self.lock.locked()
|
return self.lock.locked()
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def install(self, tag):
|
def install(self, tag, image=None):
|
||||||
"""Pull docker image."""
|
"""Pull docker image."""
|
||||||
return self.sys_run_in_executor(self._install, tag)
|
return self.sys_run_in_executor(self._install, tag, image)
|
||||||
|
|
||||||
def _install(self, tag):
|
def _install(self, tag, image=None):
|
||||||
"""Pull docker image.
|
"""Pull Docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
image = image or self.image
|
||||||
_LOGGER.info("Pull image %s tag %s.", self.image, tag)
|
|
||||||
image = self.sys_docker.images.pull(f"{self.image}:{tag}")
|
|
||||||
|
|
||||||
image.tag(self.image, tag='latest')
|
try:
|
||||||
self._meta = image.attrs
|
_LOGGER.info("Pull image %s tag %s.", image, tag)
|
||||||
|
docker_image = self.sys_docker.images.pull(f"{image}:{tag}")
|
||||||
|
|
||||||
|
docker_image.tag(image, tag='latest')
|
||||||
|
self._meta = docker_image.attrs
|
||||||
except docker.errors.APIError as err:
|
except docker.errors.APIError as err:
|
||||||
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
|
_LOGGER.error("Can't install %s:%s -> %s.", image, tag, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_LOGGER.info("Tag image %s with version %s as latest", self.image, tag)
|
_LOGGER.info("Tag image %s with version %s as latest", image, tag)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def exists(self):
|
def exists(self):
|
||||||
"""Return True if docker image exists in local repo."""
|
"""Return True if Docker image exists in local repository."""
|
||||||
return self.sys_run_in_executor(self._exists)
|
return self.sys_run_in_executor(self._exists)
|
||||||
|
|
||||||
def _exists(self):
|
def _exists(self):
|
||||||
"""Return True if docker image exists in local repo.
|
"""Return True if Docker image exists in local repository.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
image = self.sys_docker.images.get(self.image)
|
docker_image = self.sys_docker.images.get(self.image)
|
||||||
assert f"{self.image}:{self.version}" in image.tags
|
assert f"{self.image}:{self.version}" in docker_image.tags
|
||||||
except (docker.errors.DockerException, AssertionError):
|
except (docker.errors.DockerException, AssertionError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
"""Return True if docker is Running.
|
"""Return True if Docker is running.
|
||||||
|
|
||||||
Return a Future.
|
Return a Future.
|
||||||
"""
|
"""
|
||||||
return self.sys_run_in_executor(self._is_running)
|
return self.sys_run_in_executor(self._is_running)
|
||||||
|
|
||||||
def _is_running(self):
|
def _is_running(self):
|
||||||
"""Return True if docker is Running.
|
"""Return True if Docker is running.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
container = self.sys_docker.containers.get(self.name)
|
docker_container = self.sys_docker.containers.get(self.name)
|
||||||
image = self.sys_docker.images.get(self.image)
|
docker_image = self.sys_docker.images.get(self.image)
|
||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# container is not running
|
# container is not running
|
||||||
if container.status != 'running':
|
if docker_container.status != 'running':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# we run on an old image, stop and start it
|
# we run on an old image, stop and start it
|
||||||
if container.image.id != image.id:
|
if docker_container.image.id != docker_image.id:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def attach(self):
|
def attach(self):
|
||||||
"""Attach to running docker container."""
|
"""Attach to running Docker container."""
|
||||||
return self.sys_run_in_executor(self._attach)
|
return self.sys_run_in_executor(self._attach)
|
||||||
|
|
||||||
def _attach(self):
|
def _attach(self):
|
||||||
@@ -144,18 +152,18 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info("Attach to image %s with version %s", self.image,
|
||||||
"Attach to image %s with version %s", self.image, self.version)
|
self.version)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run docker image."""
|
"""Run Docker image."""
|
||||||
return self.sys_run_in_executor(self._run)
|
return self.sys_run_in_executor(self._run)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""Run docker image.
|
"""Run Docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -163,7 +171,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop/remove docker container."""
|
"""Stop/remove Docker container."""
|
||||||
return self.sys_run_in_executor(self._stop)
|
return self.sys_run_in_executor(self._stop)
|
||||||
|
|
||||||
def _stop(self):
|
def _stop(self):
|
||||||
@@ -172,24 +180,24 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
container = self.sys_docker.containers.get(self.name)
|
docker_container = self.sys_docker.containers.get(self.name)
|
||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if container.status == 'running':
|
if docker_container.status == 'running':
|
||||||
_LOGGER.info("Stop %s docker application", self.image)
|
_LOGGER.info("Stop %s Docker application", self.image)
|
||||||
with suppress(docker.errors.DockerException):
|
with suppress(docker.errors.DockerException):
|
||||||
container.stop(timeout=self.timeout)
|
docker_container.stop(timeout=self.timeout)
|
||||||
|
|
||||||
with suppress(docker.errors.DockerException):
|
with suppress(docker.errors.DockerException):
|
||||||
_LOGGER.info("Clean %s docker application", self.image)
|
_LOGGER.info("Clean %s Docker application", self.image)
|
||||||
container.remove(force=True)
|
docker_container.remove(force=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove docker images."""
|
"""Remove Docker images."""
|
||||||
return self.sys_run_in_executor(self._remove)
|
return self.sys_run_in_executor(self._remove)
|
||||||
|
|
||||||
def _remove(self):
|
def _remove(self):
|
||||||
@@ -197,11 +205,11 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
# cleanup container
|
# Cleanup container
|
||||||
self._stop()
|
self._stop()
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info("Remove Docker %s with latest and %s", self.image,
|
||||||
"Remove docker %s with latest and %s", self.image, self.version)
|
self.version)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with suppress(docker.errors.ImageNotFound):
|
with suppress(docker.errors.ImageNotFound):
|
||||||
@@ -220,49 +228,51 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def update(self, tag):
|
def update(self, tag, image=None):
|
||||||
"""Update a docker image."""
|
"""Update a Docker image."""
|
||||||
return self.sys_run_in_executor(self._update, tag)
|
return self.sys_run_in_executor(self._update, tag, image)
|
||||||
|
|
||||||
def _update(self, tag):
|
def _update(self, tag, image=None):
|
||||||
"""Update a docker image.
|
"""Update a docker image.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
_LOGGER.info(
|
image = image or self.image
|
||||||
"Update docker %s with %s:%s", self.version, self.image, tag)
|
|
||||||
|
|
||||||
# update docker image
|
_LOGGER.info("Update Docker %s:%s to %s:%s", self.image, self.version,
|
||||||
if not self._install(tag):
|
image, tag)
|
||||||
|
|
||||||
|
# Update docker image
|
||||||
|
if not self._install(tag, image):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# stop container & cleanup
|
# Stop container & cleanup
|
||||||
self._stop()
|
self._stop()
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def logs(self):
|
def logs(self):
|
||||||
"""Return docker logs of container.
|
"""Return Docker logs of container.
|
||||||
|
|
||||||
Return a Future.
|
Return a Future.
|
||||||
"""
|
"""
|
||||||
return self.sys_run_in_executor(self._logs)
|
return self.sys_run_in_executor(self._logs)
|
||||||
|
|
||||||
def _logs(self):
|
def _logs(self):
|
||||||
"""Return docker logs of container.
|
"""Return Docker logs of container.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
container = self.sys_docker.containers.get(self.name)
|
docker_container = self.sys_docker.containers.get(self.name)
|
||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return container.logs(tail=100, stdout=True, stderr=True)
|
return docker_container.logs(tail=100, stdout=True, stderr=True)
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.warning("Can't grap logs from %s: %s", self.image, err)
|
_LOGGER.warning("Can't grep logs from %s: %s", self.image, err)
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
@@ -285,7 +295,7 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
with suppress(docker.errors.DockerException):
|
with suppress(docker.errors.DockerException):
|
||||||
_LOGGER.info("Cleanup docker images: %s", image.tags)
|
_LOGGER.info("Cleanup Docker images: %s", image.tags)
|
||||||
self.sys_docker.images.remove(image.id, force=True)
|
self.sys_docker.images.remove(image.id, force=True)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -312,12 +322,12 @@ class DockerInterface(CoreSysAttributes):
|
|||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
container = self.sys_docker.containers.get(self.name)
|
docker_container = self.sys_docker.containers.get(self.name)
|
||||||
except docker.errors.DockerException:
|
except docker.errors.DockerException:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats = container.stats(stream=False)
|
stats = docker_container.stats(stream=False)
|
||||||
return DockerStats(stats)
|
return DockerStats(stats)
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.error("Can't read stats from %s: %s", self.name, err)
|
_LOGGER.error("Can't read stats from %s: %s", self.name, err)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Internal network manager for HassIO."""
|
"""Internal network manager for Hass.io."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
@@ -9,13 +9,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DockerNetwork:
|
class DockerNetwork:
|
||||||
"""Internal HassIO Network.
|
"""Internal Hass.io Network.
|
||||||
|
|
||||||
This class is not AsyncIO safe!
|
This class is not AsyncIO safe!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dock):
|
def __init__(self, dock):
|
||||||
"""Initialize internal hassio network."""
|
"""Initialize internal Hass.io network."""
|
||||||
self.docker = dock
|
self.docker = dock
|
||||||
self.network = self._get_network()
|
self.network = self._get_network()
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class DockerNetwork:
|
|||||||
try:
|
try:
|
||||||
return self.docker.networks.get(DOCKER_NETWORK)
|
return self.docker.networks.get(DOCKER_NETWORK)
|
||||||
except docker.errors.NotFound:
|
except docker.errors.NotFound:
|
||||||
_LOGGER.info("Can't find HassIO network, create new network")
|
_LOGGER.info("Can't find Hass.io network, create new network")
|
||||||
|
|
||||||
ipam_pool = docker.types.IPAMPool(
|
ipam_pool = docker.types.IPAMPool(
|
||||||
subnet=str(DOCKER_NETWORK_MASK),
|
subnet=str(DOCKER_NETWORK_MASK),
|
||||||
@@ -61,7 +61,7 @@ class DockerNetwork:
|
|||||||
})
|
})
|
||||||
|
|
||||||
def attach_container(self, container, alias=None, ipv4=None):
|
def attach_container(self, container, alias=None, ipv4=None):
|
||||||
"""Attach container to hassio network.
|
"""Attach container to Hass.io network.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
@@ -77,7 +77,7 @@ class DockerNetwork:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def detach_default_bridge(self, container):
|
def detach_default_bridge(self, container):
|
||||||
"""Detach default docker bridge.
|
"""Detach default Docker bridge.
|
||||||
|
|
||||||
Need run inside executor.
|
Need run inside executor.
|
||||||
"""
|
"""
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Calc & represent docker stats data."""
|
"""Calc and represent docker stats data."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ class DockerStats:
|
|||||||
"""Hold stats data from container inside."""
|
"""Hold stats data from container inside."""
|
||||||
|
|
||||||
def __init__(self, stats):
|
def __init__(self, stats):
|
||||||
"""Initialize docker stats."""
|
"""Initialize Docker stats."""
|
||||||
self._cpu = 0.0
|
self._cpu = 0.0
|
||||||
self._network_rx = 0
|
self._network_rx = 0
|
||||||
self._network_tx = 0
|
self._network_tx = 0
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Init file for HassIO docker object."""
|
"""Init file for Hass.io Docker object."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -11,11 +11,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
||||||
"""Docker hassio wrapper for Supervisor."""
|
"""Docker Hass.io wrapper for Supervisor."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return name of docker container."""
|
"""Return name of Docker container."""
|
||||||
return os.environ['SUPERVISOR_NAME']
|
return os.environ['SUPERVISOR_NAME']
|
||||||
|
|
||||||
def _attach(self):
|
def _attach(self):
|
||||||
@@ -29,14 +29,14 @@ class DockerSupervisor(DockerInterface, CoreSysAttributes):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self._meta = container.attrs
|
self._meta = container.attrs
|
||||||
_LOGGER.info("Attach to supervisor %s with version %s",
|
_LOGGER.info("Attach to Supervisor %s with version %s",
|
||||||
self.image, self.version)
|
self.image, self.version)
|
||||||
|
|
||||||
# if already attach
|
# If already attach
|
||||||
if container in self.sys_docker.network.containers:
|
if container in self.sys_docker.network.containers:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# attach to network
|
# Attach to network
|
||||||
return self.sys_docker.network.attach_container(
|
return self.sys_docker.network.attach_container(
|
||||||
container, alias=['hassio'],
|
container, alias=['hassio'],
|
||||||
ipv4=self.sys_docker.network.supervisor)
|
ipv4=self.sys_docker.network.supervisor)
|
||||||
|
@@ -3,99 +3,113 @@
|
|||||||
|
|
||||||
class HassioError(Exception):
|
class HassioError(Exception):
|
||||||
"""Root exception."""
|
"""Root exception."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HassioNotSupportedError(HassioError):
|
class HassioNotSupportedError(HassioError):
|
||||||
"""Function is not supported."""
|
"""Function is not supported."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# HomeAssistant
|
# HomeAssistant
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantError(HassioError):
|
class HomeAssistantError(HassioError):
|
||||||
"""Home Assistant exception."""
|
"""Home Assistant exception."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantUpdateError(HomeAssistantError):
|
class HomeAssistantUpdateError(HomeAssistantError):
|
||||||
"""Error on update of a Home Assistant."""
|
"""Error on update of a Home Assistant."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantAPIError(HomeAssistantError):
|
class HomeAssistantAPIError(HomeAssistantError):
|
||||||
"""Home Assistant API exception."""
|
"""Home Assistant API exception."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantAuthError(HomeAssistantAPIError):
|
class HomeAssistantAuthError(HomeAssistantAPIError):
|
||||||
"""Home Assistant Auth API exception."""
|
"""Home Assistant Auth API exception."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# HassOS
|
# HassOS
|
||||||
|
|
||||||
|
|
||||||
class HassOSError(HassioError):
|
class HassOSError(HassioError):
|
||||||
"""HassOS exception."""
|
"""HassOS exception."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HassOSUpdateError(HassOSError):
|
class HassOSUpdateError(HassOSError):
|
||||||
"""Error on update of a HassOS."""
|
"""Error on update of a HassOS."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HassOSNotSupportedError(HassioNotSupportedError):
|
class HassOSNotSupportedError(HassioNotSupportedError):
|
||||||
"""Function not supported by HassOS."""
|
"""Function not supported by HassOS."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
|
||||||
|
|
||||||
|
class HassioArchNotFound(HassioNotSupportedError):
|
||||||
|
"""No matches with exists arch."""
|
||||||
|
|
||||||
|
|
||||||
# Updater
|
# Updater
|
||||||
|
|
||||||
|
|
||||||
class HassioUpdaterError(HassioError):
|
class HassioUpdaterError(HassioError):
|
||||||
"""Error on Updater."""
|
"""Error on Updater."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(HassioError):
|
||||||
|
"""Auth errors."""
|
||||||
|
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
|
|
||||||
|
|
||||||
class HostError(HassioError):
|
class HostError(HassioError):
|
||||||
"""Internal Host error."""
|
"""Internal Host error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HostNotSupportedError(HassioNotSupportedError):
|
class HostNotSupportedError(HassioNotSupportedError):
|
||||||
"""Host function is not supprted."""
|
"""Host function is not supprted."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HostServiceError(HostError):
|
class HostServiceError(HostError):
|
||||||
"""Host service functions fails."""
|
"""Host service functions fails."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HostAppArmorError(HostError):
|
class HostAppArmorError(HostError):
|
||||||
"""Host apparmor functions fails."""
|
"""Host apparmor functions fails."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
||||||
class APIError(HassioError):
|
|
||||||
|
class APIError(HassioError, RuntimeError):
|
||||||
"""API errors."""
|
"""API errors."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class APINotSupportedError(HassioNotSupportedError):
|
class APIForbidden(APIError):
|
||||||
"""API not supported error."""
|
"""API forbidden error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
# Service / Discovery
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveryError(HassioError):
|
||||||
|
"""Discovery Errors."""
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesError(HassioError):
|
||||||
|
"""Services Errors."""
|
||||||
|
|
||||||
|
|
||||||
# utils/gdbus
|
# utils/gdbus
|
||||||
|
|
||||||
|
|
||||||
class DBusError(HassioError):
|
class DBusError(HassioError):
|
||||||
"""DBus generic error."""
|
"""DBus generic error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DBusNotConnectedError(HostNotSupportedError):
|
class DBusNotConnectedError(HostNotSupportedError):
|
||||||
@@ -104,26 +118,22 @@ class DBusNotConnectedError(HostNotSupportedError):
|
|||||||
|
|
||||||
class DBusFatalError(DBusError):
|
class DBusFatalError(DBusError):
|
||||||
"""DBus call going wrong."""
|
"""DBus call going wrong."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class DBusParseError(DBusError):
|
class DBusParseError(DBusError):
|
||||||
"""DBus parse error."""
|
"""DBus parse error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# util/apparmor
|
# util/apparmor
|
||||||
|
|
||||||
|
|
||||||
class AppArmorError(HostAppArmorError):
|
class AppArmorError(HostAppArmorError):
|
||||||
"""General AppArmor error."""
|
"""General AppArmor error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppArmorFileError(AppArmorError):
|
class AppArmorFileError(AppArmorError):
|
||||||
"""AppArmor profile file error."""
|
"""AppArmor profile file error."""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppArmorInvalidError(AppArmorError):
|
class AppArmorInvalidError(AppArmorError):
|
||||||
"""AppArmor profile validate error."""
|
"""AppArmor profile validate error."""
|
||||||
pass
|
|
||||||
|
@@ -66,9 +66,9 @@ class HassOS(CoreSysAttributes):
|
|||||||
return self._board
|
return self._board
|
||||||
|
|
||||||
def _check_host(self):
|
def _check_host(self):
|
||||||
"""Check if HassOS is availabe."""
|
"""Check if HassOS is available."""
|
||||||
if not self.available:
|
if not self.available:
|
||||||
_LOGGER.error("No HassOS availabe")
|
_LOGGER.error("No HassOS available")
|
||||||
raise HassOSNotSupportedError()
|
raise HassOSNotSupportedError()
|
||||||
|
|
||||||
async def _download_raucb(self, version):
|
async def _download_raucb(self, version):
|
||||||
@@ -97,7 +97,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
_LOGGER.warning("Can't fetch versions from %s: %s", url, err)
|
_LOGGER.warning("Can't fetch versions from %s: %s", url, err)
|
||||||
|
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error("Can't write ota file: %s", err)
|
_LOGGER.error("Can't write OTA file: %s", err)
|
||||||
|
|
||||||
raise HassOSUpdateError()
|
raise HassOSUpdateError()
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class HassOS(CoreSysAttributes):
|
|||||||
"""
|
"""
|
||||||
self._check_host()
|
self._check_host()
|
||||||
|
|
||||||
_LOGGER.info("Sync config from USB on HassOS.")
|
_LOGGER.info("Syncing configuration from USB with HassOS.")
|
||||||
return self.sys_host.services.restart('hassos-config.service')
|
return self.sys_host.services.restart('hassos-config.service')
|
||||||
|
|
||||||
async def update(self, version=None):
|
async def update(self, version=None):
|
||||||
@@ -182,5 +182,5 @@ class HassOS(CoreSysAttributes):
|
|||||||
if await self.instance.update(version):
|
if await self.instance.update(version):
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.error("HassOS CLI update fails.")
|
_LOGGER.error("HassOS CLI update fails")
|
||||||
raise HassOSUpdateError()
|
raise HassOSUpdateError()
|
||||||
|
@@ -13,17 +13,15 @@ import aiohttp
|
|||||||
from aiohttp import hdrs
|
from aiohttp import hdrs
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from .const import (
|
from .const import (FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION,
|
||||||
FILE_HASSIO_HOMEASSISTANT, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_UUID,
|
ATTR_UUID, ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL,
|
||||||
ATTR_BOOT, ATTR_PASSWORD, ATTR_PORT, ATTR_SSL, ATTR_WATCHDOG,
|
ATTR_WATCHDOG, ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN,
|
||||||
ATTR_WAIT_BOOT, ATTR_REFRESH_TOKEN,
|
ATTR_ACCESS_TOKEN, HEADER_HA_ACCESS)
|
||||||
HEADER_HA_ACCESS)
|
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .docker.homeassistant import DockerHomeAssistant
|
from .docker.homeassistant import DockerHomeAssistant
|
||||||
from .exceptions import (
|
from .exceptions import (HomeAssistantUpdateError, HomeAssistantError,
|
||||||
HomeAssistantUpdateError, HomeAssistantError, HomeAssistantAPIError,
|
HomeAssistantAPIError, HomeAssistantAuthError)
|
||||||
HomeAssistantAuthError)
|
from .utils import convert_to_ascii, process_lock, create_token
|
||||||
from .utils import convert_to_ascii, process_lock
|
|
||||||
from .utils.json import JsonConfig
|
from .utils.json import JsonConfig
|
||||||
from .validate import SCHEMA_HASS_CONFIG
|
from .validate import SCHEMA_HASS_CONFIG
|
||||||
|
|
||||||
@@ -31,15 +29,19 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
ConfigResult = attr.make_class('ConfigResult', ['valid', 'log'], frozen=True)
|
@attr.s(frozen=True)
|
||||||
|
class ConfigResult:
|
||||||
|
"""Return object from config check."""
|
||||||
|
valid = attr.ib()
|
||||||
|
log = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistant(JsonConfig, CoreSysAttributes):
|
class HomeAssistant(JsonConfig, CoreSysAttributes):
|
||||||
"""Hass core object for handle it."""
|
"""Home Assistant core object for handle it."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize hass object."""
|
"""Initialize Home Assistant object."""
|
||||||
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
|
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self.instance = DockerHomeAssistant(coresys)
|
self.instance = DockerHomeAssistant(coresys)
|
||||||
@@ -54,14 +56,19 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
if await self.instance.attach():
|
if await self.instance.attach():
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
|
_LOGGER.info("No Home Assistant Docker image %s found.", self.image)
|
||||||
await self.install_landingpage()
|
await self.install_landingpage()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def machine(self):
|
def machine(self):
|
||||||
"""Return System Machines."""
|
"""Return the system machines."""
|
||||||
return self.instance.machine
|
return self.instance.machine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arch(self):
|
||||||
|
"""Return arch of running Home Assistant."""
|
||||||
|
return self.instance.arch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error_state(self):
|
def error_state(self):
|
||||||
"""Return True if system is in error."""
|
"""Return True if system is in error."""
|
||||||
@@ -74,76 +81,75 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def api_port(self):
|
def api_port(self):
|
||||||
"""Return network port to home-assistant instance."""
|
"""Return network port to Home Assistant instance."""
|
||||||
return self._data[ATTR_PORT]
|
return self._data[ATTR_PORT]
|
||||||
|
|
||||||
@api_port.setter
|
@api_port.setter
|
||||||
def api_port(self, value):
|
def api_port(self, value):
|
||||||
"""Set network port for home-assistant instance."""
|
"""Set network port for Home Assistant instance."""
|
||||||
self._data[ATTR_PORT] = value
|
self._data[ATTR_PORT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_password(self):
|
def api_password(self):
|
||||||
"""Return password for home-assistant instance."""
|
"""Return password for Home Assistant instance."""
|
||||||
return self._data.get(ATTR_PASSWORD)
|
return self._data.get(ATTR_PASSWORD)
|
||||||
|
|
||||||
@api_password.setter
|
@api_password.setter
|
||||||
def api_password(self, value):
|
def api_password(self, value):
|
||||||
"""Set password for home-assistant instance."""
|
"""Set password for Home Assistant instance."""
|
||||||
self._data[ATTR_PASSWORD] = value
|
self._data[ATTR_PASSWORD] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_ssl(self):
|
def api_ssl(self):
|
||||||
"""Return if we need ssl to home-assistant instance."""
|
"""Return if we need ssl to Home Assistant instance."""
|
||||||
return self._data[ATTR_SSL]
|
return self._data[ATTR_SSL]
|
||||||
|
|
||||||
@api_ssl.setter
|
@api_ssl.setter
|
||||||
def api_ssl(self, value):
|
def api_ssl(self, value):
|
||||||
"""Set SSL for home-assistant instance."""
|
"""Set SSL for Home Assistant instance."""
|
||||||
self._data[ATTR_SSL] = value
|
self._data[ATTR_SSL] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_url(self):
|
def api_url(self):
|
||||||
"""Return API url to Home-Assistant."""
|
"""Return API url to Home Assistant."""
|
||||||
return "{}://{}:{}".format(
|
return "{}://{}:{}".format('https' if self.api_ssl else 'http',
|
||||||
'https' if self.api_ssl else 'http', self.api_ip, self.api_port
|
self.api_ip, self.api_port)
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def watchdog(self):
|
def watchdog(self):
|
||||||
"""Return True if the watchdog should protect Home-Assistant."""
|
"""Return True if the watchdog should protect Home Assistant."""
|
||||||
return self._data[ATTR_WATCHDOG]
|
return self._data[ATTR_WATCHDOG]
|
||||||
|
|
||||||
@watchdog.setter
|
@watchdog.setter
|
||||||
def watchdog(self, value):
|
def watchdog(self, value):
|
||||||
"""Return True if the watchdog should protect Home-Assistant."""
|
"""Return True if the watchdog should protect Home Assistant."""
|
||||||
self._data[ATTR_WATCHDOG] = value
|
self._data[ATTR_WATCHDOG] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wait_boot(self):
|
def wait_boot(self):
|
||||||
"""Return time to wait for Home-Assistant startup."""
|
"""Return time to wait for Home Assistant startup."""
|
||||||
return self._data[ATTR_WAIT_BOOT]
|
return self._data[ATTR_WAIT_BOOT]
|
||||||
|
|
||||||
@wait_boot.setter
|
@wait_boot.setter
|
||||||
def wait_boot(self, value):
|
def wait_boot(self, value):
|
||||||
"""Set time to wait for Home-Assistant startup."""
|
"""Set time to wait for Home Assistant startup."""
|
||||||
self._data[ATTR_WAIT_BOOT] = value
|
self._data[ATTR_WAIT_BOOT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return version of running homeassistant."""
|
"""Return version of running Home Assistant."""
|
||||||
return self.instance.version
|
return self.instance.version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_version(self):
|
def last_version(self):
|
||||||
"""Return last available version of homeassistant."""
|
"""Return last available version of Home Assistant."""
|
||||||
if self.is_custom_image:
|
if self.is_custom_image:
|
||||||
return self._data.get(ATTR_LAST_VERSION)
|
return self._data.get(ATTR_LAST_VERSION)
|
||||||
return self.sys_updater.version_homeassistant
|
return self.sys_updater.version_homeassistant
|
||||||
|
|
||||||
@last_version.setter
|
@last_version.setter
|
||||||
def last_version(self, value):
|
def last_version(self, value):
|
||||||
"""Set last available version of homeassistant."""
|
"""Set last available version of Home Assistant."""
|
||||||
if value:
|
if value:
|
||||||
self._data[ATTR_LAST_VERSION] = value
|
self._data[ATTR_LAST_VERSION] = value
|
||||||
else:
|
else:
|
||||||
@@ -151,14 +157,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self):
|
||||||
"""Return image name of hass containter."""
|
"""Return image name of the Home Assistant container."""
|
||||||
if self._data.get(ATTR_IMAGE):
|
if self._data.get(ATTR_IMAGE):
|
||||||
return self._data[ATTR_IMAGE]
|
return self._data[ATTR_IMAGE]
|
||||||
return os.environ['HOMEASSISTANT_REPOSITORY']
|
return os.environ['HOMEASSISTANT_REPOSITORY']
|
||||||
|
|
||||||
@image.setter
|
@image.setter
|
||||||
def image(self, value):
|
def image(self, value):
|
||||||
"""Set image name of hass containter."""
|
"""Set image name of Home Assistant container."""
|
||||||
if value:
|
if value:
|
||||||
self._data[ATTR_IMAGE] = value
|
self._data[ATTR_IMAGE] = value
|
||||||
else:
|
else:
|
||||||
@@ -167,24 +173,29 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
@property
|
@property
|
||||||
def is_custom_image(self):
|
def is_custom_image(self):
|
||||||
"""Return True if a custom image is used."""
|
"""Return True if a custom image is used."""
|
||||||
return all(attr in self._data for attr in
|
return all(
|
||||||
(ATTR_IMAGE, ATTR_LAST_VERSION))
|
attr in self._data for attr in (ATTR_IMAGE, ATTR_LAST_VERSION))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self):
|
def boot(self):
|
||||||
"""Return True if home-assistant boot is enabled."""
|
"""Return True if Home Assistant boot is enabled."""
|
||||||
return self._data[ATTR_BOOT]
|
return self._data[ATTR_BOOT]
|
||||||
|
|
||||||
@boot.setter
|
@boot.setter
|
||||||
def boot(self, value):
|
def boot(self, value):
|
||||||
"""Set home-assistant boot options."""
|
"""Set Home Assistant boot options."""
|
||||||
self._data[ATTR_BOOT] = value
|
self._data[ATTR_BOOT] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
"""Return a UUID of this HomeAssistant."""
|
"""Return a UUID of this Home Assistant instance."""
|
||||||
return self._data[ATTR_UUID]
|
return self._data[ATTR_UUID]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hassio_token(self):
|
||||||
|
"""Return an access token for the Hass.io API."""
|
||||||
|
return self._data.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def refresh_token(self):
|
def refresh_token(self):
|
||||||
"""Return the refresh token to authenticate with Home Assistant."""
|
"""Return the refresh token to authenticate with Home Assistant."""
|
||||||
@@ -202,15 +213,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
while True:
|
while True:
|
||||||
if await self.instance.install('landingpage'):
|
if await self.instance.install('landingpage'):
|
||||||
break
|
break
|
||||||
_LOGGER.warning("Fails install landingpage, retry after 60sec")
|
_LOGGER.warning("Fails install landingpage, retry after 30sec")
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
# Run landingpage after installation
|
|
||||||
_LOGGER.info("Start landingpage")
|
|
||||||
try:
|
|
||||||
await self._start()
|
|
||||||
except HomeAssistantError:
|
|
||||||
_LOGGER.warning("Can't start landingpage")
|
|
||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
async def install(self):
|
async def install(self):
|
||||||
@@ -224,8 +228,8 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
tag = self.last_version
|
tag = self.last_version
|
||||||
if tag and await self.instance.install(tag):
|
if tag and await self.instance.install(tag):
|
||||||
break
|
break
|
||||||
_LOGGER.warning("Error on install HomeAssistant. Retry in 60sec")
|
_LOGGER.warning("Error on install Home Assistant. Retry in 30sec")
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(30)
|
||||||
|
|
||||||
# finishing
|
# finishing
|
||||||
_LOGGER.info("Home Assistant docker now installed")
|
_LOGGER.info("Home Assistant docker now installed")
|
||||||
@@ -251,7 +255,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
_LOGGER.warning("Version %s is already installed", version)
|
_LOGGER.warning("Version %s is already installed", version)
|
||||||
return HomeAssistantUpdateError()
|
return HomeAssistantUpdateError()
|
||||||
|
|
||||||
# process a update
|
# process an update
|
||||||
async def _update(to_version):
|
async def _update(to_version):
|
||||||
"""Run Home Assistant update."""
|
"""Run Home Assistant update."""
|
||||||
try:
|
try:
|
||||||
@@ -261,7 +265,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
finally:
|
finally:
|
||||||
if running:
|
if running:
|
||||||
await self._start()
|
await self._start()
|
||||||
_LOGGER.info("Successfull run HomeAssistant %s", to_version)
|
_LOGGER.info("Successful run Home Assistant %s", to_version)
|
||||||
|
|
||||||
# Update Home Assistant
|
# Update Home Assistant
|
||||||
with suppress(HomeAssistantError):
|
with suppress(HomeAssistantError):
|
||||||
@@ -276,7 +280,15 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
raise HomeAssistantUpdateError()
|
raise HomeAssistantUpdateError()
|
||||||
|
|
||||||
async def _start(self):
|
async def _start(self):
|
||||||
"""Start HomeAssistant docker & wait."""
|
"""Start Home Assistant Docker & wait."""
|
||||||
|
if await self.instance.is_running():
|
||||||
|
_LOGGER.warning("Home Assistant is already running!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create new API token
|
||||||
|
self._data[ATTR_ACCESS_TOKEN] = create_token()
|
||||||
|
self.save_data()
|
||||||
|
|
||||||
if not await self.instance.run():
|
if not await self.instance.run():
|
||||||
raise HomeAssistantError()
|
raise HomeAssistantError()
|
||||||
await self._block_till_run()
|
await self._block_till_run()
|
||||||
@@ -291,7 +303,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop HomeAssistant docker.
|
"""Stop Home Assistant Docker.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
@@ -299,7 +311,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@process_lock
|
@process_lock
|
||||||
async def restart(self):
|
async def restart(self):
|
||||||
"""Restart HomeAssistant docker."""
|
"""Restart Home Assistant Docker."""
|
||||||
await self.instance.stop()
|
await self.instance.stop()
|
||||||
await self._start()
|
await self._start()
|
||||||
|
|
||||||
@@ -318,14 +330,14 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
return self.instance.stats()
|
return self.instance.stats()
|
||||||
|
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
"""Return True if docker container is running.
|
"""Return True if Docker container is running.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.instance.is_running()
|
return self.instance.is_running()
|
||||||
|
|
||||||
def is_initialize(self):
|
def is_initialize(self):
|
||||||
"""Return True if a docker container is exists.
|
"""Return True if a Docker container is exists.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
@@ -337,25 +349,27 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
return self.instance.in_progress or self.lock.locked()
|
return self.instance.in_progress or self.lock.locked()
|
||||||
|
|
||||||
async def check_config(self):
|
async def check_config(self):
|
||||||
"""Run homeassistant config check."""
|
"""Run Home Assistant config check."""
|
||||||
result = await self.instance.execute_command(
|
result = await self.instance.execute_command(
|
||||||
"python3 -m homeassistant -c /config --script check_config"
|
"python3 -m homeassistant -c /config --script check_config")
|
||||||
)
|
|
||||||
|
|
||||||
# if not valid
|
# if not valid
|
||||||
if result.exit_code is None:
|
if result.exit_code is None:
|
||||||
|
_LOGGER.error("Fatal error on config check!")
|
||||||
raise HomeAssistantError()
|
raise HomeAssistantError()
|
||||||
|
|
||||||
# parse output
|
# parse output
|
||||||
log = convert_to_ascii(result.output)
|
log = convert_to_ascii(result.output)
|
||||||
if result.exit_code != 0 or RE_YAML_ERROR.search(log):
|
if result.exit_code != 0 or RE_YAML_ERROR.search(log):
|
||||||
|
_LOGGER.error("Invalid Home Assistant config found!")
|
||||||
return ConfigResult(False, log)
|
return ConfigResult(False, log)
|
||||||
|
|
||||||
|
_LOGGER.info("Home Assistant config is valid")
|
||||||
return ConfigResult(True, log)
|
return ConfigResult(True, log)
|
||||||
|
|
||||||
async def ensure_access_token(self):
|
async def ensure_access_token(self):
|
||||||
"""Ensures there is an access token."""
|
"""Ensures there is an access token."""
|
||||||
if (self.access_token is not None and
|
if self.access_token is not None and self._access_token_expires > datetime.utcnow():
|
||||||
self._access_token_expires > datetime.utcnow()):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
|
with suppress(asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
@@ -365,8 +379,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
data={
|
data={
|
||||||
"grant_type": "refresh_token",
|
"grant_type": "refresh_token",
|
||||||
"refresh_token": self.refresh_token
|
"refresh_token": self.refresh_token
|
||||||
}
|
}) as resp:
|
||||||
) as resp:
|
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
_LOGGER.error("Can't update Home Assistant access token!")
|
_LOGGER.error("Can't update Home Assistant access token!")
|
||||||
raise HomeAssistantAuthError()
|
raise HomeAssistantAuthError()
|
||||||
@@ -378,8 +391,13 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
datetime.utcnow() + timedelta(seconds=tokens['expires_in'])
|
datetime.utcnow() + timedelta(seconds=tokens['expires_in'])
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def make_request(self, method, path, json=None, content_type=None,
|
async def make_request(self,
|
||||||
data=None, timeout=30):
|
method,
|
||||||
|
path,
|
||||||
|
json=None,
|
||||||
|
content_type=None,
|
||||||
|
data=None,
|
||||||
|
timeout=30):
|
||||||
"""Async context manager to make a request with right auth."""
|
"""Async context manager to make a request with right auth."""
|
||||||
url = f"{self.api_url}/{path}"
|
url = f"{self.api_url}/{path}"
|
||||||
headers = {}
|
headers = {}
|
||||||
@@ -401,8 +419,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
async with getattr(self.sys_websession_ssl, method)(
|
async with getattr(self.sys_websession_ssl, method)(
|
||||||
url, data=data, timeout=timeout, json=json,
|
url, data=data, timeout=timeout, json=json,
|
||||||
headers=headers
|
headers=headers) as resp:
|
||||||
) as resp:
|
|
||||||
# Access token expired
|
# Access token expired
|
||||||
if resp.status == 401 and self.refresh_token:
|
if resp.status == 401 and self.refresh_token:
|
||||||
self.access_token = None
|
self.access_token = None
|
||||||
@@ -416,35 +433,22 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
raise HomeAssistantAPIError()
|
raise HomeAssistantAPIError()
|
||||||
|
|
||||||
async def check_api_state(self):
|
async def check_api_state(self):
|
||||||
"""Return True if Home-Assistant up and running."""
|
"""Return True if Home Assistant up and running."""
|
||||||
with suppress(HomeAssistantAPIError):
|
with suppress(HomeAssistantAPIError):
|
||||||
async with self.make_request('get', 'api/') as resp:
|
async with self.make_request('get', 'api/') as resp:
|
||||||
if resp.status in (200, 201):
|
if resp.status in (200, 201):
|
||||||
return True
|
return True
|
||||||
err = resp.status
|
status = resp.status
|
||||||
|
_LOGGER.warning("Home Assistant API config mismatch: %s", status)
|
||||||
|
|
||||||
_LOGGER.warning("Home-Assistant API config missmatch: %d", err)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def send_event(self, event_type, event_data=None):
|
|
||||||
"""Send event to Home-Assistant."""
|
|
||||||
with suppress(HomeAssistantAPIError):
|
|
||||||
async with self.make_request(
|
|
||||||
'get', f'api/events/{event_type}'
|
|
||||||
) as resp:
|
|
||||||
if resp.status in (200, 201):
|
|
||||||
return
|
|
||||||
err = resp.status
|
|
||||||
|
|
||||||
_LOGGER.warning("HomeAssistant event %s fails: %s", event_type, err)
|
|
||||||
return HomeAssistantError()
|
|
||||||
|
|
||||||
async def _block_till_run(self):
|
async def _block_till_run(self):
|
||||||
"""Block until Home-Assistant is booting up or startup timeout."""
|
"""Block until Home-Assistant is booting up or startup timeout."""
|
||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
migration_progress = False
|
migration_progress = False
|
||||||
migration_file = Path(
|
migration_file = Path(self.sys_config.path_homeassistant,
|
||||||
self.sys_config.path_homeassistant, '.migration_progress')
|
'.migration_progress')
|
||||||
|
|
||||||
def check_port():
|
def check_port():
|
||||||
"""Check if port is mapped."""
|
"""Check if port is mapped."""
|
||||||
@@ -461,23 +465,20 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
# 1
|
# 1: Check if Container is is_running
|
||||||
# Check if Container is is_running
|
|
||||||
if not await self.instance.is_running():
|
if not await self.instance.is_running():
|
||||||
_LOGGER.error("HomeAssistant is crashed!")
|
_LOGGER.error("Home Assistant has crashed!")
|
||||||
break
|
break
|
||||||
|
|
||||||
# 2
|
# 2: Check if API response
|
||||||
# Check if API response
|
|
||||||
if await self.sys_run_in_executor(check_port):
|
if await self.sys_run_in_executor(check_port):
|
||||||
_LOGGER.info("Detect a running Home Assistant instance")
|
_LOGGER.info("Detect a running Home Assistant instance")
|
||||||
self._error_state = False
|
self._error_state = False
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3
|
# 3: Running DB Migration
|
||||||
# Running DB Migration
|
|
||||||
if migration_file.exists():
|
if migration_file.exists():
|
||||||
if not migration_progress:
|
if not migration_progress:
|
||||||
migration_progress = True
|
migration_progress = True
|
||||||
@@ -488,8 +489,7 @@ class HomeAssistant(JsonConfig, CoreSysAttributes):
|
|||||||
start_time = time.monotonic()
|
start_time = time.monotonic()
|
||||||
_LOGGER.info("Home Assistant record migration done")
|
_LOGGER.info("Home Assistant record migration done")
|
||||||
|
|
||||||
# 4
|
# 4: Timeout
|
||||||
# Timeout
|
|
||||||
if time.monotonic() - start_time > self.wait_boot:
|
if time.monotonic() - start_time > self.wait_boot:
|
||||||
_LOGGER.warning("Don't wait anymore of Home Assistant startup!")
|
_LOGGER.warning("Don't wait anymore of Home Assistant startup!")
|
||||||
break
|
break
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Host function like audio/dbus/systemd."""
|
"""Host function like audio, D-Bus or systemd."""
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class HostManager(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def apparmor(self):
|
def apparmor(self):
|
||||||
"""Return host apparmor handler."""
|
"""Return host AppArmor handler."""
|
||||||
return self._apparmor
|
return self._apparmor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Host Audio-support."""
|
"""Host Audio support."""
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -6,7 +6,8 @@ from string import Template
|
|||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from ..const import ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME
|
from ..const import (
|
||||||
|
ATTR_INPUT, ATTR_OUTPUT, ATTR_DEVICES, ATTR_NAME, CHAN_ID, CHAN_TYPE)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -19,7 +20,7 @@ class AlsaAudio(CoreSysAttributes):
|
|||||||
"""Handle Audio ALSA host data."""
|
"""Handle Audio ALSA host data."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize Alsa audio system."""
|
"""Initialize ALSA audio system."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self._data = {
|
self._data = {
|
||||||
ATTR_INPUT: {},
|
ATTR_INPUT: {},
|
||||||
@@ -58,7 +59,9 @@ class AlsaAudio(CoreSysAttributes):
|
|||||||
|
|
||||||
# Process devices
|
# Process devices
|
||||||
for dev_id, dev_data in self.sys_hardware.audio_devices.items():
|
for dev_id, dev_data in self.sys_hardware.audio_devices.items():
|
||||||
for chan_id, chan_type in dev_data[ATTR_DEVICES].items():
|
for chan_info in dev_data[ATTR_DEVICES]:
|
||||||
|
chan_id = chan_info[CHAN_ID]
|
||||||
|
chan_type = chan_info[CHAN_TYPE]
|
||||||
alsa_id = f"{dev_id},{chan_id}"
|
alsa_id = f"{dev_id},{chan_id}"
|
||||||
dev_name = dev_data[ATTR_NAME]
|
dev_name = dev_data[ATTR_NAME]
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'}
|
|||||||
|
|
||||||
|
|
||||||
class AppArmorControl(CoreSysAttributes):
|
class AppArmorControl(CoreSysAttributes):
|
||||||
"""Handle host apparmor controls."""
|
"""Handle host AppArmor controls."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys):
|
||||||
"""Initialize host power handling."""
|
"""Initialize host power handling."""
|
||||||
@@ -23,7 +23,7 @@ class AppArmorControl(CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
"""Return True if AppArmor is availabe on host."""
|
"""Return True if AppArmor is available on host."""
|
||||||
return self._service is not None
|
return self._service is not None
|
||||||
|
|
||||||
def exists(self, profile):
|
def exists(self, profile):
|
||||||
@@ -62,12 +62,12 @@ class AppArmorControl(CoreSysAttributes):
|
|||||||
if self.available:
|
if self.available:
|
||||||
await self._reload_service()
|
await self._reload_service()
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("AppArmor is not enabled on Host")
|
_LOGGER.info("AppArmor is not enabled on host")
|
||||||
|
|
||||||
async def load_profile(self, profile_name, profile_file):
|
async def load_profile(self, profile_name, profile_file):
|
||||||
"""Load/Update a new/exists profile into AppArmor."""
|
"""Load/Update a new/exists profile into AppArmor."""
|
||||||
if not validate_profile(profile_name, profile_file):
|
if not validate_profile(profile_name, profile_file):
|
||||||
_LOGGER.error("profile is not valid with name %s", profile_name)
|
_LOGGER.error("Profile is not valid with name %s", profile_name)
|
||||||
raise HostAppArmorError()
|
raise HostAppArmorError()
|
||||||
|
|
||||||
# Copy to AppArmor folder
|
# Copy to AppArmor folder
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user