mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-26 09:29:22 +00:00
Compare commits
95 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79cc23e273 | ||
![]() |
046ce0230a | ||
![]() |
33a66bee01 | ||
![]() |
f76749a933 | ||
![]() |
385af5bef5 | ||
![]() |
19b72b1a79 | ||
![]() |
f6048467ad | ||
![]() |
dbe6b860c7 | ||
![]() |
0a1e6b3e2d | ||
![]() |
f178dde589 | ||
![]() |
545d45ecf0 | ||
![]() |
c333f94cfa | ||
![]() |
76a999f650 | ||
![]() |
d2f8e35622 | ||
![]() |
1e4ed9c9d1 | ||
![]() |
57c21b4eb5 | ||
![]() |
088cc3ef15 | ||
![]() |
9e326f6324 | ||
![]() |
5840290df7 | ||
![]() |
58fdabb8ff | ||
![]() |
b8fc002fbc | ||
![]() |
d5e349266b | ||
![]() |
4c135dd617 | ||
![]() |
a98a0d1a1a | ||
![]() |
768b4d2b1a | ||
![]() |
ff640c598d | ||
![]() |
c76408e4e8 | ||
![]() |
2e168d089c | ||
![]() |
a61311e928 | ||
![]() |
85ffba90b2 | ||
![]() |
6623ec9bbc | ||
![]() |
7a4ed4029c | ||
![]() |
d4c52ce298 | ||
![]() |
c0aab8497f | ||
![]() |
cabe5b143f | ||
![]() |
3a791bace6 | ||
![]() |
19dfdb094b | ||
![]() |
ec5caa483e | ||
![]() |
e6967f8db5 | ||
![]() |
759356ff2e | ||
![]() |
4d8dbdb486 | ||
![]() |
cddb40a104 | ||
![]() |
36128d119a | ||
![]() |
dadb72aca8 | ||
![]() |
390d4fa6c7 | ||
![]() |
9559c39351 | ||
![]() |
9d95b70534 | ||
![]() |
3bad896978 | ||
![]() |
9f406df129 | ||
![]() |
a9b4174590 | ||
![]() |
9109e3803b | ||
![]() |
422dd78489 | ||
![]() |
9e1d6c9d2b | ||
![]() |
c8e3f2b48a | ||
![]() |
a287f52e47 | ||
![]() |
76952db3eb | ||
![]() |
871721f04b | ||
![]() |
3c0ebdf643 | ||
![]() |
0e258a4ae0 | ||
![]() |
645a8e2372 | ||
![]() |
c6cc8adbb7 | ||
![]() |
dd38c73b85 | ||
![]() |
c916314704 | ||
![]() |
e0dcce5895 | ||
![]() |
906616e224 | ||
![]() |
db20ea95d9 | ||
![]() |
d142ea5d23 | ||
![]() |
5d52404dab | ||
![]() |
43f4b36cfe | ||
![]() |
0393db19e6 | ||
![]() |
b197578df4 | ||
![]() |
ed428c0df4 | ||
![]() |
d38707821c | ||
![]() |
cfb392054e | ||
![]() |
0ea65efeb3 | ||
![]() |
c4ce7d1a74 | ||
![]() |
7ac95b98bc | ||
![]() |
f8413d8d63 | ||
![]() |
709b80b864 | ||
![]() |
b5b68c5c42 | ||
![]() |
d58e847978 | ||
![]() |
aad9ae6997 | ||
![]() |
139cf4fae4 | ||
![]() |
e01b2da223 | ||
![]() |
cbbe2d2d3c | ||
![]() |
7ca11a96b9 | ||
![]() |
3443d6d715 | ||
![]() |
99730734a0 | ||
![]() |
20fcd28dbe | ||
![]() |
76cead72e8 | ||
![]() |
a0f17ffd1d | ||
![]() |
86d92bdfa2 | ||
![]() |
25a0bc6549 | ||
![]() |
96971e7054 | ||
![]() |
2729877fbf |
127
API.md
127
API.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## HassIO REST API
|
## HassIO REST API
|
||||||
|
|
||||||
Interface for HomeAssistant to controll things from supervisor.
|
Interface for HomeAssistant to control things from supervisor.
|
||||||
|
|
||||||
On error:
|
On error:
|
||||||
```json
|
```json
|
||||||
@@ -22,29 +22,64 @@ On success
|
|||||||
|
|
||||||
### HassIO
|
### HassIO
|
||||||
|
|
||||||
- `/supervisor/ping`
|
- GET `/supervisor/ping`
|
||||||
|
|
||||||
- `/supervisor/info`
|
- GET `/supervisor/info`
|
||||||
|
|
||||||
|
The addons from `addons` are only installed one.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"current": "CURRENT_VERSION",
|
"last_version": "LAST_VERSION",
|
||||||
"beta": "true|false",
|
"beta_channel": "true|false",
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "xy bla",
|
"name": "xy bla",
|
||||||
"slug": "xy",
|
"slug": "xy",
|
||||||
"version": "CURRENT_VERSION",
|
"repository": "12345678|null",
|
||||||
"installed": "none|INSTALL_VERSION",
|
"version": "LAST_VERSION",
|
||||||
"dedicated": "bool",
|
"installed": "INSTALL_VERSION",
|
||||||
|
"detached": "bool",
|
||||||
"description": "description"
|
"description": "description"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"addons_repositories": [
|
||||||
|
"REPO_URL"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- GET `/supervisor/addons`
|
||||||
|
|
||||||
|
Get all available addons
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"name": "xy bla",
|
||||||
|
"slug": "xy",
|
||||||
|
"repository": "core|local|REP_ID",
|
||||||
|
"version": "LAST_VERSION",
|
||||||
|
"installed": "none|INSTALL_VERSION",
|
||||||
|
"detached": "bool",
|
||||||
|
"description": "description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"slug": "12345678",
|
||||||
|
"name": "Repitory Name",
|
||||||
|
"source": "URL_OF_REPOSITORY",
|
||||||
|
"url": "null|WEBSITE",
|
||||||
|
"maintainer": "null|BLA BLU <fla@dld.ch>"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/supervisor/update`
|
- POST `/supervisor/update`
|
||||||
Optional:
|
Optional:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -52,40 +87,44 @@ Optional:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/supervisor/option`
|
- POST `/supervisor/options`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"beta": "true|false"
|
"beta_channel": "true|false",
|
||||||
|
"addons_repositories": [
|
||||||
|
"REPO_URL"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/supervisor/reload`
|
- POST `/supervisor/reload`
|
||||||
|
|
||||||
Reload addons/version.
|
Reload addons/version.
|
||||||
|
|
||||||
- `/supervisor/logs`
|
- GET `/supervisor/logs`
|
||||||
|
|
||||||
Output the raw docker log
|
Output the raw docker log
|
||||||
|
|
||||||
### Host
|
### Host
|
||||||
|
|
||||||
- `/host/shutdown`
|
- POST `/host/shutdown`
|
||||||
|
|
||||||
- `/host/reboot`
|
- POST `/host/reboot`
|
||||||
|
|
||||||
- `/host/info`
|
- GET `/host/info`
|
||||||
See HostControll info command.
|
See HostControl info command.
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"os": "",
|
"type": "",
|
||||||
"version": "",
|
"version": "",
|
||||||
"current": "",
|
"last_version": "",
|
||||||
"level": "",
|
"features": ["shutdown", "reboot", "update", "network_info", "network_control"],
|
||||||
"hostname": "",
|
"hostname": "",
|
||||||
|
"os": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/host/update`
|
- POST `/host/update`
|
||||||
Optional:
|
Optional:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -95,9 +134,9 @@ Optional:
|
|||||||
|
|
||||||
### Network
|
### Network
|
||||||
|
|
||||||
- `/network/info`
|
- GET `/network/info`
|
||||||
|
|
||||||
- `/network/options`
|
- POST `/network/options`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"hostname": "",
|
"hostname": "",
|
||||||
@@ -111,16 +150,16 @@ Optional:
|
|||||||
|
|
||||||
### HomeAssistant
|
### HomeAssistant
|
||||||
|
|
||||||
- `/homeassistant/info`
|
- GET `/homeassistant/info`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"current": "CURRENT_VERSION"
|
"last_version": "LAST_VERSION"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/homeassistant/update`
|
- POST `/homeassistant/update`
|
||||||
Optional:
|
Optional:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -128,36 +167,36 @@ Optional:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/homeassistant/logs`
|
- GET `/homeassistant/logs`
|
||||||
|
|
||||||
Output the raw docker log
|
Output the raw docker log
|
||||||
|
|
||||||
### REST API addons
|
### REST API addons
|
||||||
|
|
||||||
- `/addons/{addon}/info`
|
- GET `/addons/{addon}/info`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "VERSION",
|
"version": "VERSION",
|
||||||
"current": "CURRENT_VERSION",
|
"last_version": "LAST_VERSION",
|
||||||
"state": "started|stopped",
|
"state": "started|stopped",
|
||||||
"boot": "auto|manual",
|
"boot": "auto|manual",
|
||||||
"options": {},
|
"options": {},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/addons/{addon}/options`
|
- POST `/addons/{addon}/options`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"boot": "auto|manual",
|
"boot": "auto|manual",
|
||||||
"options": {},
|
"options": {},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/addons/{addon}/start`
|
- POST `/addons/{addon}/start`
|
||||||
|
|
||||||
- `/addons/{addon}/stop`
|
- POST `/addons/{addon}/stop`
|
||||||
|
|
||||||
- `/addons/{addon}/install`
|
- POST `/addons/{addon}/install`
|
||||||
Optional:
|
Optional:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -165,9 +204,9 @@ Optional:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/addons/{addon}/uninstall`
|
- POST `/addons/{addon}/uninstall`
|
||||||
|
|
||||||
- `/addons/{addon}/update`
|
- POST `/addons/{addon}/update`
|
||||||
Optional:
|
Optional:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -175,18 +214,18 @@ Optional:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `/addons/{addon}/logs`
|
- GET `/addons/{addon}/logs`
|
||||||
|
|
||||||
Output the raw docker log
|
Output the raw docker log
|
||||||
|
|
||||||
## Host Controll
|
## Host Control
|
||||||
|
|
||||||
Communicate over unix socket with a host daemon.
|
Communicate over unix socket with a host daemon.
|
||||||
|
|
||||||
- commands
|
- commands
|
||||||
```
|
```
|
||||||
# info
|
# info
|
||||||
-> {'os', 'version', 'current', 'level', 'hostname'}
|
-> {'type', 'version', 'last_version', 'features', 'hostname'}
|
||||||
# reboot
|
# reboot
|
||||||
# shutdown
|
# shutdown
|
||||||
# host-update [v]
|
# host-update [v]
|
||||||
@@ -200,10 +239,12 @@ Communicate over unix socket with a host daemon.
|
|||||||
# network int route xy
|
# network int route xy
|
||||||
```
|
```
|
||||||
|
|
||||||
level:
|
features:
|
||||||
- 1: power functions
|
- shutdown
|
||||||
- 2: host update
|
- reboot
|
||||||
- 4: network functions
|
- update
|
||||||
|
- network_info
|
||||||
|
- network_control
|
||||||
|
|
||||||
Answer:
|
Answer:
|
||||||
```
|
```
|
||||||
|
38
README.md
38
README.md
@@ -1,15 +1,16 @@
|
|||||||
# HassIO
|
# HassIO
|
||||||
First private cloud solution for home automation.
|
First private cloud solution for home automation.
|
||||||
|
|
||||||
It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to controll itself over UI. It have a own eco system with addons to extend the functionality in a easy way.
|
It is a docker image (supervisor) they manage HomeAssistant docker and give a interface to control itself over UI. It have a own eco system with addons to extend the functionality in a easy way.
|
||||||
|
|
||||||
[HassIO-Addons](https://github.com/pvizeli/hassio-addons) | [HassIO-Build](https://github.com/pvizeli/hassio-build)
|

|
||||||
|
|
||||||
|
[HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build)
|
||||||
|
|
||||||
**HassIO is at the moment on development and not ready to use productive!**
|
**HassIO is at the moment on development and not ready to use productive!**
|
||||||
|
|
||||||
## Feature in progress
|
## Feature in progress
|
||||||
- Backup/Restore
|
- Backup/Restore
|
||||||
- MQTT addon
|
|
||||||
- DHCP-Server addon
|
- DHCP-Server addon
|
||||||
|
|
||||||
# HomeAssistant
|
# HomeAssistant
|
||||||
@@ -23,34 +24,7 @@ http:
|
|||||||
ssl_key: /ssl/privkey.pem
|
ssl_key: /ssl/privkey.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
# Hardware Image
|
|
||||||
The image is based on ResinOS and Yocto Linux. It comes with the HassIO supervisor pre-installed. This includes support to update the supervisor over the air. After flashing your host OS will not require any more maintenance! The image does not include Home Assistant, instead it will downloaded when the image boots up for the first time.
|
|
||||||
|
|
||||||
Download can be found here: https://drive.google.com/drive/folders/0B2o1Uz6l1wVNbFJnb2gwNXJja28?usp=sharing
|
|
||||||
|
|
||||||
After extracting the archive, flash it to a drive using [Etcher](https://etcher.io/).
|
|
||||||
|
|
||||||
## History
|
|
||||||
- **0.1**: First techpreview with dumy supervisor (ResinOS 2.0.0-RC5)
|
|
||||||
- **0.2**: Fix some bugs and update it to HassIO 0.2
|
|
||||||
- **0.3**: Update HostControll and feature for HassIO 0.3 (ResinOS 2.0.0 / need reflash)
|
|
||||||
- **0.4**: Update HostControll and bring resinos OTA (resinhub) back (ResinOS 2.0.0-rev3)
|
|
||||||
|
|
||||||
## Configuring the image
|
|
||||||
You can configure the WiFi network that the image should connect to after flashing using [`resin-device-toolbox`](https://resinos.io/docs/raspberrypi3/gettingstarted/#install-resin-device-toolbox).
|
|
||||||
|
|
||||||
## Developer access to ResinOS host
|
|
||||||
Create an `authorized_keys` file in the boot partition of your SD card with your public key. After a boot it, you can acces your device as root over ssh on port 22222.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Read logoutput from supervisor:
|
|
||||||
```bash
|
|
||||||
journalctl -f -u resin-supervisor.service
|
|
||||||
docker logs homeassistant
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install on a own System
|
## Install on a own System
|
||||||
|
|
||||||
We have a installer to install HassIO on own linux device without our hardware image:
|
- Generic Linux installation: https://github.com/home-assistant/hassio-build/tree/master/install
|
||||||
https://github.com/pvizeli/hassio-build/tree/master/install
|
- Hardware Images: https://github.com/home-assistant/hassio-build/blob/master/meta-hassio/
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for HassIO addons."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
from .git import AddonsRepo
|
from .git import AddonsRepoHassIO, AddonsRepoCustom
|
||||||
from ..const import STATE_STOPPED, STATE_STARTED
|
from ..const import STATE_STOPPED, STATE_STARTED
|
||||||
from ..dock.addon import DockerAddon
|
from ..dock.addon import DockerAddon
|
||||||
|
|
||||||
@@ -21,16 +20,29 @@ class AddonManager(AddonsData):
|
|||||||
|
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.dock = dock
|
self.dock = dock
|
||||||
self.repo = AddonsRepo(config, loop)
|
self.repositories = []
|
||||||
self.dockers = {}
|
self.dockers = {}
|
||||||
|
|
||||||
async def prepare(self, arch):
|
async def prepare(self, arch):
|
||||||
"""Startup addon management."""
|
"""Startup addon management."""
|
||||||
self.arch = arch
|
self.arch = arch
|
||||||
|
|
||||||
|
# init hassio repository
|
||||||
|
self.repositories.append(AddonsRepoHassIO(self.config, self.loop))
|
||||||
|
|
||||||
|
# init custom repositories
|
||||||
|
for url in self.config.addons_repositories:
|
||||||
|
self.repositories.append(
|
||||||
|
AddonsRepoCustom(self.config, self.loop, url))
|
||||||
|
|
||||||
# load addon repository
|
# load addon repository
|
||||||
if await self.repo.load():
|
tasks = [addon.load() for addon in self.repositories]
|
||||||
self.read_addons_repo()
|
if tasks:
|
||||||
|
await asyncio.wait(tasks, loop=self.loop)
|
||||||
|
|
||||||
|
# read data from repositories
|
||||||
|
self.read_data_from_repositories()
|
||||||
|
self.merge_update_config()
|
||||||
|
|
||||||
# load installed addons
|
# load installed addons
|
||||||
for addon in self.list_installed:
|
for addon in self.list_installed:
|
||||||
@@ -38,23 +50,53 @@ class AddonManager(AddonsData):
|
|||||||
self.config, self.loop, self.dock, self, addon)
|
self.config, self.loop, self.dock, self, addon)
|
||||||
await self.dockers[addon].attach()
|
await self.dockers[addon].attach()
|
||||||
|
|
||||||
|
async def add_git_repository(self, url):
|
||||||
|
"""Add a new custom repository."""
|
||||||
|
if url in self.config.addons_repositories:
|
||||||
|
_LOGGER.warning("Repository already exists %s", url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
repo = AddonsRepoCustom(self.config, self.loop, url)
|
||||||
|
|
||||||
|
if not await repo.load():
|
||||||
|
_LOGGER.error("Can't load from repository %s", url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.config.addons_repositories = url
|
||||||
|
self.repositories.append(repo)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def drop_git_repository(self, url):
|
||||||
|
"""Remove a custom repository."""
|
||||||
|
for repo in self.repositories:
|
||||||
|
if repo.url == url:
|
||||||
|
self.repositories.remove(repo)
|
||||||
|
self.config.drop_addon_repository(url)
|
||||||
|
repo.remove()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
async def reload(self):
|
async def reload(self):
|
||||||
"""Update addons from repo and reload list."""
|
"""Update addons from repo and reload list."""
|
||||||
if not await self.repo.pull():
|
tasks = [addon.pull() for addon in self.repositories]
|
||||||
|
if not tasks:
|
||||||
return
|
return
|
||||||
self.read_addons_repo()
|
|
||||||
|
await asyncio.wait(tasks, loop=self.loop)
|
||||||
|
|
||||||
|
# read data from repositories
|
||||||
|
self.read_data_from_repositories()
|
||||||
|
self.merge_update_config()
|
||||||
|
|
||||||
# remove stalled addons
|
# remove stalled addons
|
||||||
for addon in self.list_removed:
|
for addon in self.list_detached:
|
||||||
_LOGGER.warning("Dedicated addon '%s' found!", addon)
|
_LOGGER.warning("Dedicated addon '%s' found!", addon)
|
||||||
|
|
||||||
async def auto_boot(self, start_type):
|
async def auto_boot(self, start_type):
|
||||||
"""Boot addons with mode auto."""
|
"""Boot addons with mode auto."""
|
||||||
boot_list = self.list_startup(start_type)
|
boot_list = self.list_startup(start_type)
|
||||||
tasks = []
|
tasks = [self.start(addon) for addon in boot_list]
|
||||||
|
|
||||||
for addon in boot_list:
|
|
||||||
tasks.append(self.loop.create_task(self.start(addon)))
|
|
||||||
|
|
||||||
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
|
_LOGGER.info("Startup %s run %d addons", start_type, len(tasks))
|
||||||
if tasks:
|
if tasks:
|
||||||
@@ -70,15 +112,15 @@ class AddonManager(AddonsData):
|
|||||||
_LOGGER.error("Addon %s is already installed", addon)
|
_LOGGER.error("Addon %s is already installed", addon)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not os.path.isdir(self.path_data(addon)):
|
if not self.path_data(addon).is_dir():
|
||||||
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||||
self.path_data(addon))
|
self.path_data(addon))
|
||||||
os.mkdir(self.path_data(addon))
|
self.path_data(addon).mkdir()
|
||||||
|
|
||||||
addon_docker = DockerAddon(
|
addon_docker = DockerAddon(
|
||||||
self.config, self.loop, self.dock, self, addon)
|
self.config, self.loop, self.dock, self, addon)
|
||||||
|
|
||||||
version = version or self.get_version(addon)
|
version = version or self.get_last_version(addon)
|
||||||
if not await addon_docker.install(version):
|
if not await addon_docker.install(version):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -99,10 +141,10 @@ class AddonManager(AddonsData):
|
|||||||
if not await self.dockers[addon].remove():
|
if not await self.dockers[addon].remove():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if os.path.isdir(self.path_data(addon)):
|
if self.path_data(addon).is_dir():
|
||||||
_LOGGER.info("Remove Home-Assistant addon data folder %s",
|
_LOGGER.info("Remove Home-Assistant addon data folder %s",
|
||||||
self.path_data(addon))
|
self.path_data(addon))
|
||||||
shutil.rmtree(self.path_data(addon))
|
shutil.rmtree(str(self.path_data(addon)))
|
||||||
|
|
||||||
self.dockers.pop(addon)
|
self.dockers.pop(addon)
|
||||||
self.set_addon_uninstall(addon)
|
self.set_addon_uninstall(addon)
|
||||||
@@ -144,7 +186,7 @@ class AddonManager(AddonsData):
|
|||||||
_LOGGER.error("No docker found for addon %s", addon)
|
_LOGGER.error("No docker found for addon %s", addon)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
version = version or self.get_version(addon)
|
version = version or self.get_last_version(addon)
|
||||||
is_running = self.dockers[addon].is_running()
|
is_running = self.dockers[addon].is_running()
|
||||||
|
|
||||||
# update
|
# update
|
||||||
|
@@ -1,24 +1,29 @@
|
|||||||
"""Init file for HassIO addons."""
|
"""Init file for HassIO addons."""
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import glob
|
from pathlib import Path, PurePath
|
||||||
|
|
||||||
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_CONFIG
|
from .util import extract_hash_from_path
|
||||||
|
from .validate import (
|
||||||
|
validate_options, SCHEMA_ADDON_CONFIG, SCHEMA_REPOSITORY_CONFIG)
|
||||||
from ..const import (
|
from ..const import (
|
||||||
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
FILE_HASSIO_ADDONS, ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON,
|
||||||
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
|
ATTR_STARTUP, ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, BOOT_AUTO,
|
||||||
ATTR_PORTS, BOOT_AUTO, DOCKER_REPO, ATTR_INSTALLED, ATTR_SCHEMA,
|
DOCKER_REPO, ATTR_SCHEMA, ATTR_IMAGE, MAP_CONFIG, MAP_SSL, MAP_ADDONS,
|
||||||
ATTR_IMAGE, ATTR_DEDICATED)
|
MAP_BACKUP, ATTR_REPOSITORY)
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..tools import read_json_file, write_json_file
|
from ..tools import read_json_file, write_json_file
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ADDONS_REPO_PATTERN = "{}/*/config.json"
|
SYSTEM = 'system'
|
||||||
SYSTEM = "system"
|
USER = 'user'
|
||||||
USER = "user"
|
|
||||||
|
REPOSITORY_CORE = 'core'
|
||||||
|
REPOSITORY_LOCAL = 'local'
|
||||||
|
|
||||||
|
|
||||||
class AddonsData(Config):
|
class AddonsData(Config):
|
||||||
@@ -28,79 +33,130 @@ class AddonsData(Config):
|
|||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
super().__init__(FILE_HASSIO_ADDONS)
|
super().__init__(FILE_HASSIO_ADDONS)
|
||||||
self.config = config
|
self.config = config
|
||||||
self._addons_data = self._data.get(SYSTEM, {})
|
self._system_data = self._data.get(SYSTEM, {})
|
||||||
self._user_data = self._data.get(USER, {})
|
self._user_data = self._data.get(USER, {})
|
||||||
self._current_data = {}
|
self._addons_cache = {}
|
||||||
|
self._repositories_data = {}
|
||||||
self.arch = None
|
self.arch = None
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Store data to config file."""
|
"""Store data to config file."""
|
||||||
self._data = {
|
self._data = {
|
||||||
USER: self._user_data,
|
USER: self._user_data,
|
||||||
SYSTEM: self._addons_data,
|
SYSTEM: self._system_data,
|
||||||
}
|
}
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
def read_addons_repo(self):
|
def read_data_from_repositories(self):
|
||||||
"""Read data from addons repository."""
|
"""Read data from addons repository."""
|
||||||
self._current_data = {}
|
self._addons_cache = {}
|
||||||
|
self._repositories_data = {}
|
||||||
|
|
||||||
self._read_addons_folder(self.config.path_addons_repo)
|
# read core repository
|
||||||
self._read_addons_folder(self.config.path_addons_custom)
|
self._read_addons_folder(
|
||||||
|
self.config.path_addons_core, REPOSITORY_CORE)
|
||||||
|
|
||||||
def _read_addons_folder(self, folder):
|
# read local repository
|
||||||
|
self._read_addons_folder(
|
||||||
|
self.config.path_addons_local, REPOSITORY_LOCAL)
|
||||||
|
|
||||||
|
# read custom git repositories
|
||||||
|
for repository_element in self.config.path_addons_git.iterdir():
|
||||||
|
if repository_element.is_dir():
|
||||||
|
self._read_git_repository(repository_element)
|
||||||
|
|
||||||
|
def _read_git_repository(self, path):
|
||||||
|
"""Process a custom repository folder."""
|
||||||
|
slug = extract_hash_from_path(path)
|
||||||
|
repository_info = {ATTR_SLUG: slug}
|
||||||
|
|
||||||
|
# exists repository json
|
||||||
|
repository_file = Path(path, "repository.json")
|
||||||
|
try:
|
||||||
|
repository_info.update(SCHEMA_REPOSITORY_CONFIG(
|
||||||
|
read_json_file(repository_file)
|
||||||
|
))
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
_LOGGER.warning("Can't read repository information from %s",
|
||||||
|
repository_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
except vol.Invalid:
|
||||||
|
_LOGGER.warning("Repository parse error %s", repository_file)
|
||||||
|
return
|
||||||
|
|
||||||
|
# process data
|
||||||
|
self._repositories_data[slug] = repository_info
|
||||||
|
self._read_addons_folder(path, slug)
|
||||||
|
|
||||||
|
def _read_addons_folder(self, path, repository):
|
||||||
"""Read data from addons folder."""
|
"""Read data from addons folder."""
|
||||||
pattern = ADDONS_REPO_PATTERN.format(folder)
|
for addon in path.glob("**/config.json"):
|
||||||
|
|
||||||
for addon in glob.iglob(pattern):
|
|
||||||
try:
|
try:
|
||||||
addon_config = read_json_file(addon)
|
addon_config = read_json_file(addon)
|
||||||
|
|
||||||
|
# validate
|
||||||
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
addon_config = SCHEMA_ADDON_CONFIG(addon_config)
|
||||||
self._current_data[addon_config[ATTR_SLUG]] = addon_config
|
|
||||||
|
|
||||||
except (OSError, KeyError):
|
# Generate slug
|
||||||
|
addon_slug = "{}_{}".format(
|
||||||
|
repository, addon_config[ATTR_SLUG])
|
||||||
|
|
||||||
|
# store
|
||||||
|
addon_config[ATTR_REPOSITORY] = repository
|
||||||
|
self._addons_cache[addon_slug] = addon_config
|
||||||
|
|
||||||
|
except OSError:
|
||||||
_LOGGER.warning("Can't read %s", addon)
|
_LOGGER.warning("Can't read %s", addon)
|
||||||
|
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.warning("Can't read %s -> %s", addon,
|
_LOGGER.warning("Can't read %s -> %s", addon,
|
||||||
humanize_error(addon_config, ex))
|
humanize_error(addon_config, ex))
|
||||||
|
|
||||||
|
def merge_update_config(self):
|
||||||
|
"""Update local config if they have update.
|
||||||
|
|
||||||
|
It need to be the same version as the local version is.
|
||||||
|
"""
|
||||||
|
have_change = False
|
||||||
|
|
||||||
|
for addon, data in self._system_data.items():
|
||||||
|
# detached
|
||||||
|
if addon not in self._addons_cache:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache = self._addons_cache[addon]
|
||||||
|
if data[ATTR_VERSION] == cache[ATTR_VERSION]:
|
||||||
|
if data != cache:
|
||||||
|
self._system_data[addon] = copy.deepcopy(cache)
|
||||||
|
have_change = True
|
||||||
|
|
||||||
|
if have_change:
|
||||||
|
self.save()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_installed(self):
|
def list_installed(self):
|
||||||
"""Return a list of installed addons."""
|
"""Return a list of installed addons."""
|
||||||
return set(self._addons_data.keys())
|
return set(self._system_data.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list(self):
|
def list_all(self):
|
||||||
"""Return a list of available addons."""
|
"""Return a list of all addons."""
|
||||||
data = []
|
return {
|
||||||
all_addons = {**self._addons_data, **self._current_data}
|
**self._system_data,
|
||||||
dedicated = self.list_removed
|
**self._addons_cache
|
||||||
|
}
|
||||||
for addon, values in all_addons.items():
|
|
||||||
i_version = self._user_data.get(addon, {}).get(ATTR_VERSION)
|
|
||||||
|
|
||||||
data.append({
|
|
||||||
ATTR_NAME: values[ATTR_NAME],
|
|
||||||
ATTR_SLUG: values[ATTR_SLUG],
|
|
||||||
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
|
||||||
ATTR_VERSION: values[ATTR_VERSION],
|
|
||||||
ATTR_INSTALLED: i_version,
|
|
||||||
ATTR_DEDICATED: addon in dedicated,
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def list_startup(self, start_type):
|
def list_startup(self, start_type):
|
||||||
"""Get list of installed addon with need start by type."""
|
"""Get list of installed addon with need start by type."""
|
||||||
addon_list = set()
|
addon_list = set()
|
||||||
for addon in self._addons_data.keys():
|
for addon in self._system_data.keys():
|
||||||
if self.get_boot(addon) != BOOT_AUTO:
|
if self.get_boot(addon) != BOOT_AUTO:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._addons_data[addon][ATTR_STARTUP] == start_type:
|
if self._system_data[addon][ATTR_STARTUP] == start_type:
|
||||||
addon_list.add(addon)
|
addon_list.add(addon)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.warning("Orphaned addon detect %s", addon)
|
_LOGGER.warning("Orphaned addon detect %s", addon)
|
||||||
@@ -109,33 +165,35 @@ class AddonsData(Config):
|
|||||||
return addon_list
|
return addon_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_removed(self):
|
def list_detached(self):
|
||||||
"""Return local addons they not support from repo."""
|
"""Return local addons they not support from repo."""
|
||||||
addon_list = set()
|
addon_list = set()
|
||||||
for addon in self._addons_data.keys():
|
for addon in self._system_data.keys():
|
||||||
if addon not in self._current_data:
|
if addon not in self._addons_cache:
|
||||||
addon_list.add(addon)
|
addon_list.add(addon)
|
||||||
|
|
||||||
return addon_list
|
return addon_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_repositories(self):
|
||||||
|
"""Return list of addon repositories."""
|
||||||
|
return list(self._repositories_data.values())
|
||||||
|
|
||||||
def exists_addon(self, addon):
|
def exists_addon(self, addon):
|
||||||
"""Return True if a addon exists."""
|
"""Return True if a addon exists."""
|
||||||
return addon in self._current_data or addon in self._addons_data
|
return addon in self._addons_cache or addon in self._system_data
|
||||||
|
|
||||||
def is_installed(self, addon):
|
def is_installed(self, addon):
|
||||||
"""Return True if a addon is installed."""
|
"""Return True if a addon is installed."""
|
||||||
return addon in self._addons_data
|
return addon in self._system_data
|
||||||
|
|
||||||
def version_installed(self, addon):
|
def version_installed(self, addon):
|
||||||
"""Return installed version."""
|
"""Return installed version."""
|
||||||
if ATTR_VERSION not in self._user_data[addon]:
|
return self._user_data.get(addon, {}).get(ATTR_VERSION)
|
||||||
return self._addons_data[addon][ATTR_VERSION]
|
|
||||||
|
|
||||||
return self._user_data[addon][ATTR_VERSION]
|
|
||||||
|
|
||||||
def set_addon_install(self, addon, version):
|
def set_addon_install(self, addon, version):
|
||||||
"""Set addon as installed."""
|
"""Set addon as installed."""
|
||||||
self._addons_data[addon] = self._current_data[addon]
|
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
|
||||||
self._user_data[addon] = {
|
self._user_data[addon] = {
|
||||||
ATTR_OPTIONS: {},
|
ATTR_OPTIONS: {},
|
||||||
ATTR_VERSION: version,
|
ATTR_VERSION: version,
|
||||||
@@ -144,19 +202,19 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def set_addon_uninstall(self, addon):
|
def set_addon_uninstall(self, addon):
|
||||||
"""Set addon as uninstalled."""
|
"""Set addon as uninstalled."""
|
||||||
self._addons_data.pop(addon, None)
|
self._system_data.pop(addon, None)
|
||||||
self._user_data.pop(addon, None)
|
self._user_data.pop(addon, None)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_addon_update(self, addon, version):
|
def set_addon_update(self, addon, version):
|
||||||
"""Update version of addon."""
|
"""Update version of addon."""
|
||||||
self._addons_data[addon] = self._current_data[addon]
|
self._system_data[addon] = copy.deepcopy(self._addons_cache[addon])
|
||||||
self._user_data[addon][ATTR_VERSION] = version
|
self._user_data[addon][ATTR_VERSION] = version
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_options(self, addon, options):
|
def set_options(self, addon, options):
|
||||||
"""Store user addon options."""
|
"""Store user addon options."""
|
||||||
self._user_data[addon][ATTR_OPTIONS] = options
|
self._user_data[addon][ATTR_OPTIONS] = copy.deepcopy(options)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_boot(self, addon, boot):
|
def set_boot(self, addon, boot):
|
||||||
@@ -167,7 +225,7 @@ class AddonsData(Config):
|
|||||||
def get_options(self, addon):
|
def get_options(self, addon):
|
||||||
"""Return options with local changes."""
|
"""Return options with local changes."""
|
||||||
return {
|
return {
|
||||||
**self._addons_data[addon][ATTR_OPTIONS],
|
**self._system_data[addon][ATTR_OPTIONS],
|
||||||
**self._user_data[addon][ATTR_OPTIONS],
|
**self._user_data[addon][ATTR_OPTIONS],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,54 +234,64 @@ class AddonsData(Config):
|
|||||||
if ATTR_BOOT in self._user_data[addon]:
|
if ATTR_BOOT in self._user_data[addon]:
|
||||||
return self._user_data[addon][ATTR_BOOT]
|
return self._user_data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
return self._addons_data[addon][ATTR_BOOT]
|
return self._system_data[addon][ATTR_BOOT]
|
||||||
|
|
||||||
def get_name(self, addon):
|
def get_name(self, addon):
|
||||||
"""Return name of addon."""
|
"""Return name of addon."""
|
||||||
return self._addons_data[addon][ATTR_NAME]
|
return self._system_data[addon][ATTR_NAME]
|
||||||
|
|
||||||
def get_description(self, addon):
|
def get_description(self, addon):
|
||||||
"""Return description of addon."""
|
"""Return description of addon."""
|
||||||
return self._addons_data[addon][ATTR_DESCRIPTON]
|
return self._system_data[addon][ATTR_DESCRIPTON]
|
||||||
|
|
||||||
def get_version(self, addon):
|
def get_last_version(self, addon):
|
||||||
"""Return version of addon."""
|
"""Return version of addon."""
|
||||||
if addon not in self._current_data:
|
if addon not in self._addons_cache:
|
||||||
return self.version_installed(addon)
|
return self.version_installed(addon)
|
||||||
return self._current_data[addon][ATTR_VERSION]
|
return self._addons_cache[addon][ATTR_VERSION]
|
||||||
|
|
||||||
def get_ports(self, addon):
|
def get_ports(self, addon):
|
||||||
"""Return ports of addon."""
|
"""Return ports of addon."""
|
||||||
return self._addons_data[addon].get(ATTR_PORTS)
|
return self._system_data[addon].get(ATTR_PORTS)
|
||||||
|
|
||||||
def get_image(self, addon):
|
def get_image(self, addon):
|
||||||
"""Return image name of addon."""
|
"""Return image name of addon."""
|
||||||
addon_data = self._addons_data.get(addon, self._current_data[addon])
|
addon_data = self._system_data.get(
|
||||||
|
addon, self._addons_cache.get(addon))
|
||||||
|
|
||||||
if ATTR_IMAGE not in addon_data:
|
if ATTR_IMAGE not in addon_data:
|
||||||
return "{}/{}-addon-{}".format(DOCKER_REPO, self.arch, addon)
|
return "{}/{}-addon-{}".format(
|
||||||
|
DOCKER_REPO, self.arch, addon_data[ATTR_SLUG])
|
||||||
|
|
||||||
return addon_data[ATTR_IMAGE]
|
return addon_data[ATTR_IMAGE].format(arch=self.arch)
|
||||||
|
|
||||||
def need_config(self, addon):
|
def map_config(self, addon):
|
||||||
"""Return True if config map is needed."""
|
"""Return True if config map is needed."""
|
||||||
return self._addons_data[addon][ATTR_MAP_CONFIG]
|
return MAP_CONFIG in self._system_data[addon][ATTR_MAP]
|
||||||
|
|
||||||
def need_ssl(self, addon):
|
def map_ssl(self, addon):
|
||||||
"""Return True if ssl map is needed."""
|
"""Return True if ssl map is needed."""
|
||||||
return self._addons_data[addon][ATTR_MAP_SSL]
|
return MAP_SSL in self._system_data[addon][ATTR_MAP]
|
||||||
|
|
||||||
|
def map_addons(self, addon):
|
||||||
|
"""Return True if addons map is needed."""
|
||||||
|
return MAP_ADDONS in self._system_data[addon][ATTR_MAP]
|
||||||
|
|
||||||
|
def map_backup(self, addon):
|
||||||
|
"""Return True if backup map is needed."""
|
||||||
|
return MAP_BACKUP in self._system_data[addon][ATTR_MAP]
|
||||||
|
|
||||||
def path_data(self, addon):
|
def path_data(self, addon):
|
||||||
"""Return addon data path inside supervisor."""
|
"""Return addon data path inside supervisor."""
|
||||||
return "{}/{}".format(self.config.path_addons_data, addon)
|
return Path(self.config.path_addons_data, addon)
|
||||||
|
|
||||||
def path_data_docker(self, addon):
|
def path_extern_data(self, addon):
|
||||||
"""Return addon data path external for docker."""
|
"""Return addon data path external for docker."""
|
||||||
return "{}/{}".format(self.config.path_addons_data_docker, addon)
|
return str(PurePath(self.config.path_extern_addons_data, addon))
|
||||||
|
|
||||||
def path_addon_options(self, addon):
|
def path_addon_options(self, addon):
|
||||||
"""Return path to addons options."""
|
"""Return path to addons options."""
|
||||||
return "{}/options.json".format(self.path_data(addon))
|
return Path(self.path_data(addon), "options.json")
|
||||||
|
|
||||||
def write_addon_options(self, addon):
|
def write_addon_options(self, addon):
|
||||||
"""Return True if addon options is written to data."""
|
"""Return True if addon options is written to data."""
|
||||||
@@ -241,7 +309,7 @@ class AddonsData(Config):
|
|||||||
|
|
||||||
def get_schema(self, addon):
|
def get_schema(self, addon):
|
||||||
"""Create a schema for addon options."""
|
"""Create a schema for addon options."""
|
||||||
raw_schema = self._addons_data[addon][ATTR_SCHEMA]
|
raw_schema = self._system_data[addon][ATTR_SCHEMA]
|
||||||
|
|
||||||
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
schema = vol.Schema(vol.All(dict, validate_options(raw_schema)))
|
||||||
return schema
|
return schema
|
||||||
|
@@ -1,10 +1,12 @@
|
|||||||
"""Init file for HassIO addons git."""
|
"""Init file for HassIO addons git."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
import git
|
import git
|
||||||
|
|
||||||
|
from .util import get_hash_from_repository
|
||||||
from ..const import URL_HASSIO_ADDONS
|
from ..const import URL_HASSIO_ADDONS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -13,26 +15,29 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class AddonsRepo(object):
|
class AddonsRepo(object):
|
||||||
"""Manage addons git repo."""
|
"""Manage addons git repo."""
|
||||||
|
|
||||||
def __init__(self, config, loop):
|
def __init__(self, config, loop, path, url):
|
||||||
"""Initialize docker base wrapper."""
|
"""Initialize git base wrapper."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.repo = None
|
self.repo = None
|
||||||
|
self.path = path
|
||||||
|
self.url = url
|
||||||
self._lock = asyncio.Lock(loop=loop)
|
self._lock = asyncio.Lock(loop=loop)
|
||||||
|
|
||||||
async def load(self):
|
async def load(self):
|
||||||
"""Init git addon repo."""
|
"""Init git addon repo."""
|
||||||
if not os.path.isdir(self.config.path_addons_repo):
|
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 addons repository")
|
_LOGGER.info("Load addon %s repository", self.path)
|
||||||
self.repo = await self.loop.run_in_executor(
|
self.repo = await self.loop.run_in_executor(
|
||||||
None, git.Repo, self.config.path_addons_repo)
|
None, git.Repo, str(self.path))
|
||||||
|
|
||||||
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
|
||||||
_LOGGER.error("Can't load addons repo: %s.", err)
|
git.GitCommandError) as err:
|
||||||
|
_LOGGER.error("Can't load %s repo: %s.", self.path, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -41,13 +46,13 @@ class AddonsRepo(object):
|
|||||||
"""Clone git addon repo."""
|
"""Clone git addon repo."""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Clone addons repository")
|
_LOGGER.info("Clone addon %s repository", self.url)
|
||||||
self.repo = await self.loop.run_in_executor(
|
self.repo = await self.loop.run_in_executor(
|
||||||
None, git.Repo.clone_from, URL_HASSIO_ADDONS,
|
None, git.Repo.clone_from, self.url, str(self.path))
|
||||||
self.config.path_addons_repo)
|
|
||||||
|
|
||||||
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
|
||||||
_LOGGER.error("Can't clone addons repo: %s.", err)
|
git.GitCommandError) as err:
|
||||||
|
_LOGGER.error("Can't clone %s repo: %s.", self.url, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -60,12 +65,43 @@ class AddonsRepo(object):
|
|||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Pull addons repository")
|
_LOGGER.info("Pull addon %s repository", self.url)
|
||||||
await self.loop.run_in_executor(
|
await self.loop.run_in_executor(
|
||||||
None, self.repo.remotes.origin.pull)
|
None, self.repo.remotes.origin.pull)
|
||||||
|
|
||||||
except (git.InvalidGitRepositoryError, git.NoSuchPathError) as err:
|
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
|
||||||
_LOGGER.error("Can't pull addons repo: %s.", err)
|
git.exc.GitCommandError) as err:
|
||||||
|
_LOGGER.error("Can't pull %s repo: %s.", self.url, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class AddonsRepoHassIO(AddonsRepo):
|
||||||
|
"""HassIO addons repository."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop):
|
||||||
|
"""Initialize git hassio addon repository."""
|
||||||
|
super().__init__(
|
||||||
|
config, loop, config.path_addons_core, URL_HASSIO_ADDONS)
|
||||||
|
|
||||||
|
|
||||||
|
class AddonsRepoCustom(AddonsRepo):
|
||||||
|
"""Custom addons repository."""
|
||||||
|
|
||||||
|
def __init__(self, config, loop, url):
|
||||||
|
"""Initialize git hassio addon repository."""
|
||||||
|
path = Path(config.path_addons_git, get_hash_from_repository(url))
|
||||||
|
|
||||||
|
super().__init__(config, loop, path, url)
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
"""Remove a custom addon."""
|
||||||
|
if self.path.is_dir():
|
||||||
|
_LOGGER.info("Remove custom addon repository %s", self.url)
|
||||||
|
|
||||||
|
def log_err(funct, path, _):
|
||||||
|
"""Log error."""
|
||||||
|
_LOGGER.warning("Can't remove %s", path)
|
||||||
|
|
||||||
|
shutil.rmtree(str(self.path), onerror=log_err)
|
||||||
|
26
hassio/addons/util.py
Normal file
26
hassio/addons/util.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""Util addons functions."""
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
RE_SLUGIFY = re.compile(r'[^a-z0-9_]+')
|
||||||
|
RE_SHA1 = re.compile(r"[a-f0-9]{8}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_hash_from_repository(name):
|
||||||
|
"""Generate a hash from repository."""
|
||||||
|
key = name.lower().encode()
|
||||||
|
return hashlib.sha1(key).hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_hash_from_path(path):
|
||||||
|
"""Extract repo id from path."""
|
||||||
|
repo_dir = path.parts[-1]
|
||||||
|
|
||||||
|
if not RE_SHA1.match(repo_dir):
|
||||||
|
return get_hash_from_repository(repo_dir)
|
||||||
|
return repo_dir
|
||||||
|
|
||||||
|
|
||||||
|
def create_hash_index_list(name_list):
|
||||||
|
"""Create a dict with hash from repositories list."""
|
||||||
|
return {get_hash_from_repository(repo): repo for repo in name_list}
|
@@ -3,9 +3,9 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
|
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
|
||||||
ATTR_BOOT, ATTR_MAP_SSL, ATTR_MAP_CONFIG, ATTR_OPTIONS,
|
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER,
|
||||||
ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER, STARTUP_BEFORE, BOOT_AUTO,
|
STARTUP_BEFORE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, MAP_SSL,
|
||||||
BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE)
|
MAP_CONFIG, MAP_ADDONS, MAP_BACKUP, ATTR_URL, ATTR_MAINTAINER)
|
||||||
|
|
||||||
V_STR = 'str'
|
V_STR = 'str'
|
||||||
V_INT = 'int'
|
V_INT = 'int'
|
||||||
@@ -27,8 +27,9 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Required(ATTR_BOOT):
|
vol.Required(ATTR_BOOT):
|
||||||
vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
vol.Optional(ATTR_PORTS): dict,
|
vol.Optional(ATTR_PORTS): dict,
|
||||||
vol.Optional(ATTR_MAP_CONFIG, default=False): vol.Boolean(),
|
vol.Optional(ATTR_MAP, default=[]): [
|
||||||
vol.Optional(ATTR_MAP_SSL, default=False): vol.Boolean(),
|
vol.In([MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP])
|
||||||
|
],
|
||||||
vol.Required(ATTR_OPTIONS): dict,
|
vol.Required(ATTR_OPTIONS): dict,
|
||||||
vol.Required(ATTR_SCHEMA): {
|
vol.Required(ATTR_SCHEMA): {
|
||||||
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
|
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
|
||||||
@@ -36,7 +37,15 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
])
|
])
|
||||||
},
|
},
|
||||||
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
|
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
|
||||||
})
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_REPOSITORY_CONFIG = vol.Schema({
|
||||||
|
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
|
vol.Optional(ATTR_MAINTAINER): vol.Coerce(str),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def validate_options(raw_schema):
|
def validate_options(raw_schema):
|
||||||
@@ -71,6 +80,10 @@ def validate_options(raw_schema):
|
|||||||
def _single_validate(typ, value):
|
def _single_validate(typ, value):
|
||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
try:
|
try:
|
||||||
|
# if required argument
|
||||||
|
if value is None:
|
||||||
|
raise vol.Invalid("A required argument is not set!")
|
||||||
|
|
||||||
if typ == V_STR:
|
if typ == V_STR:
|
||||||
return str(value)
|
return str(value)
|
||||||
elif typ == V_INT:
|
elif typ == V_INT:
|
||||||
@@ -85,7 +98,7 @@ def _single_validate(typ, value):
|
|||||||
return vol.Url()(value)
|
return vol.Url()(value)
|
||||||
|
|
||||||
raise vol.Invalid("Fatal error for {}.".format(value))
|
raise vol.Invalid("Fatal error for {}.".format(value))
|
||||||
except TypeError:
|
except ValueError:
|
||||||
raise vol.Invalid(
|
raise vol.Invalid(
|
||||||
"Type {} error for {}.".format(typ, value)) from None
|
"Type {} error for {}.".format(typ, value)) from None
|
||||||
|
|
||||||
|
@@ -25,32 +25,36 @@ class RestAPI(object):
|
|||||||
self._handler = None
|
self._handler = None
|
||||||
self.server = None
|
self.server = None
|
||||||
|
|
||||||
def register_host(self, host_controll):
|
def register_host(self, host_control):
|
||||||
"""Register hostcontroll function."""
|
"""Register hostcontrol function."""
|
||||||
api_host = APIHost(self.config, self.loop, host_controll)
|
api_host = APIHost(self.config, self.loop, host_control)
|
||||||
|
|
||||||
self.webapp.router.add_get('/host/info', api_host.info)
|
self.webapp.router.add_get('/host/info', api_host.info)
|
||||||
self.webapp.router.add_get('/host/reboot', api_host.reboot)
|
self.webapp.router.add_post('/host/reboot', api_host.reboot)
|
||||||
self.webapp.router.add_get('/host/shutdown', api_host.shutdown)
|
self.webapp.router.add_post('/host/shutdown', api_host.shutdown)
|
||||||
self.webapp.router.add_get('/host/update', api_host.update)
|
self.webapp.router.add_post('/host/update', api_host.update)
|
||||||
|
|
||||||
def register_network(self, host_controll):
|
def register_network(self, host_control):
|
||||||
"""Register network function."""
|
"""Register network function."""
|
||||||
api_net = APINetwork(self.config, self.loop, host_controll)
|
api_net = APINetwork(self.config, self.loop, host_control)
|
||||||
|
|
||||||
self.webapp.router.add_get('/network/info', api_net.info)
|
self.webapp.router.add_get('/network/info', api_net.info)
|
||||||
self.webapp.router.add_get('/network/options', api_net.options)
|
self.webapp.router.add_post('/network/options', api_net.options)
|
||||||
|
|
||||||
def register_supervisor(self, supervisor, addons):
|
def register_supervisor(self, supervisor, addons, host_control):
|
||||||
"""Register supervisor function."""
|
"""Register supervisor function."""
|
||||||
api_supervisor = APISupervisor(
|
api_supervisor = APISupervisor(
|
||||||
self.config, self.loop, supervisor, addons)
|
self.config, self.loop, supervisor, addons, host_control)
|
||||||
|
|
||||||
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
|
self.webapp.router.add_get('/supervisor/ping', api_supervisor.ping)
|
||||||
self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
|
self.webapp.router.add_get('/supervisor/info', api_supervisor.info)
|
||||||
self.webapp.router.add_get('/supervisor/update', api_supervisor.update)
|
|
||||||
self.webapp.router.add_get('/supervisor/reload', api_supervisor.reload)
|
|
||||||
self.webapp.router.add_get(
|
self.webapp.router.add_get(
|
||||||
|
'/supervisor/addons', api_supervisor.available_addons)
|
||||||
|
self.webapp.router.add_post(
|
||||||
|
'/supervisor/update', api_supervisor.update)
|
||||||
|
self.webapp.router.add_post(
|
||||||
|
'/supervisor/reload', api_supervisor.reload)
|
||||||
|
self.webapp.router.add_post(
|
||||||
'/supervisor/options', api_supervisor.options)
|
'/supervisor/options', api_supervisor.options)
|
||||||
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
|
self.webapp.router.add_get('/supervisor/logs', api_supervisor.logs)
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@ class RestAPI(object):
|
|||||||
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
|
api_hass = APIHomeAssistant(self.config, self.loop, dock_homeassistant)
|
||||||
|
|
||||||
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
self.webapp.router.add_get('/homeassistant/info', api_hass.info)
|
||||||
self.webapp.router.add_get('/homeassistant/update', api_hass.update)
|
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
|
||||||
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
|
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
|
||||||
|
|
||||||
def register_addons(self, addons):
|
def register_addons(self, addons):
|
||||||
@@ -67,14 +71,15 @@ class RestAPI(object):
|
|||||||
api_addons = APIAddons(self.config, self.loop, addons)
|
api_addons = APIAddons(self.config, self.loop, addons)
|
||||||
|
|
||||||
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
|
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
|
||||||
self.webapp.router.add_get(
|
self.webapp.router.add_post(
|
||||||
'/addons/{addon}/install', api_addons.install)
|
'/addons/{addon}/install', api_addons.install)
|
||||||
self.webapp.router.add_get(
|
self.webapp.router.add_post(
|
||||||
'/addons/{addon}/uninstall', api_addons.uninstall)
|
'/addons/{addon}/uninstall', api_addons.uninstall)
|
||||||
self.webapp.router.add_get('/addons/{addon}/start', api_addons.start)
|
self.webapp.router.add_post('/addons/{addon}/start', api_addons.start)
|
||||||
self.webapp.router.add_get('/addons/{addon}/stop', api_addons.stop)
|
self.webapp.router.add_post('/addons/{addon}/stop', api_addons.stop)
|
||||||
self.webapp.router.add_get('/addons/{addon}/update', api_addons.update)
|
self.webapp.router.add_post(
|
||||||
self.webapp.router.add_get(
|
'/addons/{addon}/update', api_addons.update)
|
||||||
|
self.webapp.router.add_post(
|
||||||
'/addons/{addon}/options', api_addons.options)
|
'/addons/{addon}/options', api_addons.options)
|
||||||
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
|
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@ from voluptuous.humanize import humanize_error
|
|||||||
|
|
||||||
from .util import api_process, api_process_raw, api_validate
|
from .util import api_process, api_process_raw, api_validate
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION, ATTR_CURRENT, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
|
||||||
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
STATE_STOPPED, STATE_STARTED, BOOT_AUTO, BOOT_MANUAL)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -47,14 +47,13 @@ class APIAddons(object):
|
|||||||
"""Return addon information."""
|
"""Return addon information."""
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
|
|
||||||
info = {
|
return {
|
||||||
ATTR_VERSION: self.addons.version_installed(addon),
|
ATTR_VERSION: self.addons.version_installed(addon),
|
||||||
ATTR_CURRENT: self.addons.get_version(addon),
|
ATTR_LAST_VERSION: self.addons.get_last_version(addon),
|
||||||
ATTR_STATE: await self.addons.state(addon),
|
ATTR_STATE: await self.addons.state(addon),
|
||||||
ATTR_BOOT: self.addons.get_boot(addon),
|
ATTR_BOOT: self.addons.get_boot(addon),
|
||||||
ATTR_OPTIONS: self.addons.get_options(addon),
|
ATTR_OPTIONS: self.addons.get_options(addon),
|
||||||
}
|
}
|
||||||
return info
|
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options(self, request):
|
async def options(self, request):
|
||||||
@@ -66,12 +65,12 @@ class APIAddons(object):
|
|||||||
vol.Optional(ATTR_OPTIONS): options_schema,
|
vol.Optional(ATTR_OPTIONS): options_schema,
|
||||||
})
|
})
|
||||||
|
|
||||||
addon_config = await api_validate(addon_schema, request)
|
body = await api_validate(addon_schema, request)
|
||||||
|
|
||||||
if ATTR_OPTIONS in addon_config:
|
if ATTR_OPTIONS in body:
|
||||||
self.addons.set_options(addon, addon_config[ATTR_OPTIONS])
|
self.addons.set_options(addon, body[ATTR_OPTIONS])
|
||||||
if ATTR_BOOT in addon_config:
|
if ATTR_BOOT in body:
|
||||||
self.addons.set_options(addon, addon_config[ATTR_BOOT])
|
self.addons.set_boot(addon, body[ATTR_BOOT])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ class APIAddons(object):
|
|||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
addon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request, check_installed=False)
|
||||||
version = body.get(
|
version = body.get(
|
||||||
ATTR_VERSION, self.addons.get_version(addon))
|
ATTR_VERSION, self.addons.get_last_version(addon))
|
||||||
|
|
||||||
return await asyncio.shield(
|
return await asyncio.shield(
|
||||||
self.addons.install(addon, version), loop=self.loop)
|
self.addons.install(addon, version), loop=self.loop)
|
||||||
@@ -130,7 +129,7 @@ class APIAddons(object):
|
|||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
addon = self._extract_addon(request)
|
addon = self._extract_addon(request)
|
||||||
version = body.get(
|
version = body.get(
|
||||||
ATTR_VERSION, self.addons.get_version(addon))
|
ATTR_VERSION, self.addons.get_last_version(addon))
|
||||||
|
|
||||||
if version == self.addons.version_installed(addon):
|
if version == self.addons.version_installed(addon):
|
||||||
raise RuntimeError("Version is already in use")
|
raise RuntimeError("Version is already in use")
|
||||||
|
@@ -5,7 +5,7 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .util import api_process, api_process_raw, api_validate
|
from .util import api_process, api_process_raw, api_validate
|
||||||
from ..const import ATTR_VERSION, ATTR_CURRENT
|
from ..const import ATTR_VERSION, ATTR_LAST_VERSION
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class APIHomeAssistant(object):
|
|||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
info = {
|
info = {
|
||||||
ATTR_VERSION: self.homeassistant.version,
|
ATTR_VERSION: self.homeassistant.version,
|
||||||
ATTR_CURRENT: self.config.current_homeassistant,
|
ATTR_LAST_VERSION: self.config.last_homeassistant,
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
@@ -37,7 +37,7 @@ class APIHomeAssistant(object):
|
|||||||
async def update(self, request):
|
async def update(self, request):
|
||||||
"""Update host OS."""
|
"""Update host OS."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.config.current_homeassistant)
|
version = body.get(ATTR_VERSION, self.config.last_homeassistant)
|
||||||
|
|
||||||
if self.homeassistant.in_progress:
|
if self.homeassistant.in_progress:
|
||||||
raise RuntimeError("Other task is in progress")
|
raise RuntimeError("Other task is in progress")
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
"""Init file for HassIO host rest api."""
|
"""Init file for HassIO host rest api."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .util import api_process_hostcontroll, api_process, api_validate
|
from .util import api_process_hostcontrol, api_process, api_validate
|
||||||
from ..const import ATTR_VERSION
|
from ..const import (
|
||||||
|
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
|
||||||
|
ATTR_OS)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
UNKNOWN = 'unknown'
|
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({
|
SCHEMA_VERSION = vol.Schema({
|
||||||
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
vol.Optional(ATTR_VERSION): vol.Coerce(str),
|
||||||
})
|
})
|
||||||
@@ -18,44 +19,42 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
class APIHost(object):
|
class APIHost(object):
|
||||||
"""Handle rest api for host functions."""
|
"""Handle rest api for host functions."""
|
||||||
|
|
||||||
def __init__(self, config, loop, host_controll):
|
def __init__(self, config, loop, host_control):
|
||||||
"""Initialize host rest api part."""
|
"""Initialize host rest api part."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.host_controll = host_controll
|
self.host_control = host_control
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
if not self.host_controll.active:
|
return {
|
||||||
info = {
|
ATTR_TYPE: self.host_control.type,
|
||||||
'os': UNKNOWN,
|
ATTR_VERSION: self.host_control.version,
|
||||||
'version': UNKNOWN,
|
ATTR_LAST_VERSION: self.host_control.last_version,
|
||||||
'current': UNKNOWN,
|
ATTR_FEATURES: self.host_control.features,
|
||||||
'level': 0,
|
ATTR_HOSTNAME: self.host_control.hostname,
|
||||||
'hostname': UNKNOWN,
|
ATTR_OS: self.host_control.os_info,
|
||||||
}
|
}
|
||||||
return info
|
|
||||||
|
|
||||||
return await self.host_controll.info()
|
@api_process_hostcontrol
|
||||||
|
|
||||||
@api_process_hostcontroll
|
|
||||||
def reboot(self, request):
|
def reboot(self, request):
|
||||||
"""Reboot host."""
|
"""Reboot host."""
|
||||||
return self.host_controll.reboot()
|
return self.host_control.reboot()
|
||||||
|
|
||||||
@api_process_hostcontroll
|
@api_process_hostcontrol
|
||||||
def shutdown(self, request):
|
def shutdown(self, request):
|
||||||
"""Poweroff host."""
|
"""Poweroff host."""
|
||||||
return self.host_controll.shutdown()
|
return self.host_control.shutdown()
|
||||||
|
|
||||||
@api_process_hostcontroll
|
@api_process_hostcontrol
|
||||||
async def update(self, request):
|
async def update(self, request):
|
||||||
"""Update host OS."""
|
"""Update host OS."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION)
|
version = body.get(ATTR_VERSION, self.host_control.last_version)
|
||||||
|
|
||||||
if version == self.host_controll.version:
|
if version == self.host_control.version:
|
||||||
raise RuntimeError("Version is already in use")
|
raise RuntimeError("Version is already in use")
|
||||||
|
|
||||||
return await self.host_controll.host_update(version=version)
|
return await asyncio.shield(
|
||||||
|
self.host_control.update(version=version), loop=self.loop)
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Init file for HassIO network rest api."""
|
"""Init file for HassIO network rest api."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .util import api_process_hostcontroll
|
from .util import api_process_hostcontrol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -9,18 +9,18 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class APINetwork(object):
|
class APINetwork(object):
|
||||||
"""Handle rest api for network functions."""
|
"""Handle rest api for network functions."""
|
||||||
|
|
||||||
def __init__(self, config, loop, host_controll):
|
def __init__(self, config, loop, host_control):
|
||||||
"""Initialize network rest api part."""
|
"""Initialize network rest api part."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.host_controll = host_controll
|
self.host_control = host_control
|
||||||
|
|
||||||
@api_process_hostcontroll
|
@api_process_hostcontrol
|
||||||
def info(self, request):
|
def info(self, request):
|
||||||
"""Show network settings."""
|
"""Show network settings."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@api_process_hostcontroll
|
@api_process_hostcontrol
|
||||||
def options(self, request):
|
def options(self, request):
|
||||||
"""Edit network settings."""
|
"""Edit network settings."""
|
||||||
pass
|
pass
|
||||||
|
@@ -5,14 +5,19 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .util import api_process, api_process_raw, api_validate
|
from .util import api_process, api_process_raw, api_validate
|
||||||
|
from ..addons.util import create_hash_index_list
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDONS, ATTR_VERSION, ATTR_CURRENT, ATTR_BETA, HASSIO_VERSION)
|
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL,
|
||||||
|
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_REPOSITORIES,
|
||||||
|
ATTR_REPOSITORY, ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED,
|
||||||
|
ATTR_DETACHED, ATTR_SOURCE, ATTR_MAINTAINER, ATTR_URL)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema({
|
SCHEMA_OPTIONS = vol.Schema({
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
vol.Optional(ATTR_BETA): vol.Boolean(),
|
vol.Optional(ATTR_BETA_CHANNEL): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_ADDONS_REPOSITORIES): [vol.Url()],
|
||||||
})
|
})
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({
|
SCHEMA_VERSION = vol.Schema({
|
||||||
@@ -23,12 +28,49 @@ SCHEMA_VERSION = vol.Schema({
|
|||||||
class APISupervisor(object):
|
class APISupervisor(object):
|
||||||
"""Handle rest api for supervisor functions."""
|
"""Handle rest api for supervisor functions."""
|
||||||
|
|
||||||
def __init__(self, config, loop, supervisor, addons):
|
def __init__(self, config, loop, supervisor, addons, host_control):
|
||||||
"""Initialize supervisor rest api part."""
|
"""Initialize supervisor rest api part."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.supervisor = supervisor
|
self.supervisor = supervisor
|
||||||
self.addons = addons
|
self.addons = addons
|
||||||
|
self.host_control = host_control
|
||||||
|
|
||||||
|
def _addons_list(self, only_installed):
|
||||||
|
"""Return a list of addons."""
|
||||||
|
data = []
|
||||||
|
detached = self.addons.list_detached
|
||||||
|
|
||||||
|
for addon, values in self.addons.list_all.items():
|
||||||
|
i_version = self.addons.version_installed(addon)
|
||||||
|
|
||||||
|
data.append({
|
||||||
|
ATTR_NAME: values[ATTR_NAME],
|
||||||
|
ATTR_SLUG: addon,
|
||||||
|
ATTR_DESCRIPTON: values[ATTR_DESCRIPTON],
|
||||||
|
ATTR_VERSION: values[ATTR_VERSION],
|
||||||
|
ATTR_INSTALLED: i_version,
|
||||||
|
ATTR_DETACHED: addon in detached,
|
||||||
|
ATTR_REPOSITORY: values[ATTR_REPOSITORY],
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _repositories_list(self):
|
||||||
|
"""Return a list of addons repositories."""
|
||||||
|
data = []
|
||||||
|
list_id = create_hash_index_list(self.config.addons_repositories)
|
||||||
|
|
||||||
|
for repository in self.addons.list_repositories:
|
||||||
|
data.append({
|
||||||
|
ATTR_SLUG: repository[ATTR_SLUG],
|
||||||
|
ATTR_NAME: repository[ATTR_NAME],
|
||||||
|
ATTR_SOURCE: list_id.get(repository[ATTR_SLUG]),
|
||||||
|
ATTR_URL: repository.get(ATTR_URL),
|
||||||
|
ATTR_MAINTAINER: repository.get(ATTR_MAINTAINER),
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def ping(self, request):
|
async def ping(self, request):
|
||||||
@@ -38,29 +80,55 @@ class APISupervisor(object):
|
|||||||
@api_process
|
@api_process
|
||||||
async def info(self, request):
|
async def info(self, request):
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
info = {
|
return {
|
||||||
ATTR_VERSION: HASSIO_VERSION,
|
ATTR_VERSION: HASSIO_VERSION,
|
||||||
ATTR_CURRENT: self.config.current_hassio,
|
ATTR_LAST_VERSION: self.config.last_hassio,
|
||||||
ATTR_BETA: self.config.upstream_beta,
|
ATTR_BETA_CHANNEL: self.config.upstream_beta,
|
||||||
ATTR_ADDONS: self.addons.list,
|
ATTR_ADDONS: self._addons_list(only_installed=True),
|
||||||
|
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def available_addons(self, request):
|
||||||
|
"""Return information for all available addons."""
|
||||||
|
return {
|
||||||
|
ATTR_ADDONS: self._addons_list(only_installed=False),
|
||||||
|
ATTR_REPOSITORIES: self._repositories_list(),
|
||||||
}
|
}
|
||||||
return info
|
|
||||||
|
|
||||||
@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_BETA in body:
|
if ATTR_BETA_CHANNEL in body:
|
||||||
self.config.upstream_beta = body[ATTR_BETA]
|
self.config.upstream_beta = body[ATTR_BETA_CHANNEL]
|
||||||
|
|
||||||
return self.config.save()
|
if ATTR_ADDONS_REPOSITORIES in body:
|
||||||
|
new = set(body[ATTR_ADDONS_REPOSITORIES])
|
||||||
|
old = set(self.config.addons_repositories)
|
||||||
|
|
||||||
|
# add new repositories
|
||||||
|
tasks = [self.addons.add_git_repository(url) for url in
|
||||||
|
set(new - old)]
|
||||||
|
if tasks:
|
||||||
|
await asyncio.shield(
|
||||||
|
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
|
||||||
|
|
||||||
|
# remove old repositories
|
||||||
|
for url in set(old - new):
|
||||||
|
self.addons.drop_git_repository(url)
|
||||||
|
|
||||||
|
# read repository
|
||||||
|
self.addons.read_data_from_repositories()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
@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.config.current_hassio)
|
version = body.get(ATTR_VERSION, self.config.last_hassio)
|
||||||
|
|
||||||
if version == self.supervisor.version:
|
if version == self.supervisor.version:
|
||||||
raise RuntimeError("Version is already in use")
|
raise RuntimeError("Version is already in use")
|
||||||
@@ -71,7 +139,10 @@ class APISupervisor(object):
|
|||||||
@api_process
|
@api_process
|
||||||
async def reload(self, request):
|
async def reload(self, request):
|
||||||
"""Reload addons, config ect."""
|
"""Reload addons, config ect."""
|
||||||
tasks = [self.addons.reload(), self.config.fetch_update_infos()]
|
tasks = [
|
||||||
|
self.addons.reload(), self.config.fetch_update_infos(),
|
||||||
|
self.host_control.load()
|
||||||
|
]
|
||||||
results, _ = await asyncio.shield(
|
results, _ = await asyncio.shield(
|
||||||
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
|
asyncio.wait(tasks, loop=self.loop), loop=self.loop)
|
||||||
|
|
||||||
|
@@ -39,11 +39,11 @@ def api_process(method):
|
|||||||
return wrap_api
|
return wrap_api
|
||||||
|
|
||||||
|
|
||||||
def api_process_hostcontroll(method):
|
def api_process_hostcontrol(method):
|
||||||
"""Wrap HostControll calls to rest api."""
|
"""Wrap HostControl calls to rest api."""
|
||||||
async def wrap_hostcontroll(api, *args, **kwargs):
|
async def wrap_hostcontrol(api, *args, **kwargs):
|
||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
if not api.host_controll.active:
|
if not api.host_control.active:
|
||||||
raise HTTPServiceUnavailable()
|
raise HTTPServiceUnavailable()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -59,7 +59,7 @@ def api_process_hostcontroll(method):
|
|||||||
return api_return_ok()
|
return api_return_ok()
|
||||||
return api_return_error()
|
return api_return_error()
|
||||||
|
|
||||||
return wrap_hostcontroll
|
return wrap_hostcontrol
|
||||||
|
|
||||||
|
|
||||||
def api_process_raw(method):
|
def api_process_raw(method):
|
||||||
@@ -81,7 +81,7 @@ def api_return_error(message=None):
|
|||||||
return web.json_response({
|
return web.json_response({
|
||||||
JSON_RESULT: RESULT_ERROR,
|
JSON_RESULT: RESULT_ERROR,
|
||||||
JSON_MESSAGE: message,
|
JSON_MESSAGE: message,
|
||||||
})
|
}, status=400)
|
||||||
|
|
||||||
|
|
||||||
def api_return_ok(data=None):
|
def api_return_ok(data=None):
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
"""Bootstrap HassIO."""
|
"""Bootstrap HassIO."""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import stat
|
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
from colorlog import ColoredFormatter
|
from colorlog import ColoredFormatter
|
||||||
@@ -17,26 +16,37 @@ def initialize_system_data(websession):
|
|||||||
config = CoreConfig(websession)
|
config = CoreConfig(websession)
|
||||||
|
|
||||||
# homeassistant config folder
|
# homeassistant config folder
|
||||||
if not os.path.isdir(config.path_config):
|
if not config.path_config.is_dir():
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Create Home-Assistant config folder %s", config.path_config)
|
"Create Home-Assistant config folder %s", config.path_config)
|
||||||
os.mkdir(config.path_config)
|
config.path_config.mkdir()
|
||||||
|
|
||||||
# homeassistant ssl folder
|
# homeassistant ssl folder
|
||||||
if not os.path.isdir(config.path_ssl):
|
if not config.path_ssl.is_dir():
|
||||||
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
|
_LOGGER.info("Create Home-Assistant ssl folder %s", config.path_ssl)
|
||||||
os.mkdir(config.path_ssl)
|
config.path_ssl.mkdir()
|
||||||
|
|
||||||
# homeassistant addon data folder
|
# homeassistant addon data folder
|
||||||
if not os.path.isdir(config.path_addons_data):
|
if not config.path_addons_data.is_dir():
|
||||||
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
_LOGGER.info("Create Home-Assistant addon data folder %s",
|
||||||
config.path_addons_data)
|
config.path_addons_data)
|
||||||
os.mkdir(config.path_addons_data)
|
config.path_addons_data.mkdir(parents=True)
|
||||||
|
|
||||||
if not os.path.isdir(config.path_addons_custom):
|
if not config.path_addons_local.is_dir():
|
||||||
_LOGGER.info("Create Home-Assistant addon custom folder %s",
|
_LOGGER.info("Create Home-Assistant addon local repository folder %s",
|
||||||
config.path_addons_custom)
|
config.path_addons_local)
|
||||||
os.mkdir(config.path_addons_custom)
|
config.path_addons_local.mkdir(parents=True)
|
||||||
|
|
||||||
|
if not config.path_addons_git.is_dir():
|
||||||
|
_LOGGER.info("Create Home-Assistant addon git repositories folder %s",
|
||||||
|
config.path_addons_git)
|
||||||
|
config.path_addons_git.mkdir(parents=True)
|
||||||
|
|
||||||
|
# homeassistant backup folder
|
||||||
|
if not config.path_backup.is_dir():
|
||||||
|
_LOGGER.info("Create Home-Assistant backup folder %s",
|
||||||
|
config.path_backup)
|
||||||
|
config.path_backup.mkdir()
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -76,8 +86,7 @@ def check_environment():
|
|||||||
_LOGGER.fatal("Can't find %s in env!", key)
|
_LOGGER.fatal("Can't find %s in env!", key)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
mode = os.stat(SOCKET_DOCKER)[stat.ST_MODE]
|
if not SOCKET_DOCKER.is_socket():
|
||||||
if not stat.S_ISSOCK(mode):
|
|
||||||
_LOGGER.fatal("Can't find docker socket!")
|
_LOGGER.fatal("Can't find docker socket!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
158
hassio/config.py
158
hassio/config.py
@@ -1,49 +1,69 @@
|
|||||||
"""Bootstrap HassIO."""
|
"""Bootstrap HassIO."""
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path, PurePath
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
|
from .const import FILE_HASSIO_CONFIG, HASSIO_SHARE
|
||||||
from .tools import (
|
from .tools import (
|
||||||
fetch_current_versions, write_json_file, read_json_file)
|
fetch_last_versions, write_json_file, read_json_file)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HOMEASSISTANT_CONFIG = "{}/homeassistant"
|
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
|
||||||
HOMEASSISTANT_IMAGE = 'homeassistant_image'
|
HOMEASSISTANT_LAST = 'homeassistant_last'
|
||||||
HOMEASSISTANT_CURRENT = 'homeassistant_current'
|
|
||||||
|
|
||||||
HASSIO_SSL = "{}/ssl"
|
HASSIO_SSL = PurePath("ssl")
|
||||||
HASSIO_CURRENT = 'hassio_current'
|
HASSIO_LAST = 'hassio_last'
|
||||||
HASSIO_CLEANUP = 'hassio_cleanup'
|
HASSIO_CLEANUP = 'hassio_cleanup'
|
||||||
|
|
||||||
ADDONS_REPO = "{}/addons"
|
ADDONS_CORE = PurePath("addons/core")
|
||||||
ADDONS_DATA = "{}/addons_data"
|
ADDONS_LOCAL = PurePath("addons/local")
|
||||||
ADDONS_CUSTOM = "{}/addons_custom"
|
ADDONS_GIT = PurePath("addons/git")
|
||||||
|
ADDONS_DATA = PurePath("addons/data")
|
||||||
|
ADDONS_CUSTOM_LIST = 'addons_custom_list'
|
||||||
|
|
||||||
|
BACKUP_DATA = PurePath("backup")
|
||||||
|
|
||||||
UPSTREAM_BETA = 'upstream_beta'
|
UPSTREAM_BETA = 'upstream_beta'
|
||||||
|
|
||||||
API_ENDPOINT = 'api_endpoint'
|
API_ENDPOINT = 'api_endpoint'
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_CONFIG = vol.Schema({
|
||||||
|
vol.Optional(UPSTREAM_BETA, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(API_ENDPOINT): vol.Coerce(str),
|
||||||
|
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
|
||||||
|
vol.Optional(HASSIO_LAST): vol.Coerce(str),
|
||||||
|
vol.Optional(HASSIO_CLEANUP): vol.Coerce(str),
|
||||||
|
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
|
||||||
|
}, extra=vol.REMOVE_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
"""Hold all config data."""
|
"""Hold all config data."""
|
||||||
|
|
||||||
def __init__(self, config_file):
|
def __init__(self, config_file):
|
||||||
"""Initialize config object."""
|
"""Initialize config object."""
|
||||||
self._filename = config_file
|
self._file = config_file
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
|
||||||
# init or load data
|
# init or load data
|
||||||
if os.path.isfile(self._filename):
|
if self._file.is_file():
|
||||||
try:
|
try:
|
||||||
self._data = read_json_file(self._filename)
|
self._data = read_json_file(self._file)
|
||||||
except OSError:
|
except (OSError, json.JSONDecodeError):
|
||||||
_LOGGER.warning("Can't read %s", self._filename)
|
_LOGGER.warning("Can't read %s", self._file)
|
||||||
|
self._data = {}
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Store data to config file."""
|
"""Store data to config file."""
|
||||||
if not write_json_file(self._filename, self._data):
|
if not write_json_file(self._file, self._data):
|
||||||
_LOGGER.exception("Can't store config in %s", self._filename)
|
_LOGGER.error("Can't store config in %s", self._file)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -57,23 +77,23 @@ class CoreConfig(Config):
|
|||||||
|
|
||||||
super().__init__(FILE_HASSIO_CONFIG)
|
super().__init__(FILE_HASSIO_CONFIG)
|
||||||
|
|
||||||
# init data
|
# validate data
|
||||||
if not self._data:
|
try:
|
||||||
self._data.update({
|
self._data = SCHEMA_CONFIG(self._data)
|
||||||
HOMEASSISTANT_IMAGE: os.environ['HOMEASSISTANT_REPOSITORY'],
|
|
||||||
UPSTREAM_BETA: False,
|
|
||||||
})
|
|
||||||
self.save()
|
self.save()
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid config %s", humanize_error(self._data, ex))
|
||||||
|
|
||||||
async def fetch_update_infos(self):
|
async def fetch_update_infos(self):
|
||||||
"""Read current versions from web."""
|
"""Read current versions from web."""
|
||||||
current = await fetch_current_versions(
|
last = await fetch_last_versions(
|
||||||
self.websession, beta=self.upstream_beta)
|
self.websession, beta=self.upstream_beta)
|
||||||
|
|
||||||
if current:
|
if last:
|
||||||
self._data.update({
|
self._data.update({
|
||||||
HOMEASSISTANT_CURRENT: current.get('homeassistant_tag'),
|
HOMEASSISTANT_LAST: last.get('homeassistant'),
|
||||||
HASSIO_CURRENT: current.get('hassio_tag'),
|
HASSIO_LAST: last.get('hassio'),
|
||||||
})
|
})
|
||||||
self.save()
|
self.save()
|
||||||
return True
|
return True
|
||||||
@@ -93,7 +113,7 @@ class CoreConfig(Config):
|
|||||||
@property
|
@property
|
||||||
def upstream_beta(self):
|
def upstream_beta(self):
|
||||||
"""Return True if we run in beta upstream."""
|
"""Return True if we run in beta upstream."""
|
||||||
return self._data.get(UPSTREAM_BETA, False)
|
return self._data[UPSTREAM_BETA]
|
||||||
|
|
||||||
@upstream_beta.setter
|
@upstream_beta.setter
|
||||||
def upstream_beta(self, value):
|
def upstream_beta(self, value):
|
||||||
@@ -117,59 +137,101 @@ class CoreConfig(Config):
|
|||||||
@property
|
@property
|
||||||
def homeassistant_image(self):
|
def homeassistant_image(self):
|
||||||
"""Return docker homeassistant repository."""
|
"""Return docker homeassistant repository."""
|
||||||
return self._data.get(HOMEASSISTANT_IMAGE)
|
return os.environ['HOMEASSISTANT_REPOSITORY']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_homeassistant(self):
|
def last_homeassistant(self):
|
||||||
"""Actual version of homeassistant."""
|
"""Actual version of homeassistant."""
|
||||||
return self._data.get(HOMEASSISTANT_CURRENT)
|
return self._data.get(HOMEASSISTANT_LAST)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_hassio(self):
|
def last_hassio(self):
|
||||||
"""Actual version of hassio."""
|
"""Actual version of hassio."""
|
||||||
return self._data.get(HASSIO_CURRENT)
|
return self._data.get(HASSIO_LAST)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_hassio_docker(self):
|
def path_extern_hassio(self):
|
||||||
"""Return hassio data path extern for docker."""
|
"""Return hassio data path extern for docker."""
|
||||||
return os.environ['SUPERVISOR_SHARE']
|
return PurePath(os.environ['SUPERVISOR_SHARE'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_config_docker(self):
|
def path_extern_config(self):
|
||||||
"""Return config path extern for docker."""
|
"""Return config path extern for docker."""
|
||||||
return HOMEASSISTANT_CONFIG.format(self.path_hassio_docker)
|
return str(PurePath(self.path_extern_hassio, HOMEASSISTANT_CONFIG))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_config(self):
|
def path_config(self):
|
||||||
"""Return config path inside supervisor."""
|
"""Return config path inside supervisor."""
|
||||||
return HOMEASSISTANT_CONFIG.format(HASSIO_SHARE)
|
return Path(HASSIO_SHARE, HOMEASSISTANT_CONFIG)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_ssl_docker(self):
|
def path_extern_ssl(self):
|
||||||
"""Return SSL path extern for docker."""
|
"""Return SSL path extern for docker."""
|
||||||
return HASSIO_SSL.format(self.path_hassio_docker)
|
return str(PurePath(self.path_extern_hassio, HASSIO_SSL))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_ssl(self):
|
def path_ssl(self):
|
||||||
"""Return SSL path inside supervisor."""
|
"""Return SSL path inside supervisor."""
|
||||||
return HASSIO_SSL.format(HASSIO_SHARE)
|
return Path(HASSIO_SHARE, HASSIO_SSL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_repo(self):
|
def path_addons_core(self):
|
||||||
"""Return git repo path for addons."""
|
"""Return git path for core addons."""
|
||||||
return ADDONS_REPO.format(HASSIO_SHARE)
|
return Path(HASSIO_SHARE, ADDONS_CORE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_custom(self):
|
def path_addons_git(self):
|
||||||
|
"""Return path for git addons."""
|
||||||
|
return Path(HASSIO_SHARE, ADDONS_GIT)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_addons_local(self):
|
||||||
"""Return path for customs addons."""
|
"""Return path for customs addons."""
|
||||||
return ADDONS_CUSTOM.format(HASSIO_SHARE)
|
return Path(HASSIO_SHARE, ADDONS_LOCAL)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_extern_addons_local(self):
|
||||||
|
"""Return path for customs addons."""
|
||||||
|
return str(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 addon data folder."""
|
||||||
return ADDONS_DATA.format(HASSIO_SHARE)
|
return Path(HASSIO_SHARE, ADDONS_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_addons_data_docker(self):
|
def path_extern_addons_data(self):
|
||||||
"""Return root addon data folder extern for docker."""
|
"""Return root addon data folder extern for docker."""
|
||||||
return ADDONS_DATA.format(self.path_hassio_docker)
|
return str(PurePath(self.path_extern_hassio, ADDONS_DATA))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_backup(self):
|
||||||
|
"""Return root backup data folder."""
|
||||||
|
return Path(HASSIO_SHARE, BACKUP_DATA)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_extern_backup(self):
|
||||||
|
"""Return root backup data folder extern for docker."""
|
||||||
|
return str(PurePath(self.path_extern_hassio, BACKUP_DATA))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def addons_repositories(self):
|
||||||
|
"""Return list of addons custom repositories."""
|
||||||
|
return self._data[ADDONS_CUSTOM_LIST]
|
||||||
|
|
||||||
|
@addons_repositories.setter
|
||||||
|
def addons_repositories(self, repo):
|
||||||
|
"""Add a custom repository to list."""
|
||||||
|
if repo in self._data[ADDONS_CUSTOM_LIST]:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data[ADDONS_CUSTOM_LIST].append(repo)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def drop_addon_repository(self, repo):
|
||||||
|
"""Remove a custom repository from list."""
|
||||||
|
if repo not in self._data[ADDONS_CUSTOM_LIST]:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._data[ADDONS_CUSTOM_LIST].remove(repo)
|
||||||
|
self.save()
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
"""Const file for HassIO."""
|
"""Const file for HassIO."""
|
||||||
HASSIO_VERSION = '0.14'
|
from pathlib import Path
|
||||||
|
|
||||||
URL_HASSIO_VERSION = \
|
HASSIO_VERSION = '0.20'
|
||||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version.json'
|
|
||||||
URL_HASSIO_VERSION_BETA = \
|
|
||||||
'https://raw.githubusercontent.com/pvizeli/hassio/master/version_beta.json'
|
|
||||||
|
|
||||||
URL_HASSIO_ADDONS = 'https://github.com/pvizeli/hassio-addons'
|
URL_HASSIO_VERSION = ('https://raw.githubusercontent.com/home-assistant/'
|
||||||
|
'hassio/master/version.json')
|
||||||
|
URL_HASSIO_VERSION_BETA = ('https://raw.githubusercontent.com/home-assistant/'
|
||||||
|
'hassio/dev/version.json')
|
||||||
|
|
||||||
DOCKER_REPO = "pvizeli"
|
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons'
|
||||||
|
|
||||||
HASSIO_SHARE = "/data"
|
DOCKER_REPO = "homeassistant"
|
||||||
|
|
||||||
|
HASSIO_SHARE = Path("/data")
|
||||||
|
|
||||||
RUN_UPDATE_INFO_TASKS = 28800
|
RUN_UPDATE_INFO_TASKS = 28800
|
||||||
RUN_UPDATE_SUPERVISOR_TASKS = 29100
|
RUN_UPDATE_SUPERVISOR_TASKS = 29100
|
||||||
@@ -18,11 +20,11 @@ RUN_RELOAD_ADDONS_TASKS = 28800
|
|||||||
|
|
||||||
RESTART_EXIT_CODE = 100
|
RESTART_EXIT_CODE = 100
|
||||||
|
|
||||||
FILE_HASSIO_ADDONS = "{}/addons.json".format(HASSIO_SHARE)
|
FILE_HASSIO_ADDONS = Path(HASSIO_SHARE, "addons.json")
|
||||||
FILE_HASSIO_CONFIG = "{}/config.json".format(HASSIO_SHARE)
|
FILE_HASSIO_CONFIG = Path(HASSIO_SHARE, "config.json")
|
||||||
|
|
||||||
SOCKET_DOCKER = "/var/run/docker.sock"
|
SOCKET_DOCKER = Path("/var/run/docker.sock")
|
||||||
SOCKET_HC = "/var/run/hassio-hc.sock"
|
SOCKET_HC = Path("/var/run/hassio-hc.sock")
|
||||||
|
|
||||||
JSON_RESULT = 'result'
|
JSON_RESULT = 'result'
|
||||||
JSON_DATA = 'data'
|
JSON_DATA = 'data'
|
||||||
@@ -31,24 +33,33 @@ JSON_MESSAGE = 'message'
|
|||||||
RESULT_ERROR = 'error'
|
RESULT_ERROR = 'error'
|
||||||
RESULT_OK = 'ok'
|
RESULT_OK = 'ok'
|
||||||
|
|
||||||
|
ATTR_HOSTNAME = 'hostname'
|
||||||
|
ATTR_OS = 'os'
|
||||||
|
ATTR_TYPE = 'type'
|
||||||
|
ATTR_SOURCE = 'source'
|
||||||
|
ATTR_FEATURES = 'features'
|
||||||
ATTR_ADDONS = 'addons'
|
ATTR_ADDONS = 'addons'
|
||||||
ATTR_VERSION = 'version'
|
ATTR_VERSION = 'version'
|
||||||
ATTR_CURRENT = 'current'
|
ATTR_LAST_VERSION = 'last_version'
|
||||||
ATTR_BETA = 'beta'
|
ATTR_BETA_CHANNEL = 'beta_channel'
|
||||||
ATTR_NAME = 'name'
|
ATTR_NAME = 'name'
|
||||||
ATTR_SLUG = 'slug'
|
ATTR_SLUG = 'slug'
|
||||||
ATTR_DESCRIPTON = 'description'
|
ATTR_DESCRIPTON = 'description'
|
||||||
ATTR_STARTUP = 'startup'
|
ATTR_STARTUP = 'startup'
|
||||||
ATTR_BOOT = 'boot'
|
ATTR_BOOT = 'boot'
|
||||||
ATTR_PORTS = 'ports'
|
ATTR_PORTS = 'ports'
|
||||||
ATTR_MAP_CONFIG = 'map_config'
|
ATTR_MAP = 'map'
|
||||||
ATTR_MAP_SSL = 'map_ssl'
|
|
||||||
ATTR_OPTIONS = 'options'
|
ATTR_OPTIONS = 'options'
|
||||||
ATTR_INSTALLED = 'installed'
|
ATTR_INSTALLED = 'installed'
|
||||||
ATTR_DEDICATED = 'dedicated'
|
ATTR_DETACHED = 'detached'
|
||||||
ATTR_STATE = 'state'
|
ATTR_STATE = 'state'
|
||||||
ATTR_SCHEMA = 'schema'
|
ATTR_SCHEMA = 'schema'
|
||||||
ATTR_IMAGE = 'image'
|
ATTR_IMAGE = 'image'
|
||||||
|
ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
|
||||||
|
ATTR_REPOSITORY = 'repository'
|
||||||
|
ATTR_REPOSITORIES = 'repositories'
|
||||||
|
ATTR_URL = 'url'
|
||||||
|
ATTR_MAINTAINER = 'maintainer'
|
||||||
|
|
||||||
STARTUP_BEFORE = 'before'
|
STARTUP_BEFORE = 'before'
|
||||||
STARTUP_AFTER = 'after'
|
STARTUP_AFTER = 'after'
|
||||||
@@ -59,3 +70,8 @@ BOOT_MANUAL = 'manual'
|
|||||||
|
|
||||||
STATE_STARTED = 'started'
|
STATE_STARTED = 'started'
|
||||||
STATE_STOPPED = 'stopped'
|
STATE_STOPPED = 'stopped'
|
||||||
|
|
||||||
|
MAP_CONFIG = 'config'
|
||||||
|
MAP_SSL = 'ssl'
|
||||||
|
MAP_ADDONS = 'addons'
|
||||||
|
MAP_BACKUP = 'backup'
|
||||||
|
@@ -8,7 +8,7 @@ import docker
|
|||||||
from . import bootstrap
|
from . import bootstrap
|
||||||
from .addons import AddonManager
|
from .addons import AddonManager
|
||||||
from .api import RestAPI
|
from .api import RestAPI
|
||||||
from .host_controll import HostControll
|
from .host_control import HostControl
|
||||||
from .const import (
|
from .const import (
|
||||||
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
|
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
|
||||||
RUN_UPDATE_SUPERVISOR_TASKS, STARTUP_AFTER, STARTUP_BEFORE)
|
RUN_UPDATE_SUPERVISOR_TASKS, STARTUP_AFTER, STARTUP_BEFORE)
|
||||||
@@ -32,7 +32,7 @@ class HassIO(object):
|
|||||||
self.scheduler = Scheduler(self.loop)
|
self.scheduler = Scheduler(self.loop)
|
||||||
self.api = RestAPI(self.config, self.loop)
|
self.api = RestAPI(self.config, self.loop)
|
||||||
self.dock = docker.DockerClient(
|
self.dock = docker.DockerClient(
|
||||||
base_url="unix:/{}".format(SOCKET_DOCKER), version='auto')
|
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
|
||||||
|
|
||||||
# init basic docker container
|
# init basic docker container
|
||||||
self.supervisor = DockerSupervisor(
|
self.supervisor = DockerSupervisor(
|
||||||
@@ -40,8 +40,8 @@ class HassIO(object):
|
|||||||
self.homeassistant = DockerHomeAssistant(
|
self.homeassistant = DockerHomeAssistant(
|
||||||
self.config, self.loop, self.dock)
|
self.config, self.loop, self.dock)
|
||||||
|
|
||||||
# init HostControll
|
# init HostControl
|
||||||
self.host_controll = HostControll(self.loop)
|
self.host_control = HostControl(self.loop)
|
||||||
|
|
||||||
# init addon system
|
# init addon system
|
||||||
self.addons = AddonManager(self.config, self.loop, self.dock)
|
self.addons = AddonManager(self.config, self.loop, self.dock)
|
||||||
@@ -55,20 +55,18 @@ class HassIO(object):
|
|||||||
# set api endpoint
|
# set api endpoint
|
||||||
self.config.api_endpoint = await get_local_ip(self.loop)
|
self.config.api_endpoint = await get_local_ip(self.loop)
|
||||||
|
|
||||||
# hostcontroll
|
# hostcontrol
|
||||||
host_info = await self.host_controll.info()
|
await self.host_control.load()
|
||||||
if host_info:
|
|
||||||
self.host_controll.version = host_info.get('version')
|
# schedule update info tasks
|
||||||
_LOGGER.info(
|
self.scheduler.register_task(
|
||||||
"Connected to HostControll. OS: %s Version: %s Hostname: %s "
|
self.host_control.load, RUN_UPDATE_INFO_TASKS)
|
||||||
"Feature-lvl: %d", host_info.get('os'),
|
|
||||||
host_info.get('version'), host_info.get('hostname'),
|
|
||||||
host_info.get('level', 0))
|
|
||||||
|
|
||||||
# rest api views
|
# rest api views
|
||||||
self.api.register_host(self.host_controll)
|
self.api.register_host(self.host_control)
|
||||||
self.api.register_network(self.host_controll)
|
self.api.register_network(self.host_control)
|
||||||
self.api.register_supervisor(self.supervisor, self.addons)
|
self.api.register_supervisor(
|
||||||
|
self.supervisor, self.addons, self.host_control)
|
||||||
self.api.register_homeassistant(self.homeassistant)
|
self.api.register_homeassistant(self.homeassistant)
|
||||||
self.api.register_addons(self.addons)
|
self.api.register_addons(self.addons)
|
||||||
|
|
||||||
@@ -130,10 +128,10 @@ class HassIO(object):
|
|||||||
"""Install a homeassistant docker container."""
|
"""Install a homeassistant docker container."""
|
||||||
while True:
|
while True:
|
||||||
# read homeassistant tag and install it
|
# read homeassistant tag and install it
|
||||||
if not self.config.current_homeassistant:
|
if not self.config.last_homeassistant:
|
||||||
await self.config.fetch_update_infos()
|
await self.config.fetch_update_infos()
|
||||||
|
|
||||||
tag = self.config.current_homeassistant
|
tag = self.config.last_homeassistant
|
||||||
if tag and await self.homeassistant.install(tag):
|
if tag and await self.homeassistant.install(tag):
|
||||||
break
|
break
|
||||||
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
|
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
|
||||||
@@ -144,9 +142,9 @@ class HassIO(object):
|
|||||||
|
|
||||||
async def _hassio_update(self):
|
async def _hassio_update(self):
|
||||||
"""Check and run update of supervisor hassio."""
|
"""Check and run update of supervisor hassio."""
|
||||||
if self.config.current_hassio == self.supervisor.version:
|
if self.config.last_hassio == self.supervisor.version:
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Found new HassIO version %s.", self.config.current_hassio)
|
"Found new HassIO version %s.", self.config.last_hassio)
|
||||||
await self.supervisor.update(self.config.current_hassio)
|
await self.supervisor.update(self.config.last_hassio)
|
||||||
|
@@ -203,6 +203,8 @@ class DockerBase(object):
|
|||||||
image="{}:latest".format(self.image), force=True)
|
image="{}:latest".format(self.image), force=True)
|
||||||
self.dock.images.remove(
|
self.dock.images.remove(
|
||||||
image="{}:{}".format(self.image, self.version), force=True)
|
image="{}:{}".format(self.image, self.version), force=True)
|
||||||
|
except docker.errors.ImageNotFound:
|
||||||
|
return True
|
||||||
except docker.errors.DockerException as err:
|
except docker.errors.DockerException as err:
|
||||||
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
|
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
|
||||||
return False
|
return False
|
||||||
|
@@ -26,6 +26,40 @@ class DockerAddon(DockerBase):
|
|||||||
"""Return name of docker container."""
|
"""Return name of docker container."""
|
||||||
return "addon_{}".format(self.addon)
|
return "addon_{}".format(self.addon)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volumes(self):
|
||||||
|
"""Generate volumes for mappings."""
|
||||||
|
volumes = {
|
||||||
|
self.addons_data.path_extern_data(self.addon): {
|
||||||
|
'bind': '/data', 'mode': 'rw'
|
||||||
|
}}
|
||||||
|
|
||||||
|
if self.addons_data.map_config(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_extern_config: {
|
||||||
|
'bind': '/config', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
|
||||||
|
if self.addons_data.map_ssl(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_extern_ssl: {
|
||||||
|
'bind': '/ssl', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
|
||||||
|
if self.addons_data.map_addons(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_extern_addons_local: {
|
||||||
|
'bind': '/addons', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
|
||||||
|
if self.addons_data.map_backup(self.addon):
|
||||||
|
volumes.update({
|
||||||
|
self.config.path_extern_backup: {
|
||||||
|
'bind': '/backup', 'mode': 'rw'
|
||||||
|
}})
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
"""Run docker image.
|
"""Run docker image.
|
||||||
|
|
||||||
@@ -37,22 +71,6 @@ class DockerAddon(DockerBase):
|
|||||||
# cleanup old container
|
# cleanup old container
|
||||||
self._stop()
|
self._stop()
|
||||||
|
|
||||||
# volumes
|
|
||||||
volumes = {
|
|
||||||
self.addons_data.path_data_docker(self.addon): {
|
|
||||||
'bind': '/data', 'mode': 'rw'
|
|
||||||
}}
|
|
||||||
if self.addons_data.need_config(self.addon):
|
|
||||||
volumes.update({
|
|
||||||
self.config.path_config_docker: {
|
|
||||||
'bind': '/config', 'mode': 'rw'
|
|
||||||
}})
|
|
||||||
if self.addons_data.need_ssl(self.addon):
|
|
||||||
volumes.update({
|
|
||||||
self.config.path_ssl_docker: {
|
|
||||||
'bind': '/ssl', 'mode': 'rw'
|
|
||||||
}})
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.container = self.dock.containers.run(
|
self.container = self.dock.containers.run(
|
||||||
self.image,
|
self.image,
|
||||||
@@ -60,7 +78,7 @@ class DockerAddon(DockerBase):
|
|||||||
detach=True,
|
detach=True,
|
||||||
network_mode='bridge',
|
network_mode='bridge',
|
||||||
ports=self.addons_data.get_ports(self.addon),
|
ports=self.addons_data.get_ports(self.addon),
|
||||||
volumes=volumes,
|
volumes=self.volumes,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.version = get_version_from_env(
|
self.version = get_version_from_env(
|
||||||
|
@@ -45,9 +45,9 @@ class DockerHomeAssistant(DockerBase):
|
|||||||
'HASSIO': self.config.api_endpoint,
|
'HASSIO': self.config.api_endpoint,
|
||||||
},
|
},
|
||||||
volumes={
|
volumes={
|
||||||
self.config.path_config_docker:
|
self.config.path_extern_config:
|
||||||
{'bind': '/config', 'mode': 'rw'},
|
{'bind': '/config', 'mode': 'rw'},
|
||||||
self.config.path_ssl_docker:
|
self.config.path_extern_ssl:
|
||||||
{'bind': '/ssl', 'mode': 'rw'},
|
{'bind': '/ssl', 'mode': 'rw'},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
119
hassio/host_control.py
Normal file
119
hassio/host_control.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""Host control for HassIO."""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
SOCKET_HC, ATTR_LAST_VERSION, ATTR_VERSION, ATTR_TYPE, ATTR_FEATURES,
|
||||||
|
ATTR_HOSTNAME, ATTR_OS)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TIMEOUT = 15
|
||||||
|
UNKNOWN = 'unknown'
|
||||||
|
|
||||||
|
FEATURES_SHUTDOWN = 'shutdown'
|
||||||
|
FEATURES_REBOOT = 'reboot'
|
||||||
|
FEATURES_UPDATE = 'update'
|
||||||
|
FEATURES_NETWORK_INFO = 'network_info'
|
||||||
|
FEATURES_NETWORK_CONTROL = 'network_control'
|
||||||
|
|
||||||
|
|
||||||
|
class HostControl(object):
|
||||||
|
"""Client for host control."""
|
||||||
|
|
||||||
|
def __init__(self, loop):
|
||||||
|
"""Initialize HostControl socket client."""
|
||||||
|
self.loop = loop
|
||||||
|
self.active = False
|
||||||
|
self.version = UNKNOWN
|
||||||
|
self.last_version = UNKNOWN
|
||||||
|
self.type = UNKNOWN
|
||||||
|
self.features = []
|
||||||
|
self.hostname = UNKNOWN
|
||||||
|
self.os_info = UNKNOWN
|
||||||
|
|
||||||
|
if SOCKET_HC.is_socket():
|
||||||
|
self.active = True
|
||||||
|
|
||||||
|
async def _send_command(self, command):
|
||||||
|
"""Send command to host.
|
||||||
|
|
||||||
|
Is a coroutine.
|
||||||
|
"""
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
reader, writer = await asyncio.open_unix_connection(
|
||||||
|
str(SOCKET_HC), loop=self.loop)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# send
|
||||||
|
_LOGGER.info("Send '%s' to HostControl.", command)
|
||||||
|
|
||||||
|
with async_timeout.timeout(TIMEOUT, loop=self.loop):
|
||||||
|
writer.write("{}\n".format(command).encode())
|
||||||
|
data = await reader.readline()
|
||||||
|
|
||||||
|
response = data.decode().rstrip()
|
||||||
|
_LOGGER.info("Receive from HostControl: %s.", response)
|
||||||
|
|
||||||
|
if response == "OK":
|
||||||
|
return True
|
||||||
|
elif response == "ERROR":
|
||||||
|
return False
|
||||||
|
elif response == "WRONG":
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return json.loads(response)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
_LOGGER.warning("Json parse error from HostControl '%s'.",
|
||||||
|
response)
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
_LOGGER.error("Timeout from HostControl!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
async def load(self):
|
||||||
|
"""Load Info from host.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
info = await self._send_command("info")
|
||||||
|
if not info:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.version = info.get(ATTR_VERSION, UNKNOWN)
|
||||||
|
self.last_version = info.get(ATTR_LAST_VERSION, UNKNOWN)
|
||||||
|
self.type = info.get(ATTR_TYPE, UNKNOWN)
|
||||||
|
self.features = info.get(ATTR_FEATURES, [])
|
||||||
|
self.hostname = info.get(ATTR_HOSTNAME, UNKNOWN)
|
||||||
|
self.os_info = info.get(ATTR_OS, UNKNOWN)
|
||||||
|
|
||||||
|
def reboot(self):
|
||||||
|
"""Reboot the host system.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self._send_command("reboot")
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Shutdown the host system.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self._send_command("shutdown")
|
||||||
|
|
||||||
|
def update(self, version=None):
|
||||||
|
"""Update the host system.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
if version:
|
||||||
|
return self._send_command("update {}".format(version))
|
||||||
|
return self._send_command("update")
|
@@ -1,102 +0,0 @@
|
|||||||
"""Host controll for HassIO."""
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import stat
|
|
||||||
|
|
||||||
import async_timeout
|
|
||||||
|
|
||||||
from .const import SOCKET_HC
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
TIMEOUT = 15
|
|
||||||
|
|
||||||
LEVEL_POWER = 1
|
|
||||||
LEVEL_UPDATE_HOST = 2
|
|
||||||
LEVEL_NETWORK = 4
|
|
||||||
|
|
||||||
|
|
||||||
class HostControll(object):
|
|
||||||
"""Client for host controll."""
|
|
||||||
|
|
||||||
def __init__(self, loop):
|
|
||||||
"""Initialize HostControll socket client."""
|
|
||||||
self.loop = loop
|
|
||||||
self.active = False
|
|
||||||
self.version = None
|
|
||||||
|
|
||||||
mode = os.stat(SOCKET_HC)[stat.ST_MODE]
|
|
||||||
if stat.S_ISSOCK(mode):
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
async def _send_command(self, command):
|
|
||||||
"""Send command to host.
|
|
||||||
|
|
||||||
Is a coroutine.
|
|
||||||
"""
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
|
|
||||||
reader, writer = await asyncio.open_unix_connection(
|
|
||||||
SOCKET_HC, loop=self.loop)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# send
|
|
||||||
_LOGGER.info("Send '%s' to HostControll.", command)
|
|
||||||
|
|
||||||
with async_timeout.timeout(TIMEOUT, loop=self.loop):
|
|
||||||
writer.write("{}\n".format(command).encode())
|
|
||||||
data = await reader.readline()
|
|
||||||
|
|
||||||
response = data.decode()
|
|
||||||
_LOGGER.debug("Receive from HostControll: %s.", response)
|
|
||||||
|
|
||||||
if response == "OK":
|
|
||||||
return True
|
|
||||||
elif response == "ERROR":
|
|
||||||
return False
|
|
||||||
elif response == "WRONG":
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return json.loads(response)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
_LOGGER.warning("Json parse error from HostControll.")
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
_LOGGER.error("Timeout from HostControll!")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
writer.close()
|
|
||||||
|
|
||||||
def info(self):
|
|
||||||
"""Return Info from host.
|
|
||||||
|
|
||||||
Return a coroutine.
|
|
||||||
"""
|
|
||||||
return self._send_command("info")
|
|
||||||
|
|
||||||
def reboot(self):
|
|
||||||
"""Reboot the host system.
|
|
||||||
|
|
||||||
Return a coroutine.
|
|
||||||
"""
|
|
||||||
return self._send_command("reboot")
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
"""Shutdown the host system.
|
|
||||||
|
|
||||||
Return a coroutine.
|
|
||||||
"""
|
|
||||||
return self._send_command("shutdown")
|
|
||||||
|
|
||||||
def host_update(self, version=None):
|
|
||||||
"""Update the host system.
|
|
||||||
|
|
||||||
Return a coroutine.
|
|
||||||
"""
|
|
||||||
if version:
|
|
||||||
return self._send_command("host-update {}".format(version))
|
|
||||||
return self._send_command("host-update")
|
|
@@ -16,7 +16,7 @@ _RE_VERSION = re.compile(r"VERSION=(.*)")
|
|||||||
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
|
_IMAGE_ARCH = re.compile(r".*/([a-z0-9]*)-hassio-supervisor")
|
||||||
|
|
||||||
|
|
||||||
async def fetch_current_versions(websession, beta=False):
|
async def fetch_last_versions(websession, beta=False):
|
||||||
"""Fetch current versions from github.
|
"""Fetch current versions from github.
|
||||||
|
|
||||||
Is a coroutine.
|
Is a coroutine.
|
||||||
@@ -77,9 +77,10 @@ def get_local_ip(loop):
|
|||||||
def write_json_file(jsonfile, data):
|
def write_json_file(jsonfile, data):
|
||||||
"""Write a json file."""
|
"""Write a json file."""
|
||||||
try:
|
try:
|
||||||
with open(jsonfile, 'w') as conf_file:
|
json_str = json.dumps(data, indent=2)
|
||||||
conf_file.write(json.dumps(data))
|
with jsonfile.open('w') as conf_file:
|
||||||
except OSError:
|
conf_file.write(json_str)
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -87,5 +88,5 @@ def write_json_file(jsonfile, data):
|
|||||||
|
|
||||||
def read_json_file(jsonfile):
|
def read_json_file(jsonfile):
|
||||||
"""Read a json file and return a dict."""
|
"""Read a json file and return a dict."""
|
||||||
with open(jsonfile, 'r') as cfile:
|
with jsonfile.open('r') as cfile:
|
||||||
return json.loads(cfile.read())
|
return json.loads(cfile.read())
|
||||||
|
BIN
misc/hassio.png
Normal file
BIN
misc/hassio.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
1
misc/hassio.xml
Normal file
1
misc/hassio.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36" version="6.5.6" editor="www.draw.io" type="device"><diagram name="Page-1">5Vptc6M2EP41/ng3gHj9mPiSy820c5n6Q3sfsVBsNTJyhYid/voKkABZkOBY+KYtmYnR6pVn99ld1l6A5e74laX77a80Q2ThOdlxAb4sPC8OY/G/Erw2At9xG8GG4awR9QQr/DeSQkdKS5yhQhvIKSUc73UhpHmOINdkKWP0oA97okTfdZ9ukCFYwZSY0t9xxrdS6oZJ1/GA8GYrt469sOlYp/B5w2iZy/0WHniqr6Z7l6q15IMW2zSjh54I3C3AklHKm7vdcYlIBa2CrZl3P9LbnpuhnE+Z4DUTXlJSInXikIipt09UrCAOyF8lKOFfJVUdn4paZTdigNjtKD5ERw206DtIYKrenLJdSrrJ4m5TfX5fqX3E2Zqtmg4JS7urd9hijlb7FFbtg7A2MWjLd0S03Oo0mJAlJZTVowXYKIRQyAvO6DPq9Tj1Jc+/kutLvF4Q4+g4CqHbKkbYO6I7xNmrGKImJKCZIm09SKRuD53l+Arobc9oQjkulca6aZfuFCZupM6G9QcM/X3LcaW31WvB0e5CNGGG1vF6CE0QggRkrb7sAhhNBNCzAKBvAPiFwmfELkUOokCQ/trI+SZy3hBywAJyoYHcw9JArXaFqJpRUe9MLscQDXN5HQd+4NjB0A8DHcPQxDBwTAgDCxAmBl4oE3FINinjW7qheUruOumtjmgPPXTE/I9K/DkKZPOH6srFwZq+QDV/yBX+RJy/ygiclpwKUbfxL5Tu5RrNUavzvQ20eBxaMihHRTJ4p2yDeM9uTHUwRFKOX/TVLwFX5RK20fXeQDcB3im+deMRMSweALGfBbp/JdCj0Xxi3UX48xIMN6wSjNMEYlXuEXvBhXAJagOm+h7Sovj2fTTBaMXr0aSjMwP3fbdluKflMgybVEN3aFmA4sy347ZAoLstMJB1uPGA33JtRE3Xm4Nbbo9Yyou13NJ4VbuxeUnkqveOHouiK7EIzOO6NHh1dE/iQtc89VyFwIPfVK9YQgCJYBqGSnyPidpzqm5QnpmLCWFvqcFMfrm0qlgvvlZQUm8cvaxJrPLpRjy6wLByU9dxRSmKn6CtLFR3Rd5A/t56HS1/9224ovDKXHE/O3qQ/+zG8aWBfiKtPmjxwLR4d0Sn1i3enyVUSJ30srCJCPYcTk5zpHmb8xQ2Vl+AJXtp+WpPYdeKPa5ZUrjJMpoXhhqLbbqvbveMQlQU73sn3ZVN9lX34qr9fZMTCt07XhiBxANhEHtx7PhgpqRqyJN5bmB6ssSCI1O1nDmJ0rVOHdWlqYAkU59uc7zoXEAAOfWR4vq9Q5WqneE0Wq3Q0FJO6hdSz1ynobKxTm0U7dNMs5PYJCjk1KxYKX6WO9IMALcVOzAUyKdrRB5pgTmmuRiyppzTnRhAqo7btoitVVbrMna3xg3Bm2oup+fRvCvEnpZu5QYWiHxS0wEDNR0wkJBYqciaNJ5AUifSWOq/x1LX5OgUOk5Ity8PgO97LQshEng/L0SqvXsMPBwOpvcmBO+LWg2SiZDQMrs4Tl6FQInuz3xnIKeP5iovgLcLo9K4P5DEn8mRmTLEXqzt3hyaQ3qj0faDNPFNmjTmaz+S+icmc+pN7YVAMP6tjfNQrkcjIUzZ5fQL62uAfkH1Z4d+CThJJ4boN1TdsxLBopnY17f7yGaWOT9lP8i+YAb2TVZjYJDkK+bbuekxFp2QmwUomocevnppvQo94v9LcEpCnaOR5dgU/idjk/m9+G9oX71qUYbReBXl30s+Vf6dgXyi2f0WqlFG93szcPcP</diagram></mxfile>
|
10
version.json
10
version.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"hassio_tag": "0.13",
|
"hassio": "0.20",
|
||||||
"homeassistant_tag": "0.43.1",
|
"homeassistant": "0.43.2",
|
||||||
"resinos_version": "0.4",
|
"resinos": "0.6",
|
||||||
"resinhup_version": "0.1",
|
"resinhup": "0.1",
|
||||||
"generic_hc_version": "0.1"
|
"generic": "0.3"
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"hassio_tag": "0.13",
|
|
||||||
"homeassistant_tag": "0.43.1",
|
|
||||||
"resinos_version": "0.4",
|
|
||||||
"resinhup_version": "0.1",
|
|
||||||
"generic_hc_version": "0.1"
|
|
||||||
}
|
|
Reference in New Issue
Block a user