Compare commits

...

169 Commits
0.41 ... 0.62

Author SHA1 Message Date
Pascal Vizeli
2bf440a744 Update hass.io to version 0.62 2017-09-12 20:10:15 +02:00
Pascal Vizeli
3b26136636 More schema options (#187)
* Extend the addon schema options

* convert range to float

* convert match to string

* fix lint

* cleanup

* fix lint

* fix options name
2017-09-12 19:38:26 +02:00
Pascal Vizeli
8249f042c0 Pump version to 0.62 2017-09-11 14:42:23 +02:00
Pascal Vizeli
84bbaeee5f Fix merge conflicts with versions 2017-09-11 14:41:12 +02:00
Pascal Vizeli
b7620b7adf Update version.json 2017-09-11 14:24:48 +02:00
Pascal Vizeli
5a80be9fd4 Allow stop/start home-assistant & flow of startup (#182)
* Allow config boot

* Read boot settings

* Use internal boot time for detect reboot

* Check if Home-Assistant need to watch

* Make datetime string and parse_datetime

* Add api calls

* fix lint p1

* Use new datetime parser for sessions and make a real default boot time

* fix lint p2

* only start docker if they is running

* convert to int (timestamp)

* add boot flag
2017-09-11 14:14:26 +02:00
Pascal Vizeli
a733886803 Pump version to 0.61 2017-09-11 10:03:15 +02:00
Pascal Vizeli
834fd29fab Update HomeAssistant to 0.53 2017-09-10 16:33:37 +02:00
Pascal Vizeli
fd1caf8aa6 Update HomeAssistant to 0.53 2017-09-10 16:33:17 +02:00
Pascal Vizeli
975c9e8061 Update Home-Assistant 0.52.1 2017-08-28 23:38:43 +02:00
Pascal Vizeli
0b3c5885ec Update Home-Assistant 0.52.1 2017-08-28 23:38:17 +02:00
Pascal Vizeli
711b63e2d0 Update Home-Assistant to version 0.52 (#173) 2017-08-26 19:42:55 +02:00
Pascal Vizeli
c7b833b5eb Update Home-Assistant 0.52 2017-08-26 13:49:11 +02:00
Pascal Vizeli
fd472b3084 Update Hass.io to 0.60 2017-08-25 16:47:06 +02:00
Pascal Vizeli
dcbb6a2160 Fix socat spawn (#172)
* Update dns.py

* Update dns.py
2017-08-25 16:41:48 +02:00
Pascal Vizeli
56fa1550d2 Pump version to 0.60 2017-08-24 22:40:19 +02:00
Pascal Vizeli
e1f97860ee Update hass.io to 0.59 2017-08-24 22:27:42 +02:00
Pascal Vizeli
6ab3fe18d9 Allow to see log also if there some process (#170) 2017-08-24 22:23:24 +02:00
Pascal Vizeli
7969f3dfd7 Remove default bridge (#168)
* Remove default bridge

* rename bridge
2017-08-24 21:49:36 +02:00
Pascal Vizeli
6f05b90e4e Pump hass.io to 0.59 2017-08-24 17:17:34 +02:00
Pascal Vizeli
3aa53d99d7 Update hass.io to 0.58 2017-08-24 17:07:26 +02:00
Pascal Vizeli
3525f5a02f Cleanup network mode & fix port mapping (#166)
* Cleanup network mode & fix port mapping

* Fix lint
2017-08-24 16:39:06 +02:00
Pascal Vizeli
04514a9f5c WIP: Network docker hassio (#159)
* Create hassio network layer / allow linking

* rename docker

* fix lint

* fix lint p2

* Set network options

* First version of network code

* Finish network layer

* Remove old api_endpoint stuff

* Add DNS forwarding

* Fix DNS recorder

* Fix lint p1

* Fix lint p2

* Fix lint p3

* Fix spell

* Fix ipam struct

* Fix ip to str

* Fix ip to str v2

* Fix spell

* Fix hass on host

* Fix host attach to network

* Cleanup network code

* Fix lint & add debug

* fix link

* Remove log

* Fix network

* fix reattach of supervisor

* set options

* Fix containers

* Fix remapping & add a test

* Fix dict bug

* Fix prop

* Test with run container

* Fix problem
2017-08-24 14:57:13 +02:00
Pascal Vizeli
1c915ef4cd Pump hass.io version to 0.58 2017-08-22 16:29:10 +02:00
pvizeli
b03a2c5c5f Merge remote-tracking branch 'origin/dev' 2017-08-22 16:16:01 +02:00
Pascal Vizeli
64988b285e Update hass.io to version 0.57 2017-08-22 16:15:17 +02:00
Pascal Vizeli
5c69dca7b3 New panel with poly2 (#163) 2017-08-22 15:44:14 +02:00
Pascal Vizeli
dfda7dc748 Better cleanup for local build add-ons (#161)
* Better cleanup for local build add-ons

* fix lint
2017-08-20 23:02:58 +02:00
Pascal Vizeli
cb7710c23f Add a error message if that is not a local build addon (#162) 2017-08-20 22:33:27 +02:00
Pascal Vizeli
f9b12a2eb2 Allow rebuild for local build addons (#158)
* Allow rebuild for local build addons

* fix lint
2017-08-19 22:44:39 +02:00
Pascal Vizeli
6a7617faad Use deepmerge for options (#157)
Add an optional extended description…
2017-08-18 15:57:13 +02:00
Pascal Vizeli
05554ccf7e Pump version to 0.57 2017-08-16 11:38:43 +02:00
pvizeli
a94e6c5303 Merge remote-tracking branch 'origin/dev' 2017-08-16 11:38:07 +02:00
Pascal Vizeli
d6fc8892db Update hass.io to 0.56 2017-08-16 11:36:50 +02:00
Pascal Vizeli
fa9b3b939e Use exit code to detect wrong configs (#156)
* Update homeassistant.py

* Update homeassistant.py

* Update util.py

* add support for yaml errors
2017-08-16 11:25:38 +02:00
Pascal Vizeli
70685c41be Update const.py 2017-08-16 02:18:33 +02:00
Pascal Vizeli
a3209c4bde Merge remote-tracking branch 'origin/dev' 2017-08-16 02:17:57 +02:00
Pascal Vizeli
f3e60f6c28 Update hass.io to 0.55 2017-08-16 02:13:54 +02:00
Pascal Vizeli
7798e7cde2 Code cleanup & add config check to API (#155)
* Code cleanup & add config check to API

* Fix comments

* fix lint p1

* Fix lint p2

* fix coro

* fix parameter

* add log output

* fix command layout

* fix Pannel

* fix lint p4

* fix regex

* convert to ascii

* fix lint p5

* add temp logging

* fix output on non-zero exit

* remove temporary log
2017-08-16 02:10:38 +02:00
Pascal Vizeli
4af92b9d25 Don't return false for addon startup on update (#153) 2017-08-15 21:30:05 +02:00
Pascal Vizeli
eab958860c Pump hassio to 0.55 2017-08-15 09:15:21 +02:00
pvizeli
09bba96940 Merge remote-tracking branch 'origin/dev' 2017-08-15 09:07:47 +02:00
Pascal Vizeli
a34806d4e2 Update hass.io to 0.54 2017-08-15 08:56:40 +02:00
Pascal Vizeli
f00b21dc28 Bugfix git clone (#152)
Add an optional extended description…
2017-08-15 08:55:55 +02:00
Pascal Vizeli
021946e181 Pump version to 0.54 2017-08-15 00:35:58 +02:00
Pascal Vizeli
6cab017042 Fix version conflict 2017-08-15 00:27:47 +02:00
Pascal Vizeli
5999b48be4 Update Hass.io 0.53 2017-08-15 00:05:55 +02:00
Pascal Vizeli
57f3178408 Change update flow to a higher level (#150) 2017-08-14 23:56:52 +02:00
Franck Nijhof
14013ac923 Recursively git clone addon repositories, allowing submodules (#148) 2017-08-14 16:15:08 +02:00
Pascal Vizeli
d08343d040 Update HomeAssistant 0.51.2 2017-08-14 13:15:45 +02:00
Pascal Vizeli
2f9f9c6165 Update HomeAssistant 0.51.2 2017-08-14 13:15:15 +02:00
Pascal Vizeli
8ab0ed5047 Update HomeAssistant 0.51.1 2017-08-13 14:59:50 +02:00
Pascal Vizeli
0119b52e11 Update HomeAssistant 0.51.1 2017-08-13 14:59:33 +02:00
Pascal Vizeli
1382a7b36e Update to HomeAssistant 0.51 2017-08-13 09:24:05 +02:00
Pascal Vizeli
2eeb8bf388 Update to HomeAssistant 0.51 2017-08-13 09:08:41 +02:00
Pascal Vizeli
5af3040223 Pump version to 0.53 2017-08-10 11:25:07 +02:00
pvizeli
47491ca55b Fix merge conflict with versions 2017-08-10 11:24:21 +02:00
Pascal Vizeli
b06ce9b6b4 Update Hass.io 0.52 2017-08-10 11:15:25 +02:00
Pascal Vizeli
38284e036d Move system into startup protection (#145)
Add an optional extended description…
2017-08-10 11:08:43 +02:00
Pascal Vizeli
27a079742d Fix hostname / add device if it use audio (#144)
Add an optional extended description…
2017-08-10 11:06:55 +02:00
Pascal Vizeli
7f33b3b5aa Pump version to 0.52 2017-08-08 21:14:37 +02:00
Pascal Vizeli
261bda82db Fix version merge conflict 2017-08-08 21:12:32 +02:00
Pascal Vizeli
c39d6357f3 fix parameter 2017-08-08 18:31:37 +02:00
Pascal Vizeli
d1b30a0e95 fix last version of HomeAssistant (#140) 2017-08-08 18:17:23 +02:00
Pascal Vizeli
6a74893a30 Bugfix docker have no version (#139) 2017-08-08 18:01:53 +02:00
Pascal Vizeli
b61d5625fe update hass.io to 0.51 2017-08-08 16:59:43 +02:00
Pascal Vizeli
8d468328f3 Expose new function to add-ons (#138)
* Expose new function to add-ons

* Rename `hassio` to `hassio_api`

* fix lint

* done
2017-08-08 16:54:42 +02:00
Pascal Vizeli
cd3b382902 Cleanup json / api code with new options (#137)
* Cleanup json / api code with new options

* fix lint
2017-08-08 10:47:39 +02:00
Pascal Vizeli
99cf44aacd Cleanup config / new updater object / New audio (#135)
* Cleanup config / new updater object / New audio

* Cleanup beta_channel

* fix lint

* fix lint p3

* Fix lint p4

* Allow set audio options

* Fix errors

* add host options
2017-08-08 00:53:54 +02:00
William Johansson
eaa489abec Allow privileged capability SYS_RAWIO (#136)
In order to allow writes to /dev/mem, which is needed e.g. to use the
GPIO ports on Raspberry Pi, the SYS_RAWIO capability needs to be
granted.

Fixes #134.
2017-08-07 21:58:57 +02:00
Pascal Vizeli
46f323791d Update to new beta version of image 2017-08-06 22:48:46 +02:00
Fabian Affolter
ec72d38220 Sync names 2017-08-04 17:00:31 +02:00
Pascal Vizeli
f5b166a7f0 Use addon slug as hostname instead docker name (#132) 2017-08-04 16:32:17 +02:00
Pascal Vizeli
8afde1e881 Return a error on update with own version (#124)
Return a error on update with own version
2017-08-02 16:59:38 +02:00
Pascal Vizeli
f751b0e6fc Update homeassistant to 0.50.2 2017-08-01 11:35:07 +02:00
Pascal Vizeli
3809f20c6a Update homeassistant to 0.50.2 2017-08-01 11:34:29 +02:00
Pascal Vizeli
68390469df Pump hass.io version to 0.51 2017-07-31 11:51:34 +02:00
pvizeli
4c122a0630 Fix version merge 2017-07-31 11:49:16 +02:00
Pascal Vizeli
d06696cd94 Update Hass.io to version 0.50 2017-07-31 11:44:02 +02:00
Pascal Vizeli
8d094d5c70 Fix wrong addon config break supervisor (#123) 2017-07-31 11:41:08 +02:00
Pascal Vizeli
068c463c98 Update Home-Assistant to version 0.50 2017-07-30 02:41:07 +02:00
Pascal Vizeli
fc95933098 Update Home-Assistant to version 0.50 2017-07-30 02:35:27 +02:00
Pascal Vizeli
630137a576 Fix wrong list (#119) 2017-07-30 00:06:43 +02:00
Pascal Vizeli
857f346b35 Pump version to 0.50 2017-07-29 00:11:06 +02:00
Pascal Vizeli
d98b4f039f Merge pull request #118 from home-assistant/dev
Release 0.49
2017-07-29 00:03:00 +02:00
Pascal Vizeli
8fee52da5e Update Hass.io to 0.49 2017-07-28 23:46:13 +02:00
Pascal Vizeli
0f9ad3658b Update panel (#117) 2017-07-28 23:45:36 +02:00
Pascal Vizeli
1155ee07e5 Hardware interface for UI (#116)
* Init hardware object

* Update API

* Update hardware list

* fix api description

* fix lint

* add hardware to API

* fix lint

* fix wrong

* fix view
2017-07-28 23:05:40 +02:00
Pascal Vizeli
fa687e982e Set hostname on homeassistant / addons (#115) 2017-07-27 22:17:48 +02:00
Fabian Affolter
4e902af937 Update name and remove/add blank lines 2017-07-26 22:34:20 +02:00
Pascal Vizeli
6455ad14a7 Pump version 0.49 2017-07-26 21:44:00 +02:00
Pascal Vizeli
4753c058a3 Merge pull request #114 from home-assistant/dev
Release 0.48
2017-07-26 21:02:44 +02:00
Pascal Vizeli
1567cbfe37 Update version.json 2017-07-26 20:56:49 +02:00
Pascal Vizeli
3ed66c802e Bugfix frontent repositories list (#113) 2017-07-26 18:49:39 +02:00
Pascal Vizeli
980baf23a8 Pump version to 0.48 2017-07-25 00:20:10 +02:00
Pascal Vizeli
d69af6a62b Fix merge version.json 2017-07-25 00:18:34 +02:00
Pascal Vizeli
863456525f fix json validate (#112) 2017-07-25 00:01:02 +02:00
Pascal Vizeli
dae49df7b1 Update Hass.io to 0.47 2017-07-24 23:42:28 +02:00
Pascal Vizeli
282fc03687 Look schema for update (#111)
* Check valid schema for update

* fix merge options

* fix style & return value

* simplify
2017-07-24 23:35:22 +02:00
Pascal Vizeli
f9f7e07c52 Update HomeAssistant 0.49.1 2017-07-24 22:43:08 +02:00
Pascal Vizeli
12a2ccf0ec Update HomeAssistant 0.49.1 2017-07-24 22:42:48 +02:00
Pascal Vizeli
a98d76618a Update tasks.py (#110) 2017-07-24 12:13:16 +02:00
Pascal Vizeli
7a59e7392b update name 2017-07-24 12:05:23 +02:00
Pascal Vizeli
446aff3fa6 Merge pull request #109 from pvizeli/allow_mount
Allow SYS_ADMIN, show devices and privileged on API
2017-07-24 11:48:35 +02:00
Pascal Vizeli
3272403141 Update frontend 2017-07-24 10:58:32 +02:00
pvizeli
d1f265da9e Update UI 2017-07-24 10:44:23 +02:00
Pascal Vizeli
4915c935dd use set for speedup 2017-07-24 10:36:47 +02:00
Pascal Vizeli
e78d935824 fix spell 2017-07-24 10:33:04 +02:00
pvizeli
934ca64a32 Allow SYS_ADMIN, show devices and privileged on API 2017-07-24 10:30:51 +02:00
Pascal Vizeli
0860e6d202 Update resinhup to v0.3 2017-07-24 00:47:39 +02:00
Pascal Vizeli
c3e1c8b58e Update resinhup to v0.3 2017-07-24 00:46:21 +02:00
Paulus Schoutsen
44e48095c7 Update license to be Apache 2.0 2017-07-23 12:11:44 -07:00
Paulus Schoutsen
a13eb7841d Remove not ready for production line 2017-07-23 11:28:43 -07:00
Pascal Vizeli
b5701c5878 Pump dev version to 0.47 2017-07-23 00:07:41 +02:00
Pascal Vizeli
803eb0f8c9 Merge pull request #108 from home-assistant/dev
Release 0.46
2017-07-22 23:47:51 +02:00
Pascal Vizeli
58c5ed7ba1 Update supervisor.py 2017-07-22 22:49:58 +02:00
Pascal Vizeli
c4d7d671d1 Update frontend 0.46 (#107) 2017-07-22 22:38:07 +02:00
Pascal Vizeli
9d88255225 API cleanup (#106)
* API cleanup

* fix lint

* fix wrong return

* fix snapshots/reload

* cleanup

* fix lint

* fix lint
2017-07-22 22:34:25 +02:00
Pascal Vizeli
bfbc366f55 Update hassio 0.46 2017-07-21 01:36:10 +02:00
Pascal Vizeli
0f30a23f3e Add support for webui (#105)
* Add support for webui

* support lists

* fix regex
2017-07-21 01:34:46 +02:00
Pascal Vizeli
7e1bb42bb7 add logo support (#104)
* fix lint

* fix lint p2

* fix api output

* fix decorator

* fix decorator p2

* fix UnboundLocalError

* revert

* fix trace bug

* fix conent type

* allow logo
2017-07-21 00:23:31 +02:00
Pascal Vizeli
251a43216e fix lint 2017-07-20 22:28:55 +02:00
Pascal Vizeli
4801b9903c fix coro 2017-07-20 22:05:58 +02:00
Pascal Vizeli
cd5a09938f Add support for logo 2017-07-20 22:04:44 +02:00
Pascal Vizeli
14bf834224 Update resinos to 1.0 2017-07-19 20:51:43 +02:00
Pascal Vizeli
8aec943a5c Update version.json 2017-07-19 20:51:18 +02:00
Pascal Vizeli
d817e75d98 Update resinos to version 0.10 2017-07-19 16:56:10 +02:00
Pascal Vizeli
fbd8abdcd5 Update version.json 2017-07-19 16:55:32 +02:00
Pascal Vizeli
ca02977505 Update OTA resinhup utility 2017-07-19 14:15:19 +02:00
Pascal Vizeli
6533b57c6d Update OTA resinhup utility 2017-07-19 14:14:48 +02:00
Pascal Vizeli
0a818282d3 Update const.py 2017-07-17 09:48:05 +02:00
Pascal Vizeli
ce2f5f9f7a Resolve merge conflict by version.json 2017-07-17 09:46:21 +02:00
Pascal Vizeli
01f767e66c New UI with ICON fix (#103)
Add an optional extended description…
2017-07-17 07:45:20 +02:00
Pascal Vizeli
106ab924e3 Update ui 45 (#102)
* Update UI

* Update panel
2017-07-17 00:53:57 +02:00
Pascal Vizeli
d031594bf9 Update HassIO 0.45 2017-07-17 00:52:36 +02:00
Pascal Vizeli
f2f146063b Pump version 2017-07-17 00:06:04 +02:00
Pascal Vizeli
5abe7a3fb9 Update HomeAssistant to 0.49 2017-07-16 12:34:29 +02:00
Pascal Vizeli
f592971b6e Update HomeAssistant to 0.49 2017-07-16 12:34:03 +02:00
Pascal Vizeli
ed2caa0d81 Merge pull request #100 from home-assistant/dev
Release 0.44
2017-07-15 01:32:20 +02:00
Pascal Vizeli
0b04c90b1f fix validate bug 2017-07-15 01:00:06 +02:00
Pascal Vizeli
2eac4b8d9b Update version.json 2017-07-14 23:51:48 +02:00
Pascal Vizeli
143a358b0c Change startup system / Allow nested dict (#99)
* Change startup system / Allow nested dict

* fix schema

* fix lint
2017-07-14 23:51:24 +02:00
Pascal Vizeli
fa049066fc Update version.json 2017-07-12 23:12:34 +02:00
Pascal Vizeli
3877dcf355 Pump version to 0.44 2017-07-12 17:25:12 +02:00
Pascal Vizeli
bfa7443ae2 Merge pull request #97 from home-assistant/dev
Release 0.43
2017-07-12 02:31:19 +02:00
Pascal Vizeli
253962df87 Update version.json 2017-07-12 02:25:18 +02:00
Pascal Vizeli
f8fbee68f4 Merge pull request #96 from pvizeli/cleanup_api_snapshot_p2
Update API / fix isoformat
2017-07-12 02:24:20 +02:00
Pascal Vizeli
3c5d4037f7 fix lint 2017-07-12 02:19:01 +02:00
Pascal Vizeli
772709dd75 Update API / fix isoformat 2017-07-12 01:38:15 +02:00
Pascal Vizeli
bcfd76d33c Merge pull request #94 from pvizeli/extend_hass
Add landingpage / cleanup homeassistant core handling
2017-07-12 00:12:12 +02:00
Pascal Vizeli
2bbe7e7dc1 cleanup after install 2017-07-12 00:09:03 +02:00
Pascal Vizeli
dbcd090244 fix flow 2017-07-11 23:45:29 +02:00
Pascal Vizeli
a0a1fd4875 fix coro 2017-07-11 23:29:38 +02:00
Pascal Vizeli
d978ec00aa fix progress v2 2017-07-11 23:08:47 +02:00
Pascal Vizeli
e40963a686 fix calling 2017-07-11 23:02:20 +02:00
Pascal Vizeli
55ec1a84fa expose running state of docker 2017-07-11 22:58:05 +02:00
Pascal Vizeli
cf154b57f3 fix lint p4 2017-07-11 22:45:09 +02:00
Pascal Vizeli
ebf4daf4cc fix lint 2017-07-11 22:38:07 +02:00
Pascal Vizeli
40e8f411ff fix lint p2 2017-07-11 22:19:22 +02:00
Pascal Vizeli
421b380043 fix lint 2017-07-11 22:11:25 +02:00
pvizeli
5ebf2068b2 Update interface, allow update every time 2017-07-11 16:57:03 +02:00
pvizeli
e5fc6846e0 fix snapshot with custom image 2017-07-11 16:52:16 +02:00
pvizeli
906c4e03fb Finish new homeassistant handling 2017-07-11 16:19:24 +02:00
pvizeli
02c8baef68 Add landingpage / cleanup homeassistant core handling 2017-07-11 12:26:58 +02:00
Pascal Vizeli
a14917e017 Pump version 0.43 2017-07-07 14:21:41 +02:00
Pascal Vizeli
7e5b2673dc Merge pull request #93 from home-assistant/dev
* Pump version to 0.42

* Update util.py (#92)

Add an optional extended description…

* Update version.json
2017-07-07 14:21:01 +02:00
Pascal Vizeli
d31895123e Update version.json 2017-07-07 14:14:55 +02:00
Pascal Vizeli
6c1456902e Update util.py (#92)
Add an optional extended description…
2017-07-07 14:12:26 +02:00
Pascal Vizeli
03bed162f4 Pump version to 0.42 2017-07-07 00:30:04 +02:00
42 changed files with 2565 additions and 1243 deletions

232
API.md
View File

@@ -1,10 +1,11 @@
# HassIO Server
# Hass.io Server
## HassIO REST API
## Hass.io RESTful API
Interface for HomeAssistant to control things from supervisor.
Interface for Home Assistant to control things from supervisor.
On error:
```json
{
"result": "error",
@@ -12,7 +13,8 @@ On error:
}
```
On success
On success:
```json
{
"result": "ok",
@@ -20,10 +22,9 @@ On success
}
```
### HassIO
### Hass.io
- GET `/supervisor/ping`
- GET `/supervisor/info`
The addons from `addons` are only installed one.
@@ -40,62 +41,23 @@ The addons from `addons` are only installed one.
"name": "xy bla",
"slug": "xy",
"description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "12345678|null",
"version": "LAST_VERSION",
"installed": "INSTALL_VERSION",
"detached": "bool",
"build": "bool",
"url": "null|url"
"logo": "bool",
"state": "started|stopped",
}
],
"addons_repositories": [
"REPO_URL"
],
"snapshots": [
{
"slug": "SLUG",
"data": "ISO",
"name": "Custom name"
}
]
}
```
- GET `/supervisor/addons`
Get all available addons
```json
{
"addons": [
{
"name": "xy bla",
"slug": "xy",
"description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool",
"build": "bool",
"url": "null|url"
}
],
"repositories": [
{
"slug": "12345678",
"name": "Repitory Name|unknown",
"source": "URL_OF_REPOSITORY",
"url": "WEBSITE|REPOSITORY",
"maintainer": "BLA BLU <fla@dld.ch>|unknown"
}
]
}
```
- POST `/supervisor/update`
Optional:
```json
{
"version": "VERSION"
@@ -103,6 +65,7 @@ Optional:
```
- POST `/supervisor/options`
```json
{
"beta_channel": "true|false",
@@ -119,11 +82,12 @@ Reload addons/version.
- GET `/supervisor/logs`
Output the raw docker log
Output is the raw docker log.
### Security
- GET `/security/info`
```json
{
"initialize": "bool",
@@ -132,6 +96,7 @@ Output the raw docker log
```
- POST `/security/options`
```json
{
"password": "xy"
@@ -139,6 +104,7 @@ Output the raw docker log
```
- POST `/security/totp`
```json
{
"password": "xy"
@@ -157,7 +123,24 @@ Return QR-Code
### Backup/Snapshot
- GET `/snapshots`
```json
{
"snapshots": [
{
"slug": "SLUG",
"date": "ISO",
"name": "Custom name"
}
]
}
```
- POST `/snapshots/reload`
- POST `/snapshots/new/full`
```json
{
"name": "Optional"
@@ -165,6 +148,7 @@ Return QR-Code
```
- POST `/snapshots/new/partial`
```json
{
"name": "Optional",
@@ -176,6 +160,7 @@ Return QR-Code
- POST `/snapshots/reload`
- GET `/snapshots/{slug}/info`
```json
{
"slug": "SNAPSHOT ID",
@@ -200,10 +185,9 @@ Return QR-Code
```
- POST `/snapshots/{slug}/remove`
- POST `/snapshots/{slug}/restore/full`
- POST `/snapshots/{slug}/restore/partial`
```json
{
"homeassistant": "bool",
@@ -213,36 +197,68 @@ Return QR-Code
```
### Host
- POST `/host/reload`
- POST `/host/shutdown`
- POST `/host/reboot`
- GET `/host/info`
See HostControl info command.
```json
{
"type": "",
"version": "",
"last_version": "",
"features": ["shutdown", "reboot", "update", "network_info", "network_control"],
"features": ["shutdown", "reboot", "update", "hostname", "network_info", "network_control"],
"hostname": "",
"os": ""
"os": "",
"audio": {
"input": "0,0",
"output": "0,0"
}
}
```
- POST `/host/options`
```json
{
"audio_input": "0,0",
"audio_output": "0,0"
}
```
- POST `/host/update`
Optional:
```json
{
"version": "VERSION"
}
```
- GET `/host/hardware`
```json
{
"serial": ["/dev/xy"],
"input": ["Input device name"],
"disk": ["/dev/sdax"],
"audio": {
"CARD_ID": {
"name": "xy",
"type": "microphone",
"devices": {
"DEV_ID": "type of device"
}
}
}
}
```
### Network
- GET `/network/info`
```json
{
"hostname": ""
@@ -250,18 +266,14 @@ Optional:
```
- POST `/network/options`
```json
{
"hostname": "",
"mode": "dhcp|fixed",
"ssid": "",
"ip": "",
"netmask": "",
"gateway": ""
}
```
### HomeAssistant
### Home Assistant
- GET `/homeassistant/info`
@@ -269,12 +281,17 @@ Optional:
{
"version": "INSTALL_VERSION",
"last_version": "LAST_VERSION",
"devices": []
"devices": [""],
"image": "str",
"custom": "bool -> if custom image",
"boot": "bool"
}
```
- POST `/homeassistant/update`
Optional:
```json
{
"version": "VERSION"
@@ -283,22 +300,66 @@ Optional:
- GET `/homeassistant/logs`
Output the raw docker log
Output is the raw Docker log.
- POST `/homeassistant/restart`
- POST `/homeassistant/options`
- POST `/homeassistant/check`
- POST `/homeassistant/start`
- POST `/homeassistant/stop`
```json
{
"devices": [],
"image": "Optional|null",
"last_version": "Optional for custom image|null"
}
```
### REST API addons
Image with `null` and last_version with `null` reset this options.
### RESTful for API addons
- GET `/addons`
Get all available addons.
```json
{
"addons": [
{
"name": "xy bla",
"slug": "xy",
"description": "description",
"arch": ["armhf", "aarch64", "i386", "amd64"],
"repository": "core|local|REP_ID",
"version": "LAST_VERSION",
"installed": "none|INSTALL_VERSION",
"detached": "bool",
"build": "bool",
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
"devices": ["/dev/xy"],
"url": "null|url",
"logo": "bool",
"audio": "bool",
"hassio_api": "bool"
}
],
"repositories": [
{
"slug": "12345678",
"name": "Repitory Name|unknown",
"source": "URL_OF_REPOSITORY",
"url": "WEBSITE|REPOSITORY",
"maintainer": "BLA BLU <fla@dld.ch>|unknown"
}
]
}
```
- POST `/addons/reload`
- GET `/addons/{addon}/info`
```json
{
"name": "xy bla",
@@ -314,11 +375,22 @@ Output the raw docker log
"build": "bool",
"options": "{}",
"network": "{}|null",
"host_network": "bool"
"host_network": "bool",
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
"devices": ["/dev/xy"],
"logo": "bool",
"hassio_api": "bool",
"webui": "null|http(s)://[HOST]:port/xy/zx",
"audio": "bool",
"audio_input": "null|0,0",
"audio_output": "null|0,0"
}
```
- GET `/addons/{addon}/logo`
- POST `/addons/{addon}/options`
```json
{
"boot": "auto|manual",
@@ -327,17 +399,21 @@ Output the raw docker log
"CONTAINER": "port|[ip, port]"
},
"options": {},
"audio_output": "null|0,0",
"audio_input": "null|0,0"
}
```
For reset custom network settings, set it `null`.
For reset custom network/audio settings, set it `null`.
- POST `/addons/{addon}/start`
- POST `/addons/{addon}/stop`
- POST `/addons/{addon}/install`
Optional:
```json
{
"version": "VERSION"
@@ -347,7 +423,9 @@ Optional:
- POST `/addons/{addon}/uninstall`
- POST `/addons/{addon}/update`
Optional:
```json
{
"version": "VERSION"
@@ -356,15 +434,20 @@ Optional:
- GET `/addons/{addon}/logs`
Output the raw docker log
Output is the raw Docker log.
- POST `/addons/{addon}/restart`
- POST `/addons/{addon}/rebuild`
Only supported for local build addons
## Host Control
Communicate over unix socket with a host daemon.
Communicate over UNIX socket with a host daemon.
- commands
```
# info
-> {'type', 'version', 'last_version', 'features', 'hostname'}
@@ -383,7 +466,8 @@ Communicate over unix socket with a host daemon.
# network int route xy
```
features:
Features:
- shutdown
- reboot
- update

218
LICENSE
View File

@@ -1,29 +1,201 @@
BSD 3-Clause License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2017, Pascal Vizeli
All rights reserved.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Definitions.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017 Pascal Vizeli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,13 +1,13 @@
# HassIO
# Hass.io
### First private cloud solution for home automation.
Hass.io is a Docker based system for managing your Home Assistant installation and related applications. The system is controlled via Home Assistant which communicates with the supervisor. The supervisor provides an API to manage the installation. This includes changing network settings or installing and updating software.
![](misc/hassio.png?raw=true)
[HassIO-Addons](https://github.com/home-assistant/hassio-addons) | [HassIO-Build](https://github.com/home-assistant/hassio-build)
**HassIO is under active development and is not ready yet for production use.**
- [Hass.io Addons](https://github.com/home-assistant/hassio-addons)
- [Hass.io Build](https://github.com/home-assistant/hassio-build)
## Installation

View File

@@ -15,11 +15,11 @@ BUILTIN_REPOSITORIES = set((REPOSITORY_CORE, REPOSITORY_LOCAL))
class AddonManager(object):
"""Manage addons inside HassIO."""
def __init__(self, config, loop, dock):
def __init__(self, config, loop, docker):
"""Initialize docker base wrapper."""
self.loop = loop
self.config = config
self.dock = dock
self.docker = docker
self.data = Data(config)
self.addons = {}
self.repositories = {}
@@ -78,7 +78,7 @@ class AddonManager(object):
# don't add built-in repository to config
if url not in BUILTIN_REPOSITORIES:
self.config.addons_repositories = url
self.config.add_addon_repository(url)
tasks = [_add_repository(url) for url in new_rep - old_rep]
if tasks:
@@ -108,7 +108,7 @@ class AddonManager(object):
tasks = []
for addon_slug in add_addons:
addon = Addon(
self.config, self.loop, self.dock, self.data, addon_slug)
self.config, self.loop, self.docker, self.data, addon_slug)
tasks.append(addon.load())
self.addons[addon_slug] = addon

View File

@@ -8,43 +8,47 @@ import shutil
import tarfile
from tempfile import TemporaryDirectory
from deepmerge import Merger
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .validate import (
validate_options, SCHEMA_ADDON_SNAPSHOT, MAP_VOLUME)
validate_options, SCHEMA_ADDON_SNAPSHOT, RE_VOLUME)
from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_BOOT, ATTR_MAP,
ATTR_OPTIONS, ATTR_PORTS, ATTR_SCHEMA, ATTR_IMAGE, ATTR_REPOSITORY,
ATTR_URL, ATTR_ARCH, ATTR_LOCATON, ATTR_DEVICES, ATTR_ENVIRONMENT,
ATTR_HOST_NETWORK, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_STARTUP,
STATE_STARTED, STATE_STOPPED, STATE_NONE, ATTR_USER, ATTR_SYSTEM,
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK)
ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI,
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT)
from .util import check_installed
from ..dock.addon import DockerAddon
from ..tools import write_json_file, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
RE_WEBUI = re.compile(r"^(.*\[HOST\]:)\[PORT:(\d+)\](.*)$")
MERGE_OPT = Merger([(dict, ['merge'])], ['override'], ['override'])
class Addon(object):
"""Hold data for addon inside HassIO."""
def __init__(self, config, loop, dock, data, slug):
def __init__(self, config, loop, docker, data, slug):
"""Initialize data holder."""
self.loop = loop
self.config = config
self.data = data
self._id = slug
self.addon_docker = DockerAddon(config, loop, dock, self)
self.docker = DockerAddon(config, loop, docker, self)
async def load(self):
"""Async initialize of object."""
if self.is_installed:
await self.addon_docker.attach()
await self.docker.attach()
@property
def slug(self):
@@ -102,10 +106,10 @@ class Addon(object):
def options(self):
"""Return options with local changes."""
if self.is_installed:
return {
**self.data.system[self._id][ATTR_OPTIONS],
**self.data.user[self._id][ATTR_OPTIONS],
}
return MERGE_OPT.merge(
self.data.system[self._id][ATTR_OPTIONS],
self.data.user[self._id][ATTR_OPTIONS],
)
return self.data.cache[self._id][ATTR_OPTIONS]
@options.setter
@@ -130,7 +134,8 @@ class Addon(object):
@property
def auto_update(self):
"""Return if auto update is enable."""
return self.data.user[self._id][ATTR_AUTO_UPDATE]
if ATTR_AUTO_UPDATE in self.data.user.get(self._id, {}):
return self.data.user[self._id][ATTR_AUTO_UPDATE]
@auto_update.setter
def auto_update(self, value):
@@ -173,8 +178,8 @@ class Addon(object):
@property
def ports(self):
"""Return ports of addon."""
if self.network_mode != 'bridge' or ATTR_PORTS not in self._mesh:
return
if self.host_network or ATTR_PORTS not in self._mesh:
return None
if not self.is_installed or \
ATTR_NETWORK not in self.data.user[self._id]:
@@ -197,11 +202,28 @@ class Addon(object):
self.data.save()
@property
def network_mode(self):
"""Return network mode of addon."""
if self._mesh[ATTR_HOST_NETWORK]:
return 'host'
return 'bridge'
def webui(self):
"""Return URL to webui or None."""
if ATTR_WEBUI not in self._mesh:
return None
webui = self._mesh[ATTR_WEBUI]
dock_port = RE_WEBUI.sub(r"\2", webui)
if self.ports is None:
real_port = dock_port
else:
real_port = self.ports.get("{}/tcp".format(dock_port), dock_port)
# for interface config or port lists
if isinstance(real_port, (tuple, list)):
real_port = real_port[-1]
return RE_WEBUI.sub(r"\g<1>{}\g<3>".format(real_port), webui)
@property
def host_network(self):
"""Return True if addon run on host network."""
return self._mesh[ATTR_HOST_NETWORK]
@property
def devices(self):
@@ -223,11 +245,66 @@ class Addon(object):
"""Return list of privilege."""
return self._mesh.get(ATTR_PRIVILEGED)
@property
def use_hassio_api(self):
"""Return True if the add-on access to hassio api."""
return self._mesh[ATTR_HASSIO_API]
@property
def with_audio(self):
"""Return True if the add-on access to audio."""
return self._mesh[ATTR_AUDIO]
@property
def audio_output(self):
"""Return ALSA config for output or None."""
if not self.with_audio:
return None
setting = self.config.audio_output
if self.is_installed and ATTR_AUDIO_OUTPUT in self.data.user[self._id]:
setting = self.data.user[self._id][ATTR_AUDIO_OUTPUT]
return setting
@audio_output.setter
def audio_output(self, value):
"""Set/remove custom audio output settings."""
if value is None:
self.data.user[self._id].pop(ATTR_AUDIO_OUTPUT, None)
else:
self.data.user[self._id][ATTR_AUDIO_OUTPUT] = value
self.data.save()
@property
def audio_input(self):
"""Return ALSA config for input or None."""
if not self.with_audio:
return
setting = self.config.audio_input
if self.is_installed and ATTR_AUDIO_INPUT in self.data.user[self._id]:
setting = self.data.user[self._id][ATTR_AUDIO_INPUT]
return setting
@audio_input.setter
def audio_input(self, value):
"""Set/remove custom audio input settings."""
if value is None:
self.data.user[self._id].pop(ATTR_AUDIO_INPUT, None)
else:
self.data.user[self._id][ATTR_AUDIO_INPUT] = value
self.data.save()
@property
def url(self):
"""Return url of addon."""
return self._mesh.get(ATTR_URL)
@property
def with_logo(self):
"""Return True if a logo exists."""
return self.path_logo.exists()
@property
def supported_arch(self):
"""Return list of supported arch."""
@@ -273,15 +350,20 @@ class Addon(object):
return PurePath(self.config.path_extern_addons_data, self._id)
@property
def path_addon_options(self):
def path_options(self):
"""Return path to addons options."""
return Path(self.path_data, "options.json")
@property
def path_addon_location(self):
def path_location(self):
"""Return path to this addon."""
return Path(self._mesh[ATTR_LOCATON])
@property
def path_logo(self):
"""Return path to addon logo."""
return Path(self.path_location, 'logo.png')
def write_options(self):
"""Return True if addon options is written to data."""
schema = self.schema
@@ -289,7 +371,7 @@ class Addon(object):
try:
schema(options)
return write_json_file(self.path_addon_options, options)
return write_json_file(self.path_options, options)
except vol.Invalid as ex:
_LOGGER.error("Addon %s have wrong options -> %s", self._id,
humanize_error(options, ex))
@@ -305,6 +387,36 @@ class Addon(object):
return vol.Schema(dict)
return vol.Schema(vol.All(dict, validate_options(raw_schema)))
def test_udpate_schema(self):
"""Check if the exists config valid after update."""
if not self.is_installed or self.is_detached:
return True
# load next schema
new_raw_schema = self.data.cache[self._id][ATTR_SCHEMA]
default_options = self.data.cache[self._id][ATTR_OPTIONS]
# if disabled
if isinstance(new_raw_schema, bool):
return True
# merge options
options = {
**self.data.user[self._id][ATTR_OPTIONS],
**default_options,
}
# create voluptuous
new_schema = \
vol.Schema(vol.All(dict, validate_options(new_raw_schema)))
# validate
try:
new_schema(options)
except vol.Invalid:
return False
return True
async def install(self, version=None):
"""Install a addon."""
if self.config.arch not in self.supported_arch:
@@ -322,7 +434,7 @@ class Addon(object):
self.path_data.mkdir()
version = version or self.last_version
if not await self.addon_docker.install(version):
if not await self.docker.install(version):
return False
self._set_install(version)
@@ -331,7 +443,7 @@ class Addon(object):
@check_installed
async def uninstall(self):
"""Remove a addon."""
if not await self.addon_docker.remove():
if not await self.docker.remove():
return False
if self.path_data.is_dir():
@@ -347,45 +459,82 @@ class Addon(object):
if not self.is_installed:
return STATE_NONE
if await self.addon_docker.is_running():
if await self.docker.is_running():
return STATE_STARTED
return STATE_STOPPED
@check_installed
async def start(self):
"""Set options and start addon."""
return await self.addon_docker.run()
def start(self):
"""Set options and start addon.
Return a coroutine.
"""
return self.docker.run()
@check_installed
async def stop(self):
"""Stop addon."""
return await self.addon_docker.stop()
def stop(self):
"""Stop addon.
Return a coroutine.
"""
return self.docker.stop()
@check_installed
async def update(self, version=None):
"""Update addon."""
version = version or self.last_version
last_state = await self.state()
if version == self.version_installed:
_LOGGER.warning(
"Addon %s is already installed in %s", self._id, version)
return True
if not await self.addon_docker.update(version):
return False
if not await self.docker.update(version):
return False
self._set_update(version)
# restore state
if last_state == STATE_STARTED:
await self.docker.run()
return True
@check_installed
async def restart(self):
"""Restart addon."""
return await self.addon_docker.restart()
def restart(self):
"""Restart addon.
Return a coroutine.
"""
return self.docker.restart()
@check_installed
async def logs(self):
"""Return addons log output."""
return await self.addon_docker.logs()
def logs(self):
"""Return addons log output.
Return a coroutine.
"""
return self.docker.logs()
@check_installed
async def rebuild(self):
"""Performe a rebuild of local build addon."""
last_state = await self.state()
if not self.need_build:
_LOGGER.error("Can't rebuild a none local build addon!")
return False
# remove docker container but not addon config
if not await self.docker.remove():
return False
if not await self.docker.install(self.version_installed):
return False
# restore state
if last_state == STATE_STARTED:
await self.docker.run()
return True
@check_installed
async def snapshot(self, tar_file):
@@ -393,7 +542,7 @@ class Addon(object):
with TemporaryDirectory(dir=str(self.config.path_tmp)) as temp:
# store local image
if self.need_build and not await \
self.addon_docker.export_image(Path(temp, "image.tar")):
self.docker.export_image(Path(temp, "image.tar")):
return False
data = {
@@ -458,15 +607,15 @@ class Addon(object):
# check version / restore image
version = data[ATTR_VERSION]
if version != self.addon_docker.version:
if version != self.docker.version:
image_file = Path(temp, "image.tar")
if image_file.is_file():
await self.addon_docker.import_image(image_file, version)
await self.docker.import_image(image_file, version)
else:
if await self.addon_docker.install(version):
await self.addon_docker.cleanup()
if await self.docker.install(version):
await self.docker.cleanup()
else:
await self.addon_docker.stop()
await self.docker.stop()
# restore data
def _restore_data():

View File

@@ -3,65 +3,30 @@ import copy
import logging
import json
from pathlib import Path
import re
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .util import extract_hash_from_path
from .validate import (
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG,
MAP_VOLUME)
SCHEMA_ADDON_CONFIG, SCHEMA_ADDON_FILE, SCHEMA_REPOSITORY_CONFIG)
from ..const import (
FILE_HASSIO_ADDONS, ATTR_VERSION, ATTR_SLUG, ATTR_REPOSITORY, ATTR_LOCATON,
REPOSITORY_CORE, REPOSITORY_LOCAL, ATTR_USER, ATTR_SYSTEM)
from ..tools import read_json_file, write_json_file
from ..tools import JsonConfig, read_json_file
_LOGGER = logging.getLogger(__name__)
RE_VOLUME = re.compile(MAP_VOLUME)
class Data(object):
class Data(JsonConfig):
"""Hold data for addons inside HassIO."""
def __init__(self, config):
"""Initialize data holder."""
self._file = FILE_HASSIO_ADDONS
self._data = {}
super().__init__(FILE_HASSIO_ADDONS, SCHEMA_ADDON_FILE)
self.config = config
self._cache = {}
self._repositories = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons.json -> %s",
humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = SCHEMA_ADDON_FILE(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse addons data -> %s",
humanize_error(self._data, ex))
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
self._cache = {}
@property
def user(self):
@@ -149,7 +114,7 @@ class Data(object):
addon_config[ATTR_LOCATON] = str(addon.parent)
self._cache[addon_slug] = addon_config
except OSError:
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", addon)
except vol.Invalid as ex:

View File

@@ -1,6 +1,7 @@
"""Init file for HassIO addons git."""
import asyncio
import logging
import functools as ft
from pathlib import Path
import shutil
@@ -48,7 +49,9 @@ class GitRepo(object):
try:
_LOGGER.info("Clone addon %s repository", self.url)
self.repo = await self.loop.run_in_executor(
None, git.Repo.clone_from, self.url, str(self.path))
None, ft.partial(
git.Repo.clone_from, self.url, str(self.path),
recursive=True))
except (git.InvalidGitRepositoryError, git.NoSuchPathError,
git.GitCommandError) as err:

View File

@@ -1,19 +1,23 @@
"""Validate addons options schema."""
import re
import voluptuous as vol
from ..const import (
ATTR_NAME, ATTR_VERSION, ATTR_SLUG, ATTR_DESCRIPTON, ATTR_STARTUP,
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE, STARTUP_AFTER,
STARTUP_BEFORE, STARTUP_INITIALIZE, BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA,
ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER, ATTR_ARCH, ATTR_DEVICES,
ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64,
ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED, ATTR_USER, ATTR_STATE, ATTR_SYSTEM,
STATE_STARTED, STATE_STOPPED, ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT,
ATTR_NETWORK, ATTR_AUTO_UPDATE)
from ..validate import NETWORK_PORT, DOCKER_PORTS
ATTR_BOOT, ATTR_MAP, ATTR_OPTIONS, ATTR_PORTS, STARTUP_ONCE,
STARTUP_SYSTEM, STARTUP_SERVICES, STARTUP_APPLICATION, STARTUP_INITIALIZE,
BOOT_AUTO, BOOT_MANUAL, ATTR_SCHEMA, ATTR_IMAGE, ATTR_URL, ATTR_MAINTAINER,
ATTR_ARCH, ATTR_DEVICES, ATTR_ENVIRONMENT, ATTR_HOST_NETWORK, ARCH_ARMHF,
ARCH_AARCH64, ARCH_AMD64, ARCH_I386, ATTR_TMPFS, ATTR_PRIVILEGED,
ATTR_USER, ATTR_STATE, ATTR_SYSTEM, STATE_STARTED, STATE_STOPPED,
ATTR_LOCATON, ATTR_REPOSITORY, ATTR_TIMEOUT, ATTR_NETWORK,
ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API)
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL
MAP_VOLUME = r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$"
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share)(?::(rw|:ro))?$")
V_STR = 'str'
V_INT = 'int'
@@ -22,17 +26,43 @@ V_BOOL = 'bool'
V_EMAIL = 'email'
V_URL = 'url'
V_PORT = 'port'
V_MATCH = 'match'
ADDON_ELEMENT = vol.In([V_STR, V_INT, V_FLOAT, V_BOOL, V_EMAIL, V_URL, V_PORT])
RE_SCHEMA_ELEMENT = re.compile(
r"^(?:"
r"|str|bool|email|url|port"
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
r"|match\((?P<match>.*)\)"
r")$"
)
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
ARCH_ALL = [
ARCH_ARMHF, ARCH_AARCH64, ARCH_AMD64, ARCH_I386
]
PRIVILEGE_ALL = [
"NET_ADMIN"
STARTUP_ALL = [
STARTUP_ONCE, STARTUP_INITIALIZE, STARTUP_SYSTEM, STARTUP_SERVICES,
STARTUP_APPLICATION
]
PRIVILEGED_ALL = [
"NET_ADMIN",
"SYS_ADMIN",
"SYS_RAWIO"
]
def _simple_startup(value):
"""Simple startup schema."""
if value == "before":
return STARTUP_SERVICES
if value == "after":
return STARTUP_APPLICATION
return value
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_CONFIG = vol.Schema({
@@ -43,24 +73,27 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
vol.Optional(ATTR_URL): vol.Url(),
vol.Optional(ATTR_ARCH, default=ARCH_ALL): [vol.In(ARCH_ALL)],
vol.Required(ATTR_STARTUP):
vol.In([STARTUP_BEFORE, STARTUP_AFTER, STARTUP_ONCE,
STARTUP_INITIALIZE]),
vol.All(_simple_startup, vol.In(STARTUP_ALL)),
vol.Required(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
vol.Optional(ATTR_WEBUI):
vol.Match(r"^(?:https?):\/\/\[HOST\]:\[PORT:\d+\].*$"),
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
vol.Optional(ATTR_TMPFS):
vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
vol.Optional(ATTR_MAP, default=[]): [vol.Match(MAP_VOLUME)],
vol.Optional(ATTR_MAP, default=[]): [vol.Match(RE_VOLUME)],
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGE_ALL)],
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
vol.Required(ATTR_OPTIONS): dict,
vol.Required(ATTR_SCHEMA): vol.Any({
vol.Coerce(str): vol.Any(ADDON_ELEMENT, [
vol.Any(ADDON_ELEMENT, {vol.Coerce(str): ADDON_ELEMENT})
])
}, False),
vol.Required(ATTR_SCHEMA): vol.Any(vol.Schema({
vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [
vol.Any(SCHEMA_ELEMENT, {vol.Coerce(str): SCHEMA_ELEMENT})
], vol.Schema({vol.Coerce(str): SCHEMA_ELEMENT}))
}), False),
vol.Optional(ATTR_IMAGE): vol.Match(r"\w*/\w*"),
vol.Optional(ATTR_TIMEOUT, default=10):
vol.All(vol.Coerce(int), vol.Range(min=10, max=120))
@@ -78,11 +111,13 @@ SCHEMA_REPOSITORY_CONFIG = vol.Schema({
# pylint: disable=no-value-for-parameter
SCHEMA_ADDON_USER = vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Required(ATTR_OPTIONS): dict,
vol.Optional(ATTR_OPTIONS, default={}): dict,
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
vol.Optional(ATTR_BOOT):
vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
})
@@ -124,8 +159,11 @@ def validate_options(raw_schema):
typ = raw_schema[key]
try:
if isinstance(typ, list):
# nested value
options[key] = _nested_validate(typ[0], value, key)
# nested value list
options[key] = _nested_validate_list(typ[0], value, key)
elif isinstance(typ, dict):
# nested value dict
options[key] = _nested_validate_dict(typ, value, key)
else:
# normal value
options[key] = _single_validate(typ, value, key)
@@ -146,28 +184,40 @@ def _single_validate(typ, value, key):
if value is None:
raise vol.Invalid("Missing required option '{}'.".format(key))
if typ == V_STR:
return str(value)
elif typ == V_INT:
return int(value)
elif typ == V_FLOAT:
return float(value)
elif typ == V_BOOL:
return vol.Boolean()(value)
elif typ == V_EMAIL:
return vol.Email()(value)
elif typ == V_URL:
return vol.Url()(value)
elif typ == V_PORT:
return NETWORK_PORT(value)
# parse extend data from type
match = RE_SCHEMA_ELEMENT.match(typ)
raise vol.Invalid("Fatal error for {} type {}.".format(key, typ))
# prepare range
range_args = {}
for group_name in ('i_min', 'i_max', 'f_min', 'f_max'):
group_value = match.group(group_name)
if group_value:
range_args[group_name[2:]] = float(group_value)
if typ.startswith(V_STR):
return str(value)
elif typ.startswith(V_INT):
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
elif typ.startswith(V_FLOAT):
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
elif typ.startswith(V_BOOL):
return vol.Boolean()(value)
elif typ.startswith(V_EMAIL):
return vol.Email()(value)
elif typ.startswith(V_URL):
return vol.Url()(value)
elif typ.startswith(V_PORT):
return NETWORK_PORT(value)
elif typ.startswith(V_MATCH):
return vol.Match(match.group('match'))(str(value))
raise vol.Invalid("Fatal error for {} type {}".format(key, typ))
except ValueError:
raise vol.Invalid(
"Type {} error for '{}' on {}.".format(typ, value, key)) from None
def _nested_validate(typ, data_list, key):
def _nested_validate_list(typ, data_list, key):
"""Validate nested items."""
options = []
@@ -178,7 +228,7 @@ def _nested_validate(typ, data_list, key):
for c_key, c_value in element.items():
if c_key not in typ:
raise vol.Invalid(
"Unknown nested options {}.".format(c_key))
"Unknown nested options {}".format(c_key))
c_options[c_key] = _single_validate(typ[c_key], c_value, c_key)
options.append(c_options)
@@ -187,3 +237,16 @@ def _nested_validate(typ, data_list, key):
options.append(_single_validate(typ, element, key))
return options
def _nested_validate_dict(typ, data_dict, key):
"""Validate nested items."""
options = {}
for c_key, c_value in data_dict.items():
if c_key not in typ:
raise vol.Invalid("Unknow nested dict options {}".format(c_key))
options[c_key] = _single_validate(typ[c_key], c_value, c_key)
return options

View File

@@ -28,14 +28,16 @@ class RestAPI(object):
self._handler = None
self.server = None
def register_host(self, host_control):
def register_host(self, host_control, hardware):
"""Register hostcontrol function."""
api_host = APIHost(self.config, self.loop, host_control)
api_host = APIHost(self.config, self.loop, host_control, hardware)
self.webapp.router.add_get('/host/info', api_host.info)
self.webapp.router.add_get('/host/hardware', api_host.hardware)
self.webapp.router.add_post('/host/reboot', api_host.reboot)
self.webapp.router.add_post('/host/shutdown', api_host.shutdown)
self.webapp.router.add_post('/host/update', api_host.update)
self.webapp.router.add_post('/host/options', api_host.options)
def register_network(self, host_control):
"""Register network function."""
@@ -45,16 +47,14 @@ class RestAPI(object):
self.webapp.router.add_post('/network/options', api_net.options)
def register_supervisor(self, supervisor, snapshots, addons, host_control,
websession):
updater):
"""Register supervisor function."""
api_supervisor = APISupervisor(
self.config, self.loop, supervisor, snapshots, addons,
host_control, websession)
host_control, updater)
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/addons', api_supervisor.available_addons)
self.webapp.router.add_post(
'/supervisor/update', api_supervisor.update)
self.webapp.router.add_post(
@@ -68,15 +68,21 @@ class RestAPI(object):
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/logs', api_hass.logs)
self.webapp.router.add_post('/homeassistant/options', api_hass.options)
self.webapp.router.add_post('/homeassistant/update', api_hass.update)
self.webapp.router.add_post('/homeassistant/restart', api_hass.restart)
self.webapp.router.add_get('/homeassistant/logs', api_hass.logs)
self.webapp.router.add_post('/homeassistant/stop', api_hass.stop)
self.webapp.router.add_post('/homeassistant/start', api_hass.start)
self.webapp.router.add_post('/homeassistant/check', api_hass.check)
def register_addons(self, addons):
"""Register homeassistant function."""
api_addons = APIAddons(self.config, self.loop, addons)
self.webapp.router.add_get('/addons', api_addons.list)
self.webapp.router.add_post('/addons/reload', api_addons.reload)
self.webapp.router.add_get('/addons/{addon}/info', api_addons.info)
self.webapp.router.add_post(
'/addons/{addon}/install', api_addons.install)
@@ -90,7 +96,10 @@ class RestAPI(object):
'/addons/{addon}/update', api_addons.update)
self.webapp.router.add_post(
'/addons/{addon}/options', api_addons.options)
self.webapp.router.add_post(
'/addons/{addon}/rebuild', api_addons.rebuild)
self.webapp.router.add_get('/addons/{addon}/logs', api_addons.logs)
self.webapp.router.add_get('/addons/{addon}/logo', api_addons.logo)
def register_security(self):
"""Register security function."""
@@ -105,6 +114,9 @@ class RestAPI(object):
"""Register snapshots function."""
api_snapshots = APISnapshots(self.config, self.loop, snapshots)
self.webapp.router.add_get('/snapshots', api_snapshots.list)
self.webapp.router.add_post('/snapshots/reload', api_snapshots.reload)
self.webapp.router.add_post(
'/snapshots/new/full', api_snapshots.snapshot_full)
self.webapp.router.add_post(

View File

@@ -9,8 +9,11 @@ from .util import api_process, api_process_raw, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_STATE, ATTR_BOOT, ATTR_OPTIONS,
ATTR_URL, ATTR_DESCRIPTON, ATTR_DETACHED, ATTR_NAME, ATTR_REPOSITORY,
ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK,
BOOT_AUTO, BOOT_MANUAL)
ATTR_BUILD, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_HOST_NETWORK, ATTR_SLUG,
ATTR_SOURCE, ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_ARCH, ATTR_MAINTAINER,
ATTR_INSTALLED, ATTR_LOGO, ATTR_WEBUI, ATTR_DEVICES, ATTR_PRIVILEGED,
ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API,
BOOT_AUTO, BOOT_MANUAL, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY)
from ..validate import DOCKER_PORTS
_LOGGER = logging.getLogger(__name__)
@@ -47,6 +50,58 @@ class APIAddons(object):
return addon
@staticmethod
def _pretty_devices(addon):
"""Return a simplified device list."""
dev_list = addon.devices
if not dev_list:
return
return [row.split(':')[0] for row in dev_list]
@api_process
async def list(self, request):
"""Return all addons / repositories ."""
data_addons = []
for addon in self.addons.list_addons:
data_addons.append({
ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed,
ATTR_ARCH: addon.supported_arch,
ATTR_DETACHED: addon.is_detached,
ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build,
ATTR_PRIVILEGED: addon.privileged,
ATTR_DEVICES: self._pretty_devices(addon),
ATTR_URL: addon.url,
ATTR_LOGO: addon.with_logo,
ATTR_HASSIO_API: addon.use_hassio_api,
ATTR_AUDIO: addon.with_audio,
})
data_repositories = []
for repository in self.addons.list_repositories:
data_repositories.append({
ATTR_SLUG: repository.slug,
ATTR_NAME: repository.name,
ATTR_SOURCE: repository.source,
ATTR_URL: repository.url,
ATTR_MAINTAINER: repository.maintainer,
})
return {
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: data_repositories,
}
@api_process
async def reload(self, request):
"""Reload all addons data."""
await asyncio.shield(self.addons.reload(), loop=self.loop)
return True
@api_process
async def info(self, request):
"""Return addon information."""
@@ -66,7 +121,15 @@ class APIAddons(object):
ATTR_DETACHED: addon.is_detached,
ATTR_BUILD: addon.need_build,
ATTR_NETWORK: addon.ports,
ATTR_HOST_NETWORK: addon.network_mode == 'host',
ATTR_HOST_NETWORK: addon.host_network,
ATTR_PRIVILEGED: addon.privileged,
ATTR_DEVICES: self._pretty_devices(addon),
ATTR_LOGO: addon.with_logo,
ATTR_WEBUI: addon.webui,
ATTR_HASSIO_API: addon.use_hassio_api,
ATTR_AUDIO: addon.with_audio,
ATTR_AUDIO_INPUT: addon.audio_input,
ATTR_AUDIO_OUTPUT: addon.audio_output,
}
@api_process
@@ -88,6 +151,10 @@ class APIAddons(object):
addon.auto_update = body[ATTR_AUTO_UPDATE]
if ATTR_NETWORK in body:
addon.ports = body[ATTR_NETWORK]
if ATTR_AUDIO_INPUT in body:
addon.audio_input = body[ATTR_AUDIO_INPUT]
if ATTR_AUDIO_OUTPUT in body:
addon.audio_output = body[ATTR_AUDIO_OUTPUT]
return True
@@ -96,19 +163,19 @@ class APIAddons(object):
"""Install addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request, check_installed=False)
version = body.get(ATTR_VERSION)
version = body.get(ATTR_VERSION, addon.last_version)
return await asyncio.shield(
addon.install(version=version), loop=self.loop)
@api_process
async def uninstall(self, request):
def uninstall(self, request):
"""Uninstall addon."""
addon = self._extract_addon(request)
return await asyncio.shield(addon.uninstall(), loop=self.loop)
return asyncio.shield(addon.uninstall(), loop=self.loop)
@api_process
async def start(self, request):
def start(self, request):
"""Start addon."""
addon = self._extract_addon(request)
@@ -119,32 +186,54 @@ class APIAddons(object):
except vol.Invalid as ex:
raise RuntimeError(humanize_error(options, ex)) from None
return await asyncio.shield(addon.start(), loop=self.loop)
return asyncio.shield(addon.start(), loop=self.loop)
@api_process
async def stop(self, request):
def stop(self, request):
"""Stop addon."""
addon = self._extract_addon(request)
return await asyncio.shield(addon.stop(), loop=self.loop)
return asyncio.shield(addon.stop(), loop=self.loop)
@api_process
async def update(self, request):
"""Update addon."""
body = await api_validate(SCHEMA_VERSION, request)
addon = self._extract_addon(request)
version = body.get(ATTR_VERSION)
version = body.get(ATTR_VERSION, addon.last_version)
if version == addon.version_installed:
raise RuntimeError("Version %s is already in use", version)
return await asyncio.shield(
addon.update(version=version), loop=self.loop)
@api_process
async def restart(self, request):
def restart(self, request):
"""Restart addon."""
addon = self._extract_addon(request)
return await asyncio.shield(addon.restart(), loop=self.loop)
return asyncio.shield(addon.restart(), loop=self.loop)
@api_process_raw
@api_process
def rebuild(self, request):
"""Rebuild local build addon."""
addon = self._extract_addon(request)
if not addon.need_build:
raise RuntimeError("Only local build addons are supported")
return asyncio.shield(addon.rebuild(), loop=self.loop)
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return logs from addon."""
addon = self._extract_addon(request)
return addon.logs()
@api_process_raw(CONTENT_TYPE_PNG)
async def logo(self, request):
"""Return logo from addon."""
addon = self._extract_addon(request, check_installed=False)
if not addon.with_logo:
raise RuntimeError("No image found!")
with addon.path_logo.open('rb') as png:
return png.read()

View File

@@ -5,14 +5,21 @@ import logging
import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_DEVICES, ATTR_IMAGE, ATTR_CUSTOM,
ATTR_BOOT, CONTENT_TYPE_BINARY)
from ..validate import HASS_DEVICES
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_DEVICES): HASS_DEVICES,
vol.Optional(ATTR_BOOT): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Any(None, vol.Coerce(str)),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'):
vol.Any(None, vol.Coerce(str)),
})
SCHEMA_VERSION = vol.Schema({
@@ -34,8 +41,11 @@ class APIHomeAssistant(object):
"""Return host information."""
return {
ATTR_VERSION: self.homeassistant.version,
ATTR_LAST_VERSION: self.config.last_homeassistant,
ATTR_DEVICES: self.config.homeassistant_devices,
ATTR_LAST_VERSION: self.homeassistant.last_version,
ATTR_IMAGE: self.homeassistant.image,
ATTR_DEVICES: self.homeassistant.devices,
ATTR_CUSTOM: self.homeassistant.is_custom_image,
ATTR_BOOT: self.homeassistant.boot,
}
@api_process
@@ -44,7 +54,14 @@ class APIHomeAssistant(object):
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_DEVICES in body:
self.config.homeassistant_devices = body[ATTR_DEVICES]
self.homeassistant.devices = body[ATTR_DEVICES]
if ATTR_IMAGE in body:
self.homeassistant.set_custom(
body[ATTR_IMAGE], body[ATTR_LAST_VERSION])
if ATTR_BOOT in body:
self.homeassistant.boot = body[ATTR_BOOT]
return True
@@ -52,30 +69,39 @@ class APIHomeAssistant(object):
async def update(self, request):
"""Update homeassistant."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.last_homeassistant)
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
version = body.get(ATTR_VERSION, self.homeassistant.last_version)
if version == self.homeassistant.version:
raise RuntimeError("Version is already in use")
raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield(
self.homeassistant.update(version), loop=self.loop)
@api_process
async def restart(self, request):
def stop(self, request):
"""Stop homeassistant."""
return asyncio.shield(self.homeassistant.stop(), loop=self.loop)
@api_process
def start(self, request):
"""Start homeassistant."""
return asyncio.shield(self.homeassistant.run(), loop=self.loop)
@api_process
def restart(self, request):
"""Restart homeassistant."""
if self.homeassistant.in_progress:
raise RuntimeError("Other task is in progress")
return asyncio.shield(self.homeassistant.restart(), loop=self.loop)
return await asyncio.shield(
self.homeassistant.restart(), loop=self.loop)
@api_process_raw
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return homeassistant docker logs.
Return a coroutine.
"""
"""Return homeassistant docker logs."""
return self.homeassistant.logs()
@api_process
async def check(self, request):
"""Check config of homeassistant."""
code, message = await self.homeassistant.check_config()
if not code:
raise RuntimeError(message)
return True

View File

@@ -7,7 +7,9 @@ import voluptuous as vol
from .util import api_process_hostcontrol, api_process, api_validate
from ..const import (
ATTR_VERSION, ATTR_LAST_VERSION, ATTR_TYPE, ATTR_HOSTNAME, ATTR_FEATURES,
ATTR_OS)
ATTR_OS, ATTR_SERIAL, ATTR_INPUT, ATTR_DISK, ATTR_AUDIO, ATTR_AUDIO_INPUT,
ATTR_AUDIO_OUTPUT)
from ..validate import ALSA_CHANNEL
_LOGGER = logging.getLogger(__name__)
@@ -15,15 +17,21 @@ SCHEMA_VERSION = vol.Schema({
vol.Optional(ATTR_VERSION): vol.Coerce(str),
})
SCHEMA_OPTIONS = vol.Schema({
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
})
class APIHost(object):
"""Handle rest api for host functions."""
def __init__(self, config, loop, host_control):
def __init__(self, config, loop, host_control, hardware):
"""Initialize host rest api part."""
self.config = config
self.loop = loop
self.host_control = host_control
self.local_hw = hardware
@api_process
async def info(self, request):
@@ -37,6 +45,18 @@ class APIHost(object):
ATTR_OS: self.host_control.os_info,
}
@api_process
async def options(self, request):
"""Process host options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_AUDIO_OUTPUT in body:
self.config.audio_output = body[ATTR_AUDIO_OUTPUT]
if ATTR_AUDIO_INPUT in body:
self.config.audio_input = body[ATTR_AUDIO_INPUT]
return True
@api_process_hostcontrol
def reboot(self, request):
"""Reboot host."""
@@ -54,7 +74,17 @@ class APIHost(object):
version = body.get(ATTR_VERSION, self.host_control.last_version)
if version == self.host_control.version:
raise RuntimeError("Version is already in use")
raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield(
self.host_control.update(version=version), loop=self.loop)
@api_process
async def hardware(self, request):
"""Return local hardware infos."""
return {
ATTR_SERIAL: self.local_hw.serial_devices,
ATTR_INPUT: self.local_hw.input_devices,
ATTR_DISK: self.local_hw.disk_devices,
ATTR_AUDIO: self.local_hw.audio_devices,
}

View File

@@ -98,5 +98,5 @@ class APISecurity(object):
session = hashlib.sha256(os.urandom(54)).hexdigest()
# store session
self.config.security_sessions = (session, valid_until)
self.config.add_security_session(session, valid_until)
return {ATTR_SESSION: session}

View File

@@ -9,7 +9,7 @@ from ..snapshots.validate import ALL_FOLDERS
from ..const import (
ATTR_NAME, ATTR_SLUG, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_VERSION, ATTR_SIZE, ATTR_FOLDERS, ATTR_TYPE,
ATTR_DEVICES)
ATTR_DEVICES, ATTR_SNAPSHOTS)
_LOGGER = logging.getLogger(__name__)
@@ -47,23 +47,40 @@ class APISnapshots(object):
raise RuntimeError("Snapshot not exists")
return snapshot
@staticmethod
def _addons_list(snapshot):
"""Generate a list with addons data."""
data = []
for addon_data in snapshot.addons:
data.append({
ATTR_SLUG: addon_data[ATTR_SLUG],
ATTR_NAME: addon_data[ATTR_NAME],
ATTR_VERSION: addon_data[ATTR_VERSION],
@api_process
async def list(self, request):
"""Return snapshot list."""
data_snapshots = []
for snapshot in self.snapshots.list_snapshots:
data_snapshots.append({
ATTR_SLUG: snapshot.slug,
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
})
return data
return {
ATTR_SNAPSHOTS: data_snapshots,
}
@api_process
async def reload(self, request):
"""Reload snapshot list."""
await asyncio.shield(self.snapshots.reload(), loop=self.loop)
return True
@api_process
async def info(self, request):
"""Return snapshot info."""
snapshot = self._extract_snapshot(request)
data_addons = []
for addon_data in snapshot.addons:
data_addons.append({
ATTR_SLUG: addon_data[ATTR_SLUG],
ATTR_NAME: addon_data[ATTR_NAME],
ATTR_VERSION: addon_data[ATTR_VERSION],
})
return {
ATTR_SLUG: snapshot.slug,
ATTR_TYPE: snapshot.sys_type,
@@ -74,7 +91,7 @@ class APISnapshots(object):
ATTR_VERSION: snapshot.homeassistant_version,
ATTR_DEVICES: snapshot.homeassistant_devices,
},
ATTR_ADDONS: self._addons_list(snapshot),
ATTR_ADDONS: data_addons,
ATTR_REPOSITORIES: snapshot.repositories,
ATTR_FOLDERS: snapshot.folders,
}
@@ -94,10 +111,10 @@ class APISnapshots(object):
self.snapshots.do_snapshot_partial(**body), loop=self.loop)
@api_process
async def restore_full(self, request):
def restore_full(self, request):
"""Full-Restore a snapshot."""
snapshot = self._extract_snapshot(request)
return await asyncio.shield(
return asyncio.shield(
self.snapshots.do_restore_full(snapshot), loop=self.loop)
@api_process
@@ -108,7 +125,8 @@ class APISnapshots(object):
return await asyncio.shield(
self.snapshots.do_restore_partial(snapshot, **body),
loop=self.loop)
loop=self.loop
)
@api_process
async def remove(self, request):

View File

@@ -6,12 +6,11 @@ import voluptuous as vol
from .util import api_process, api_process_raw, api_validate
from ..const import (
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, ATTR_ARCH,
ATTR_BUILD, ATTR_TIMEZONE, ATTR_DATE, ATTR_SNAPSHOTS)
from ..tools import validate_timezone
ATTR_ADDONS, ATTR_VERSION, ATTR_LAST_VERSION, ATTR_BETA_CHANNEL, ATTR_ARCH,
HASSIO_VERSION, ATTR_ADDONS_REPOSITORIES, ATTR_LOGO, ATTR_REPOSITORY,
ATTR_DESCRIPTON, ATTR_NAME, ATTR_SLUG, ATTR_INSTALLED, ATTR_TIMEZONE,
ATTR_STATE, CONTENT_TYPE_BINARY)
from ..validate import validate_timezone
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +30,7 @@ class APISupervisor(object):
"""Handle rest api for supervisor functions."""
def __init__(self, config, loop, supervisor, snapshots, addons,
host_control, websession):
host_control, updater):
"""Initialize supervisor rest api part."""
self.config = config
self.loop = loop
@@ -39,55 +38,7 @@ class APISupervisor(object):
self.addons = addons
self.snapshots = snapshots
self.host_control = host_control
self.websession = websession
def _addons_list(self, only_installed=False):
"""Return a list of addons."""
data = []
for addon in self.addons.list_addons:
if only_installed and not addon.is_installed:
continue
data.append({
ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug,
ATTR_DESCRIPTON: addon.description,
ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed,
ATTR_ARCH: addon.supported_arch,
ATTR_DETACHED: addon.is_detached,
ATTR_REPOSITORY: addon.repository,
ATTR_BUILD: addon.need_build,
ATTR_URL: addon.url,
})
return data
def _repositories_list(self):
"""Return a list of addons repositories."""
data = []
for repository in self.addons.list_repositories:
data.append({
ATTR_SLUG: repository.slug,
ATTR_NAME: repository.name,
ATTR_SOURCE: repository.source,
ATTR_URL: repository.url,
ATTR_MAINTAINER: repository.maintainer,
})
return data
def _snapshots_list(self):
"""Return a list of available snapshots."""
data = []
for snapshot in self.snapshots.list_snapshots:
data.append({
ATTR_SLUG: snapshot.slug,
ATTR_NAME: snapshot.name,
ATTR_DATE: snapshot.date,
})
return data
self.updater = updater
@api_process
async def ping(self, request):
@@ -97,23 +48,28 @@ class APISupervisor(object):
@api_process
async def info(self, request):
"""Return host information."""
list_addons = []
for addon in self.addons.list_addons:
if addon.is_installed:
list_addons.append({
ATTR_NAME: addon.name,
ATTR_SLUG: addon.slug,
ATTR_DESCRIPTON: addon.description,
ATTR_STATE: await addon.state(),
ATTR_VERSION: addon.last_version,
ATTR_INSTALLED: addon.version_installed,
ATTR_REPOSITORY: addon.repository,
ATTR_LOGO: addon.with_logo,
})
return {
ATTR_VERSION: HASSIO_VERSION,
ATTR_LAST_VERSION: self.config.last_hassio,
ATTR_BETA_CHANNEL: self.config.upstream_beta,
ATTR_LAST_VERSION: self.updater.version_hassio,
ATTR_BETA_CHANNEL: self.updater.beta_channel,
ATTR_ARCH: self.config.arch,
ATTR_TIMEZONE: self.config.timezone,
ATTR_ADDONS: self._addons_list(only_installed=True),
ATTR_ADDONS: list_addons,
ATTR_ADDONS_REPOSITORIES: self.config.addons_repositories,
ATTR_SNAPSHOTS: self._snapshots_list(),
}
@api_process
async def available_addons(self, request):
"""Return information for all available addons."""
return {
ATTR_ADDONS: self._addons_list(),
ATTR_REPOSITORIES: self._repositories_list(),
}
@api_process
@@ -122,7 +78,7 @@ class APISupervisor(object):
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_BETA_CHANNEL in body:
self.config.upstream_beta = body[ATTR_BETA_CHANNEL]
self.updater.beta_channel = body[ATTR_BETA_CHANNEL]
if ATTR_TIMEZONE in body:
self.config.timezone = body[ATTR_TIMEZONE]
@@ -137,10 +93,10 @@ class APISupervisor(object):
async def update(self, request):
"""Update supervisor OS."""
body = await api_validate(SCHEMA_VERSION, request)
version = body.get(ATTR_VERSION, self.config.last_hassio)
version = body.get(ATTR_VERSION, self.updater.version_hassio)
if version == self.supervisor.version:
raise RuntimeError("Version is already in use")
raise RuntimeError("Version {} is already in use".format(version))
return await asyncio.shield(
self.supervisor.update(version), loop=self.loop)
@@ -151,7 +107,7 @@ class APISupervisor(object):
tasks = [
self.addons.reload(),
self.snapshots.reload(),
self.config.fetch_update_infos(self.websession),
self.updater.fetch_data(),
self.host_control.load()
]
results, _ = await asyncio.shield(
@@ -163,10 +119,7 @@ class APISupervisor(object):
return True
@api_process_raw
@api_process_raw(CONTENT_TYPE_BINARY)
def logs(self, request):
"""Return supervisor docker logs.
Return a coroutine.
"""
"""Return supervisor docker logs."""
return self.supervisor.logs()

View File

@@ -9,7 +9,8 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from ..const import (
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR)
JSON_RESULT, JSON_DATA, JSON_MESSAGE, RESULT_OK, RESULT_ERROR,
CONTENT_TYPE_BINARY)
_LOGGER = logging.getLogger(__name__)
@@ -65,25 +66,27 @@ def api_process_hostcontrol(method):
return wrap_hostcontrol
def api_process_raw(method):
"""Wrap function with raw output to rest api."""
async def wrap_api(api, *args, **kwargs):
"""Return api information."""
try:
message = await method(api, *args, **kwargs)
except RuntimeError as err:
message = str(err).encode()
def api_process_raw(content):
"""Wrap content_type into function."""
def wrap_method(method):
"""Wrap function with raw output to rest api."""
async def wrap_api(api, *args, **kwargs):
"""Return api information."""
try:
msg_data = await method(api, *args, **kwargs)
msg_type = content
except RuntimeError as err:
msg_data = str(err).encode()
msg_type = CONTENT_TYPE_BINARY
return web.Response(body=message)
return web.Response(body=msg_data, content_type=msg_type)
return wrap_api
return wrap_api
return wrap_method
def api_return_error(message=None):
"""Return a API error message."""
if message:
_LOGGER.error(message)
return web.json_response({
JSON_RESULT: RESULT_ERROR,
JSON_MESSAGE: message,

View File

@@ -2,6 +2,7 @@
import logging
import os
import signal
import shutil
from pathlib import Path
from colorlog import ColoredFormatter
@@ -100,6 +101,7 @@ def initialize_logging():
def check_environment():
"""Check if all environment are exists."""
# check environment variables
for key in ('SUPERVISOR_SHARE', 'SUPERVISOR_NAME',
'HOMEASSISTANT_REPOSITORY'):
try:
@@ -108,10 +110,16 @@ def check_environment():
_LOGGER.fatal("Can't find %s in env!", key)
return False
# check docker socket
if not SOCKET_DOCKER.is_socket():
_LOGGER.fatal("Can't find docker socket!")
return False
# check socat exec
if not shutil.which('socat'):
_LOGGER.fatal("Can0t find socat program!")
return False
return True

View File

@@ -1,181 +1,69 @@
"""Bootstrap HassIO."""
from datetime import datetime
import logging
import json
import os
from pathlib import Path, PurePath
import voluptuous as vol
from voluptuous.humanize import humanize_error
from .const import FILE_HASSIO_CONFIG, HASSIO_DATA
from .tools import (
fetch_last_versions, write_json_file, read_json_file, validate_timezone)
from .validate import HASS_DEVICES
from .const import (
FILE_HASSIO_CONFIG, HASSIO_DATA, ATTR_SECURITY, ATTR_SESSIONS,
ATTR_PASSWORD, ATTR_TOTP, ATTR_TIMEZONE, ATTR_ADDONS_CUSTOM_LIST,
ATTR_AUDIO_INPUT, ATTR_AUDIO_OUTPUT, ATTR_LAST_BOOT)
from .tools import JsonConfig, parse_datetime
from .validate import SCHEMA_HASSIO_CONFIG
_LOGGER = logging.getLogger(__name__)
DATETIME_FORMAT = "%Y%m%d %H:%M:%S"
HOMEASSISTANT_CONFIG = PurePath("homeassistant")
HOMEASSISTANT_LAST = 'homeassistant_last'
HOMEASSISTANT_DEVICES = 'homeassistant_devices'
HASSIO_SSL = PurePath("ssl")
HASSIO_LAST = 'hassio_last'
ADDONS_CORE = PurePath("addons/core")
ADDONS_LOCAL = PurePath("addons/local")
ADDONS_GIT = PurePath("addons/git")
ADDONS_DATA = PurePath("addons/data")
ADDONS_CUSTOM_LIST = 'addons_custom_list'
BACKUP_DATA = PurePath("backup")
SHARE_DATA = PurePath("share")
TMP_DATA = PurePath("tmp")
UPSTREAM_BETA = 'upstream_beta'
API_ENDPOINT = 'api_endpoint'
TIMEZONE = 'timezone'
SECURITY_INITIALIZE = 'security_initialize'
SECURITY_TOTP = 'security_totp'
SECURITY_PASSWORD = 'security_password'
SECURITY_SESSIONS = 'security_sessions'
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
# 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(TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(HOMEASSISTANT_LAST): vol.Coerce(str),
vol.Optional(HOMEASSISTANT_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(HASSIO_LAST): vol.Coerce(str),
vol.Optional(ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(SECURITY_INITIALIZE, default=False): vol.Boolean(),
vol.Optional(SECURITY_TOTP): vol.Coerce(str),
vol.Optional(SECURITY_PASSWORD): vol.Coerce(str),
vol.Optional(SECURITY_SESSIONS, default={}):
{vol.Coerce(str): vol.Coerce(str)},
}, extra=vol.REMOVE_EXTRA)
class CoreConfig(object):
class CoreConfig(JsonConfig):
"""Hold all core config data."""
def __init__(self):
"""Initialize config object."""
super().__init__(FILE_HASSIO_CONFIG, SCHEMA_HASSIO_CONFIG)
self.arch = None
self._file = FILE_HASSIO_CONFIG
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate data
if not self._validate_config():
self._data = SCHEMA_CONFIG({})
def _validate_config(self):
"""Validate config and return True or False."""
# validate data
try:
self._data = SCHEMA_CONFIG(self._data)
except vol.Invalid as ex:
_LOGGER.warning(
"Invalid config %s", humanize_error(self._data, ex))
return False
return True
def save(self):
"""Store data to config file."""
if not self._validate_config():
return False
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
async def fetch_update_infos(self, websession):
"""Read current versions from web."""
last = await fetch_last_versions(websession, beta=self.upstream_beta)
if last:
self._data.update({
HOMEASSISTANT_LAST: last.get('homeassistant'),
HASSIO_LAST: last.get('hassio'),
})
self.save()
return True
return False
@property
def api_endpoint(self):
"""Return IP address of api endpoint."""
return self._data[API_ENDPOINT]
@api_endpoint.setter
def api_endpoint(self, value):
"""Store IP address of api endpoint."""
self._data[API_ENDPOINT] = value
@property
def upstream_beta(self):
"""Return True if we run in beta upstream."""
return self._data[UPSTREAM_BETA]
@upstream_beta.setter
def upstream_beta(self, value):
"""Set beta upstream mode."""
self._data[UPSTREAM_BETA] = bool(value)
self.save()
@property
def timezone(self):
"""Return system timezone."""
return self._data[TIMEZONE]
return self._data[ATTR_TIMEZONE]
@timezone.setter
def timezone(self, value):
"""Set system timezone."""
self._data[TIMEZONE] = value
self._data[ATTR_TIMEZONE] = value
self.save()
@property
def homeassistant_devices(self):
"""Return list of special device to map into homeassistant."""
return self._data[HOMEASSISTANT_DEVICES]
def last_boot(self):
"""Return last boot datetime."""
boot_str = self._data.get(ATTR_LAST_BOOT, DEFAULT_BOOT_TIME)
@homeassistant_devices.setter
def homeassistant_devices(self, value):
"""Set list of special device."""
self._data[HOMEASSISTANT_DEVICES] = value
boot_time = parse_datetime(boot_str)
if not boot_time:
return datetime.utcfromtimestamp(1)
return boot_time
@last_boot.setter
def last_boot(self, value):
"""Set last boot datetime."""
self._data[ATTR_LAST_BOOT] = value.isoformat()
self.save()
@property
def homeassistant_image(self):
"""Return docker homeassistant repository."""
return os.environ['HOMEASSISTANT_REPOSITORY']
@property
def last_homeassistant(self):
"""Actual version of homeassistant."""
return self._data.get(HOMEASSISTANT_LAST)
@property
def last_hassio(self):
"""Actual version of hassio."""
return self._data.get(HASSIO_LAST)
@property
def path_hassio(self):
"""Return hassio data path."""
@@ -264,73 +152,95 @@ class CoreConfig(object):
@property
def addons_repositories(self):
"""Return list of addons custom repositories."""
return self._data[ADDONS_CUSTOM_LIST]
return self._data[ATTR_ADDONS_CUSTOM_LIST]
@addons_repositories.setter
def addons_repositories(self, repo):
def add_addon_repository(self, repo):
"""Add a custom repository to list."""
if repo in self._data[ADDONS_CUSTOM_LIST]:
if repo in self._data[ATTR_ADDONS_CUSTOM_LIST]:
return
self._data[ADDONS_CUSTOM_LIST].append(repo)
self._data[ATTR_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]:
if repo not in self._data[ATTR_ADDONS_CUSTOM_LIST]:
return
self._data[ADDONS_CUSTOM_LIST].remove(repo)
self._data[ATTR_ADDONS_CUSTOM_LIST].remove(repo)
self.save()
@property
def security_initialize(self):
"""Return is security was initialize."""
return self._data[SECURITY_INITIALIZE]
return self._data[ATTR_SECURITY]
@security_initialize.setter
def security_initialize(self, value):
"""Set is security initialize."""
self._data[SECURITY_INITIALIZE] = value
self._data[ATTR_SECURITY] = value
self.save()
@property
def security_totp(self):
"""Return the TOTP key."""
return self._data.get(SECURITY_TOTP)
return self._data.get(ATTR_TOTP)
@security_totp.setter
def security_totp(self, value):
"""Set the TOTP key."""
self._data[SECURITY_TOTP] = value
self._data[ATTR_TOTP] = value
self.save()
@property
def security_password(self):
"""Return the password key."""
return self._data.get(SECURITY_PASSWORD)
return self._data.get(ATTR_PASSWORD)
@security_password.setter
def security_password(self, value):
"""Set the password key."""
self._data[SECURITY_PASSWORD] = value
self._data[ATTR_PASSWORD] = value
self.save()
@property
def security_sessions(self):
"""Return api sessions."""
return {session: datetime.strptime(until, DATETIME_FORMAT) for
session, until in self._data[SECURITY_SESSIONS].items()}
return {
session: parse_datetime(until) for
session, until in self._data[ATTR_SESSIONS].items()
}
@security_sessions.setter
def security_sessions(self, value):
def add_security_session(self, session, valid):
"""Set the a new session."""
session, valid = value
if valid is None:
self._data[SECURITY_SESSIONS].pop(session, None)
else:
self._data[SECURITY_SESSIONS].update(
{session: valid.strftime(DATETIME_FORMAT)}
)
self._data[ATTR_SESSIONS].update(
{session: valid.isoformat()}
)
self.save()
def drop_security_session(self, session):
"""Delete the a session."""
self._data[ATTR_SESSIONS].pop(session, None)
self.save()
@property
def audio_output(self):
"""Return ALSA audio output card,dev."""
return self._data.get(ATTR_AUDIO_OUTPUT)
@audio_output.setter
def audio_output(self, value):
"""Set ALSA audio output card,dev."""
self._data[ATTR_AUDIO_OUTPUT] = value
self.save()
@property
def audio_input(self):
"""Return ALSA audio input card,dev."""
return self._data.get(ATTR_AUDIO_INPUT)
@audio_input.setter
def audio_input(self, value):
"""Set ALSA audio input card,dev."""
self._data[ATTR_AUDIO_INPUT] = value
self.save()

View File

@@ -1,12 +1,11 @@
"""Const file for HassIO."""
from pathlib import Path
from ipaddress import ip_network
HASSIO_VERSION = '0.41'
HASSIO_VERSION = '0.62'
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')
'hassio/{}/version.json')
URL_HASSIO_ADDONS = 'https://github.com/home-assistant/hassio-addons'
@@ -24,10 +23,16 @@ RESTART_EXIT_CODE = 100
FILE_HASSIO_ADDONS = Path(HASSIO_DATA, "addons.json")
FILE_HASSIO_CONFIG = Path(HASSIO_DATA, "config.json")
FILE_HASSIO_HOMEASSISTANT = Path(HASSIO_DATA, "homeassistant.json")
FILE_HASSIO_UPDATER = Path(HASSIO_DATA, "updater.json")
SOCKET_DOCKER = Path("/var/run/docker.sock")
SOCKET_HC = Path("/var/run/hassio-hc.sock")
DOCKER_NETWORK = 'hassio'
DOCKER_NETWORK_MASK = ip_network('172.30.32.0/23')
DOCKER_NETWORK_RANGE = ip_network('172.30.33.0/24')
LABEL_VERSION = 'io.hass.version'
LABEL_ARCH = 'io.hass.arch'
LABEL_TYPE = 'io.hass.type'
@@ -43,6 +48,9 @@ JSON_MESSAGE = 'message'
RESULT_ERROR = 'error'
RESULT_OK = 'ok'
CONTENT_TYPE_BINARY = 'application/octet-stream'
CONTENT_TYPE_PNG = 'image/png'
ATTR_DATE = 'date'
ATTR_ARCH = 'arch'
ATTR_HOSTNAME = 'hostname'
@@ -53,6 +61,7 @@ ATTR_SOURCE = 'source'
ATTR_FEATURES = 'features'
ATTR_ADDONS = 'addons'
ATTR_VERSION = 'version'
ATTR_LAST_BOOT = 'last_boot'
ATTR_LAST_VERSION = 'last_version'
ATTR_BETA_CHANNEL = 'beta_channel'
ATTR_NAME = 'name'
@@ -62,12 +71,14 @@ ATTR_STARTUP = 'startup'
ATTR_BOOT = 'boot'
ATTR_PORTS = 'ports'
ATTR_MAP = 'map'
ATTR_WEBUI = 'webui'
ATTR_OPTIONS = 'options'
ATTR_INSTALLED = 'installed'
ATTR_DETACHED = 'detached'
ATTR_STATE = 'state'
ATTR_SCHEMA = 'schema'
ATTR_IMAGE = 'image'
ATTR_LOGO = 'logo'
ATTR_ADDONS_REPOSITORIES = 'addons_repositories'
ATTR_REPOSITORY = 'repository'
ATTR_REPOSITORIES = 'repositories'
@@ -77,6 +88,7 @@ ATTR_PASSWORD = 'password'
ATTR_TOTP = 'totp'
ATTR_INITIALIZE = 'initialize'
ATTR_SESSION = 'session'
ATTR_SESSIONS = 'sessions'
ATTR_LOCATON = 'location'
ATTR_BUILD = 'build'
ATTR_DEVICES = 'devices'
@@ -89,15 +101,28 @@ ATTR_USER = 'user'
ATTR_SYSTEM = 'system'
ATTR_SNAPSHOTS = 'snapshots'
ATTR_HOMEASSISTANT = 'homeassistant'
ATTR_HASSIO = 'hassio'
ATTR_HASSIO_API = 'hassio_api'
ATTR_FOLDERS = 'folders'
ATTR_SIZE = 'size'
ATTR_TYPE = 'type'
ATTR_TIMEOUT = 'timeout'
ATTR_AUTO_UPDATE = 'auto_update'
ATTR_CUSTOM = 'custom'
ATTR_AUDIO = 'audio'
ATTR_AUDIO_INPUT = 'audio_input'
ATTR_AUDIO_OUTPUT = 'audio_output'
ATTR_INPUT = 'input'
ATTR_OUTPUT = 'output'
ATTR_DISK = 'disk'
ATTR_SERIAL = 'serial'
ATTR_SECURITY = 'security'
ATTR_ADDONS_CUSTOM_LIST = 'addons_custom_list'
STARTUP_INITIALIZE = 'initialize'
STARTUP_BEFORE = 'before'
STARTUP_AFTER = 'after'
STARTUP_SYSTEM = 'system'
STARTUP_SERVICES = 'services'
STARTUP_APPLICATION = 'application'
STARTUP_ONCE = 'once'
BOOT_AUTO = 'auto'

View File

@@ -3,24 +3,27 @@ import asyncio
import logging
import aiohttp
import docker
from .addons import AddonManager
from .api import RestAPI
from .host_control import HostControl
from .const import (
SOCKET_DOCKER, RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_INFO_TASKS, RUN_RELOAD_ADDONS_TASKS,
RUN_UPDATE_SUPERVISOR_TASKS, RUN_WATCHDOG_HOMEASSISTANT,
RUN_CLEANUP_API_SESSIONS, STARTUP_AFTER, STARTUP_BEFORE,
STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS, RUN_UPDATE_ADDONS_TASKS)
RUN_CLEANUP_API_SESSIONS, STARTUP_SYSTEM, STARTUP_SERVICES,
STARTUP_APPLICATION, STARTUP_INITIALIZE, RUN_RELOAD_SNAPSHOTS_TASKS,
RUN_UPDATE_ADDONS_TASKS)
from .hardware import Hardware
from .homeassistant import HomeAssistant
from .scheduler import Scheduler
from .dock.homeassistant import DockerHomeAssistant
from .dock import DockerAPI
from .dock.supervisor import DockerSupervisor
from .dns import DNSForward
from .snapshots import SnapshotsManager
from .updater import Updater
from .tasks import (
hassio_update, homeassistant_watchdog, homeassistant_setup,
api_sessions_cleanup, addons_update)
from .tools import get_local_ip, fetch_timezone
hassio_update, homeassistant_watchdog, api_sessions_cleanup, addons_update)
from .tools import fetch_timezone
_LOGGER = logging.getLogger(__name__)
@@ -34,20 +37,26 @@ class HassIO(object):
self.loop = loop
self.config = config
self.websession = aiohttp.ClientSession(loop=loop)
self.updater = Updater(config, loop, self.websession)
self.scheduler = Scheduler(loop)
self.api = RestAPI(config, loop)
self.dock = docker.DockerClient(
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
self.hardware = Hardware()
self.docker = DockerAPI()
self.dns = DNSForward()
# init basic docker container
self.supervisor = DockerSupervisor(config, loop, self.dock, self.stop)
self.homeassistant = DockerHomeAssistant(config, loop, self.dock)
self.supervisor = DockerSupervisor(
config, loop, self.docker, self.stop)
# init homeassistant
self.homeassistant = HomeAssistant(
config, loop, self.docker, self.updater)
# init HostControl
self.host_control = HostControl(loop)
# init addon system
self.addons = AddonManager(config, loop, self.dock)
self.addons = AddonManager(config, loop, self.docker)
# init snapshot system
self.snapshots = SnapshotsManager(
@@ -57,15 +66,12 @@ class HassIO(object):
"""Setup HassIO orchestration."""
# supervisor
if not await self.supervisor.attach():
_LOGGER.fatal("Can't attach to supervisor docker container!")
_LOGGER.fatal("Can't setup supervisor docker container!")
await self.supervisor.cleanup()
# set running arch
self.config.arch = self.supervisor.arch
# set api endpoint
self.config.api_endpoint = await get_local_ip(self.loop)
# update timezone
if self.config.timezone == 'UTC':
self.config.timezone = await fetch_timezone(self.websession)
@@ -78,11 +84,11 @@ class HassIO(object):
self.host_control.load, RUN_UPDATE_INFO_TASKS)
# rest api views
self.api.register_host(self.host_control)
self.api.register_host(self.host_control, self.hardware)
self.api.register_network(self.host_control)
self.api.register_supervisor(
self.supervisor, self.snapshots, self.addons, self.host_control,
self.websession)
self.updater)
self.api.register_homeassistant(self.homeassistant)
self.api.register_addons(self.addons)
self.api.register_security()
@@ -94,13 +100,8 @@ class HassIO(object):
api_sessions_cleanup(self.config), RUN_CLEANUP_API_SESSIONS,
now=True)
# first start of supervisor?
if not await self.homeassistant.exists():
_LOGGER.info("No HomeAssistant docker found.")
await homeassistant_setup(
self.config, self.loop, self.homeassistant, self.websession)
else:
await self.homeassistant.attach()
# Load homeassistant
await self.homeassistant.prepare()
# Load addons
await self.addons.prepare()
@@ -113,13 +114,16 @@ class HassIO(object):
# schedule self update task
self.scheduler.register_task(
hassio_update(self.config, self.supervisor, self.websession),
hassio_update(self.supervisor, self.updater),
RUN_UPDATE_SUPERVISOR_TASKS)
# schedule snapshot update tasks
self.scheduler.register_task(
self.snapshots.reload, RUN_RELOAD_SNAPSHOTS_TASKS, now=True)
# start dns forwarding
self.loop.create_task(self.dns.start())
# start addon mark as initialize
await self.addons.auto_boot(STARTUP_INITIALIZE)
@@ -128,28 +132,35 @@ class HassIO(object):
# on release channel, try update itself
# on beta channel, only read new versions
await asyncio.wait(
[hassio_update(self.config, self.supervisor, self.websession)()],
[hassio_update(self.supervisor, self.updater)()],
loop=self.loop
)
# start api
await self.api.start()
_LOGGER.info("Start hassio api on %s", self.config.api_endpoint)
_LOGGER.info("Start hassio api on %s", self.docker.network.supervisor)
try:
# HomeAssistant is already running / supervisor have only reboot
if await self.homeassistant.is_running():
if self.hardware.last_boot == self.config.last_boot:
_LOGGER.info("HassIO reboot detected")
return
# start addon mark as before
await self.addons.auto_boot(STARTUP_BEFORE)
# start addon mark as system
await self.addons.auto_boot(STARTUP_SYSTEM)
# start addon mark as services
await self.addons.auto_boot(STARTUP_SERVICES)
# run HomeAssistant
await self.homeassistant.run()
if self.homeassistant.boot:
await self.homeassistant.run()
# start addon mark as after
await self.addons.auto_boot(STARTUP_AFTER)
# start addon mark as application
await self.addons.auto_boot(STARTUP_APPLICATION)
# store new last boot
self.config.last_boot = self.hardware.last_boot
finally:
# schedule homeassistant watchdog
@@ -157,6 +168,10 @@ class HassIO(object):
homeassistant_watchdog(self.loop, self.homeassistant),
RUN_WATCHDOG_HOMEASSISTANT)
# If landingpage / run upgrade in background
if self.homeassistant.version == 'landingpage':
self.loop.create_task(self.homeassistant.install())
async def stop(self, exit_code=0):
"""Stop a running orchestration."""
# don't process scheduler anymore
@@ -164,7 +179,7 @@ class HassIO(object):
# process stop tasks
self.websession.close()
await self.api.stop()
await asyncio.wait([self.api.stop(), self.dns.stop()], loop=self.loop)
self.exit_code = exit_code
self.loop.stop()

40
hassio/dns.py Normal file
View File

@@ -0,0 +1,40 @@
"""Setup the internal DNS service for host applications."""
import asyncio
import logging
import shlex
_LOGGER = logging.getLogger(__name__)
COMMAND = "socat UDP-RECVFROM:53,fork UDP-SENDTO:127.0.0.11:53"
class DNSForward(object):
"""Manage DNS forwarding to internal DNS."""
def __init__(self):
"""Initialize DNS forwarding."""
self.proc = None
async def start(self):
"""Start DNS forwarding."""
try:
self.proc = await asyncio.create_subprocess_exec(
*shlex.split(COMMAND),
stdin=asyncio.subprocess.DEVNULL,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
except OSError as err:
_LOGGER.error("Can't start DNS forwarding -> %s", err)
else:
_LOGGER.info("Start DNS port forwarding for host add-ons")
async def stop(self):
"""Stop DNS forwarding."""
if not self.proc:
_LOGGER.warning("DNS forwarding is not running!")
return
self.proc.kill()
await self.proc.wait()
_LOGGER.info("Stop DNS forwarding")

View File

@@ -1,349 +1,108 @@
"""Init file for HassIO docker object."""
import asyncio
from contextlib import suppress
import logging
import docker
from ..const import LABEL_VERSION, LABEL_ARCH
from .network import DockerNetwork
from ..const import SOCKET_DOCKER
_LOGGER = logging.getLogger(__name__)
class DockerBase(object):
"""Docker hassio wrapper."""
class DockerAPI(object):
"""Docker hassio wrapper.
def __init__(self, config, loop, dock, image=None, timeout=30):
This class is not AsyncIO safe!
"""
def __init__(self):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.dock = dock
self.image = image
self.timeout = timeout
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
self.docker = docker.DockerClient(
base_url="unix:/{}".format(str(SOCKET_DOCKER)), version='auto')
self.network = DockerNetwork(self.docker)
@property
def name(self):
"""Return name of docker container."""
return None
def images(self):
"""Return api images."""
return self.docker.images
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self._lock.locked()
def containers(self):
"""Return api containers."""
return self.docker.containers
def process_metadata(self, metadata, force=False):
"""Read metadata and set it to object."""
# read image
if not self.image:
self.image = metadata['Config']['Image']
@property
def api(self):
"""Return api containers."""
return self.docker.api
# read version
need_version = force or not self.version
if need_version and LABEL_VERSION in metadata['Config']['Labels']:
self.version = metadata['Config']['Labels'][LABEL_VERSION]
elif need_version:
_LOGGER.warning("Can't read version from %s", self.name)
# read arch
need_arch = force or not self.arch
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
async def install(self, tag):
"""Pull docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute install while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._install, tag)
def _install(self, tag):
"""Pull docker image.
def run(self, image, **kwargs):
""""Create a docker and run it.
Need run inside executor.
"""
name = kwargs.get('name', image)
network_mode = kwargs.get('network_mode')
hostname = kwargs.get('hostname')
# setup network
if network_mode:
kwargs['dns'] = [str(self.network.supervisor)]
else:
kwargs['network'] = None
# create container
try:
_LOGGER.info("Pull image %s tag %s.", self.image, tag)
image = self.dock.images.pull("{}:{}".format(self.image, tag))
image.tag(self.image, tag='latest')
self.process_metadata(image.attrs, force=True)
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
container = self.docker.containers.create(image, **kwargs)
except docker.errors.DockerException as err:
_LOGGER.error("Can't create container from %s -> %s", name, err)
return False
_LOGGER.info("Tag image %s with version %s as latest", self.image, tag)
return True
def exists(self):
"""Return True if docker image exists in local repo.
Return a Future.
"""
return self.loop.run_in_executor(None, self._exists)
def _exists(self):
"""Return True if docker image exists in local repo.
Need run inside executor.
"""
try:
self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
return True
def is_running(self):
"""Return True if docker is Running.
Return a Future.
"""
return self.loop.run_in_executor(None, self._is_running)
def _is_running(self):
"""Return True if docker is Running.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
image = self.dock.images.get(self.image)
except docker.errors.DockerException:
return False
# container is not running
if container.status != 'running':
return False
# we run on a old image, stop and start it
if container.image.id != image.id:
return False
return True
async def attach(self):
"""Attach to running docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute attach while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._attach)
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
if self.image:
obj_data = self.dock.images.get(self.image).attrs
# attach network
if not network_mode:
alias = [hostname] if hostname else None
if self.network.attach_container(container, alias=alias):
self.network.detach_default_bridge(container)
else:
obj_data = self.dock.containers.get(self.name).attrs
except docker.errors.DockerException:
return False
_LOGGER.warning("Can't attach %s to hassio-net!", name)
self.process_metadata(obj_data)
_LOGGER.info(
"Attach to image %s with version %s", self.image, self.version)
# run container
try:
container.start()
except docker.errors.DockerException as err:
_LOGGER.error("Can't start %s -> %s", name, err)
return False
return True
async def run(self):
"""Run docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute run while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._run)
def _run(self):
"""Run docker image.
def run_command(self, image, command=None, **kwargs):
"""Create a temporary container and run command.
Need run inside executor.
"""
raise NotImplementedError()
stdout = kwargs.get('stdout', True)
stderr = kwargs.get('stderr', True)
async def stop(self):
"""Stop/remove docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute stop while a task is in progress")
return False
async with self._lock:
await self.loop.run_in_executor(None, self._stop)
return True
def _stop(self):
"""Stop/remove and remove docker container.
Need run inside executor.
"""
_LOGGER.info("Run command '%s' on %s", command, image)
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return
container = self.docker.containers.run(
image,
command=command,
network=self.network.name,
**kwargs
)
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=self.timeout)
# wait until command is done
exit_code = container.wait()
output = container.logs(stdout=stdout, stderr=stderr)
except docker.errors.DockerException as err:
_LOGGER.error("Can't execute command -> %s", err)
return (None, b"")
# cleanup container
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True)
async def remove(self):
"""Remove docker images."""
if self._lock.locked():
_LOGGER.error("Can't excute remove while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker images.
Need run inside executor.
"""
# cleanup container
self._stop()
_LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version)
try:
with suppress(docker.errors.ImageNotFound):
self.dock.images.remove(
image="{}:latest".format(self.image), force=True)
with suppress(docker.errors.ImageNotFound):
self.dock.images.remove(
image="{}:{}".format(self.image, self.version), force=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False
# clean metadata
self.version = None
self.arch = None
return True
async def update(self, tag):
"""Update a docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._update, tag)
def _update(self, tag):
"""Update a docker image.
Need run inside executor.
"""
was_running = self._is_running()
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
# update docker image
if not self._install(tag):
return False
# cleanup old stuff
if was_running:
self._run()
self._cleanup()
return True
async def logs(self):
"""Return docker logs of container."""
if self._lock.locked():
_LOGGER.error("Can't excute logs while a task is in progress")
return b""
async with self._lock:
return await self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return b""
try:
return container.logs(tail=100, stdout=True, stderr=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s -> %s", self.image, err)
async def restart(self):
"""Restart docker container."""
if self._lock.locked():
_LOGGER.error("Can't excute restart while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
Need run inside executor.
"""
try:
container = self.dock.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
return False
return True
async def cleanup(self):
"""Check if old version exists and cleanup."""
if self._lock.locked():
_LOGGER.error("Can't excute cleanup while a task is in progress")
return False
async with self._lock:
await self.loop.run_in_executor(None, self._cleanup)
def _cleanup(self):
"""Check if old version exists and cleanup.
Need run inside executor.
"""
try:
latest = self.dock.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image)
return
for image in self.dock.images.list(name=self.image):
if latest.id == image.id:
continue
with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags)
self.dock.images.remove(image.id, force=True)
return (exit_code, output)

View File

@@ -6,21 +6,23 @@ import shutil
import docker
import requests
from . import DockerBase
from .util import dockerfile_template
from .interface import DockerInterface
from .util import dockerfile_template, docker_process
from ..const import (
META_ADDON, MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE)
_LOGGER = logging.getLogger(__name__)
AUDIO_DEVICE = "/dev/snd:/dev/snd:rwm"
class DockerAddon(DockerBase):
class DockerAddon(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, addon):
def __init__(self, config, loop, api, addon):
"""Initialize docker homeassistant wrapper."""
super().__init__(
config, loop, dock, image=addon.image, timeout=addon.timeout)
config, loop, api, image=addon.image, timeout=addon.timeout)
self.addon = addon
@property
@@ -28,16 +30,52 @@ class DockerAddon(DockerBase):
"""Return name of docker container."""
return "addon_{}".format(self.addon.slug)
@property
def hostname(self):
"""Return slug/id of addon."""
return self.addon.slug.replace('_', '-')
@property
def environment(self):
"""Return environment for docker add-on."""
addon_env = self.addon.environment or {}
if self.addon.with_audio:
addon_env.update({
'ALSA_OUTPUT': self.addon.audio_output,
'ALSA_INPUT': self.addon.audio_input,
})
return {
**addon_env,
'TZ': self.config.timezone,
}
@property
def devices(self):
"""Return needed devices."""
devices = self.addon.devices or []
# use audio devices
if self.addon.with_audio and AUDIO_DEVICE not in devices:
devices.append(AUDIO_DEVICE)
# Return None if no devices is present
if devices:
return devices
return None
@property
def ports(self):
"""Filter None from addon ports."""
if not self.addon.ports:
return None
return {
container_port: host_port
for container_port, host_port in self.addon.ports.items()
if host_port
}
@property
def tmpfs(self):
"""Return tmpfs for docker add-on."""
@@ -46,6 +84,20 @@ class DockerAddon(DockerBase):
return {"/tmpfs": "{}".format(options)}
return None
@property
def network_mapping(self):
"""Return hosts mapping."""
return {
'homeassistant': self.docker.network.gateway,
}
@property
def network_mode(self):
"""Return network mode for addon."""
if self.addon.host_network:
return 'host'
return None
@property
def volumes(self):
"""Generate volumes for mappings."""
@@ -103,27 +155,26 @@ class DockerAddon(DockerBase):
if not self.addon.write_options():
return False
try:
self.dock.containers.run(
self.image,
name=self.name,
detach=True,
network_mode=self.addon.network_mode,
ports=self.addon.ports,
devices=self.addon.devices,
cap_add=self.addon.privileged,
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
)
ret = self.docker.run(
self.image,
name=self.name,
hostname=self.hostname,
detach=True,
network_mode=self.network_mode,
ports=self.ports,
extra_hosts=self.network_mapping,
devices=self.devices,
cap_add=self.addon.privileged,
environment=self.environment,
volumes=self.volumes,
tmpfs=self.tmpfs
)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
return False
if ret:
_LOGGER.info("Start docker addon %s with version %s",
self.image, self.version)
_LOGGER.info(
"Start docker addon %s with version %s", self.image, self.version)
return True
return ret
def _install(self, tag):
"""Pull docker image or build it.
@@ -144,7 +195,7 @@ class DockerAddon(DockerBase):
try:
# prepare temporary addon build folder
try:
source = self.addon.path_addon_location
source = self.addon.path_location
shutil.copytree(str(source), str(build_dir))
except shutil.Error as err:
_LOGGER.error("Can't copy %s to temporary build folder -> %s",
@@ -164,8 +215,10 @@ class DockerAddon(DockerBase):
build_tag = "{}:{}".format(self.image, tag)
_LOGGER.info("Start build %s on %s", build_tag, build_dir)
image = self.dock.images.build(
path=str(build_dir), tag=build_tag, pull=True)
image = self.docker.images.build(
path=str(build_dir), tag=build_tag, pull=True,
forcerm=True
)
image.tag(self.image, tag='latest')
self.process_metadata(image.attrs, force=True)
@@ -180,15 +233,10 @@ class DockerAddon(DockerBase):
finally:
shutil.rmtree(str(build_dir), ignore_errors=True)
async def export_image(self, path):
@docker_process
def export_image(self, path):
"""Export current images into a tar file."""
if self._lock.locked():
_LOGGER.error("Can't excute export while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(
None, self._export_image, path)
return self.loop.run_in_executor(None, self._export_image, path)
def _export_image(self, tar_file):
"""Export current images into a tar file.
@@ -196,7 +244,7 @@ class DockerAddon(DockerBase):
Need run inside executor.
"""
try:
image = self.dock.api.get_image(self.image)
image = self.docker.api.get_image(self.image)
except docker.errors.DockerException as err:
_LOGGER.error("Can't fetch image %s -> %s", self.image, err)
return False
@@ -212,15 +260,10 @@ class DockerAddon(DockerBase):
_LOGGER.info("Export image %s to %s", self.image, tar_file)
return True
async def import_image(self, path, tag):
@docker_process
def import_image(self, path, tag):
"""Import a tar file as image."""
if self._lock.locked():
_LOGGER.error("Can't excute import while a task is in progress")
return False
async with self._lock:
return await self.loop.run_in_executor(
None, self._import_image, path, tag)
return self.loop.run_in_executor(None, self._import_image, path, tag)
def _import_image(self, tar_file, tag):
"""Import a tar file as image.
@@ -229,9 +272,9 @@ class DockerAddon(DockerBase):
"""
try:
with tar_file.open("rb") as read_tar:
self.dock.api.load_image(read_tar)
self.docker.api.load_image(read_tar)
image = self.dock.images.get(self.image)
image = self.docker.images.get(self.image)
image.tag(self.image, tag=tag)
except (docker.errors.DockerException, OSError) as err:
_LOGGER.error("Can't import image %s -> %s", self.image, err)

View File

@@ -3,19 +3,20 @@ import logging
import docker
from . import DockerBase
from .interface import DockerInterface
_LOGGER = logging.getLogger(__name__)
HASS_DOCKER_NAME = 'homeassistant'
class DockerHomeAssistant(DockerBase):
class DockerHomeAssistant(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock):
def __init__(self, config, loop, api, data):
"""Initialize docker homeassistant wrapper."""
super().__init__(config, loop, dock, image=config.homeassistant_image)
super().__init__(config, loop, api, image=data.image)
self.data = data
@property
def name(self):
@@ -25,11 +26,11 @@ class DockerHomeAssistant(DockerBase):
@property
def devices(self):
"""Create list of special device to map into docker."""
if not self.config.homeassistant_devices:
if not self.data.devices:
return
devices = []
for device in self.config.homeassistant_devices:
for device in self.data.devices:
devices.append("/dev/{0}:/dev/{0}:rwm".format(device))
return devices
@@ -45,31 +46,68 @@ class DockerHomeAssistant(DockerBase):
# cleanup
self._stop()
try:
self.dock.containers.run(
self.image,
name=self.name,
detach=True,
privileged=True,
devices=self.devices,
network_mode='host',
environment={
'HASSIO': self.config.api_endpoint,
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'rw'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
})
ret = self.docker.run(
self.image,
name=self.name,
hostname=self.name,
detach=True,
privileged=True,
devices=self.devices,
network_mode='host',
environment={
'HASSIO': self.docker.network.supervisor,
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'rw'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
str(self.config.path_extern_share):
{'bind': '/share', 'mode': 'rw'},
}
)
except docker.errors.DockerException as err:
_LOGGER.error("Can't run %s -> %s", self.image, err)
if ret:
_LOGGER.info("Start homeassistant %s with version %s",
self.image, self.version)
return ret
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
return self.docker.run_command(
self.image,
command,
detach=True,
stdout=True,
stderr=True,
environment={
'TZ': self.config.timezone,
},
volumes={
str(self.config.path_extern_config):
{'bind': '/config', 'mode': 'ro'},
str(self.config.path_extern_ssl):
{'bind': '/ssl', 'mode': 'ro'},
}
)
def is_initialize(self):
"""Return True if docker container exists."""
return self.loop.run_in_executor(None, self._is_initialize)
def _is_initialize(self):
"""Return True if docker container exists.
Need run inside executor.
"""
try:
self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info(
"Start homeassistant %s with version %s", self.image, self.version)
return True

327
hassio/dock/interface.py Normal file
View File

@@ -0,0 +1,327 @@
"""Interface class for HassIO docker object."""
import asyncio
from contextlib import suppress
import logging
import docker
from .util import docker_process
from ..const import LABEL_VERSION, LABEL_ARCH
_LOGGER = logging.getLogger(__name__)
class DockerInterface(object):
"""Docker hassio interface."""
def __init__(self, config, loop, api, image=None, timeout=30):
"""Initialize docker base wrapper."""
self.config = config
self.loop = loop
self.docker = api
self.image = image
self.timeout = timeout
self.version = None
self.arch = None
self._lock = asyncio.Lock(loop=loop)
@property
def name(self):
"""Return name of docker container."""
return None
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self._lock.locked()
def process_metadata(self, metadata, force=False):
"""Read metadata and set it to object."""
# read image
if not self.image:
self.image = metadata['Config']['Image']
# read version
need_version = force or not self.version
if need_version and LABEL_VERSION in metadata['Config']['Labels']:
self.version = metadata['Config']['Labels'][LABEL_VERSION]
elif need_version:
_LOGGER.warning("Can't read version from %s", self.name)
# read arch
need_arch = force or not self.arch
if need_arch and LABEL_ARCH in metadata['Config']['Labels']:
self.arch = metadata['Config']['Labels'][LABEL_ARCH]
@docker_process
def install(self, tag):
"""Pull docker image."""
return self.loop.run_in_executor(None, self._install, tag)
def _install(self, tag):
"""Pull docker image.
Need run inside executor.
"""
try:
_LOGGER.info("Pull image %s tag %s.", self.image, tag)
image = self.docker.images.pull("{}:{}".format(self.image, tag))
image.tag(self.image, tag='latest')
self.process_metadata(image.attrs, force=True)
except docker.errors.APIError as err:
_LOGGER.error("Can't install %s:%s -> %s.", self.image, tag, err)
return False
_LOGGER.info("Tag image %s with version %s as latest", self.image, tag)
return True
def exists(self):
"""Return True if docker image exists in local repo."""
return self.loop.run_in_executor(None, self._exists)
def _exists(self):
"""Return True if docker image exists in local repo.
Need run inside executor.
"""
try:
self.docker.images.get(self.image)
except docker.errors.DockerException:
return False
return True
def is_running(self):
"""Return True if docker is Running.
Return a Future.
"""
return self.loop.run_in_executor(None, self._is_running)
def _is_running(self):
"""Return True if docker is Running.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
image = self.docker.images.get(self.image)
except docker.errors.DockerException:
return False
# container is not running
if container.status != 'running':
return False
# we run on a old image, stop and start it
if container.image.id != image.id:
return False
return True
@docker_process
def attach(self):
"""Attach to running docker container."""
return self.loop.run_in_executor(None, self._attach)
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
if self.image:
obj_data = self.docker.images.get(self.image).attrs
else:
obj_data = self.docker.containers.get(self.name).attrs
except docker.errors.DockerException:
return False
self.process_metadata(obj_data)
_LOGGER.info(
"Attach to image %s with version %s", self.image, self.version)
return True
@docker_process
def run(self):
"""Run docker image."""
return self.loop.run_in_executor(None, self._run)
def _run(self):
"""Run docker image.
Need run inside executor.
"""
raise NotImplementedError()
@docker_process
def stop(self):
"""Stop/remove docker container."""
return self.loop.run_in_executor(None, self._stop)
def _stop(self):
"""Stop/remove and remove docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
if container.status == 'running':
_LOGGER.info("Stop %s docker application", self.image)
with suppress(docker.errors.DockerException):
container.stop(timeout=self.timeout)
with suppress(docker.errors.DockerException):
_LOGGER.info("Clean %s docker application", self.image)
container.remove(force=True)
return True
@docker_process
def remove(self):
"""Remove docker images."""
return self.loop.run_in_executor(None, self._remove)
def _remove(self):
"""remove docker images.
Need run inside executor.
"""
# cleanup container
self._stop()
_LOGGER.info(
"Remove docker %s with latest and %s", self.image, self.version)
try:
with suppress(docker.errors.ImageNotFound):
self.docker.images.remove(
image="{}:latest".format(self.image), force=True)
with suppress(docker.errors.ImageNotFound):
self.docker.images.remove(
image="{}:{}".format(self.image, self.version), force=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't remove image %s -> %s", self.image, err)
return False
# clean metadata
self.version = None
self.arch = None
return True
@docker_process
def update(self, tag):
"""Update a docker image."""
return self.loop.run_in_executor(None, self._update, tag)
def _update(self, tag):
"""Update a docker image.
Need run inside executor.
"""
_LOGGER.info(
"Update docker %s with %s:%s", self.version, self.image, tag)
# update docker image
if not self._install(tag):
return False
# stop container & cleanup
self._stop()
self._cleanup()
return True
def logs(self):
"""Return docker logs of container.
Return a Future.
"""
return self.loop.run_in_executor(None, self._logs)
def _logs(self):
"""Return docker logs of container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return b""
try:
return container.logs(tail=100, stdout=True, stderr=True)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't grap logs from %s -> %s", self.image, err)
@docker_process
def restart(self):
"""Restart docker container."""
return self.loop.run_in_executor(None, self._restart)
def _restart(self):
"""Restart docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
_LOGGER.info("Restart %s", self.image)
try:
container.restart(timeout=self.timeout)
except docker.errors.DockerException as err:
_LOGGER.warning("Can't restart %s -> %s", self.image, err)
return False
return True
@docker_process
def cleanup(self):
"""Check if old version exists and cleanup."""
return self.loop.run_in_executor(None, self._cleanup)
def _cleanup(self):
"""Check if old version exists and cleanup.
Need run inside executor.
"""
try:
latest = self.docker.images.get(self.image)
except docker.errors.DockerException:
_LOGGER.warning("Can't find %s for cleanup", self.image)
return False
for image in self.docker.images.list(name=self.image):
if latest.id == image.id:
continue
with suppress(docker.errors.DockerException):
_LOGGER.info("Cleanup docker images: %s", image.tags)
self.docker.images.remove(image.id, force=True)
return True
@docker_process
def execute_command(self, command):
"""Create a temporary container and run command."""
return self.loop.run_in_executor(None, self._execute_command, command)
def _execute_command(self, command):
"""Create a temporary container and run command.
Need run inside executor.
"""
raise NotImplementedError()

89
hassio/dock/network.py Normal file
View File

@@ -0,0 +1,89 @@
"""Internal network manager for HassIO."""
import logging
import docker
from ..const import DOCKER_NETWORK_MASK, DOCKER_NETWORK, DOCKER_NETWORK_RANGE
_LOGGER = logging.getLogger(__name__)
class DockerNetwork(object):
"""Internal HassIO Network."""
def __init__(self, dock):
"""Initialize internal hassio network."""
self.docker = dock
self.network = self._get_network()
@property
def name(self):
"""Return name of network."""
return DOCKER_NETWORK
@property
def containers(self):
"""Return of connected containers from network."""
return self.network.containers
@property
def gateway(self):
"""Return gateway of the network."""
return DOCKER_NETWORK_MASK[1]
@property
def supervisor(self):
"""Return supervisor of the network."""
return DOCKER_NETWORK_MASK[2]
def _get_network(self):
"""Get HassIO network."""
try:
return self.docker.networks.get(DOCKER_NETWORK)
except docker.errors.NotFound:
_LOGGER.info("Can't find HassIO network, create new network")
ipam_pool = docker.types.IPAMPool(
subnet=str(DOCKER_NETWORK_MASK),
gateway=str(self.gateway),
iprange=str(DOCKER_NETWORK_RANGE)
)
ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool])
return self.docker.networks.create(
DOCKER_NETWORK, driver='bridge', ipam=ipam_config, options={
"com.docker.network.bridge.name": DOCKER_NETWORK,
})
def attach_container(self, container, alias=None, ipv4=None):
"""Attach container to hassio network.
Need run inside executor.
"""
ipv4 = str(ipv4) if ipv4 else None
try:
self.network.connect(container, aliases=alias, ipv4_address=ipv4)
except docker.errors.APIError as err:
_LOGGER.error("Can't link container to hassio-net -> %s", err)
return False
self.network.reload()
return True
def detach_default_bridge(self, container):
"""Detach default docker bridge.
Need run inside executor.
"""
try:
default_network = self.docker.networks.get('bridge')
default_network.disconnect(container)
except docker.errors.NotFound:
return
except docker.errors.APIError as err:
_LOGGER.warning(
"Can't disconnect container from default -> %s", err)

View File

@@ -2,18 +2,21 @@
import logging
import os
from . import DockerBase
import docker
from .interface import DockerInterface
from .util import docker_process
from ..const import RESTART_EXIT_CODE
_LOGGER = logging.getLogger(__name__)
class DockerSupervisor(DockerBase):
class DockerSupervisor(DockerInterface):
"""Docker hassio wrapper for HomeAssistant."""
def __init__(self, config, loop, dock, stop_callback, image=None):
def __init__(self, config, loop, api, stop_callback, image=None):
"""Initialize docker base wrapper."""
super().__init__(config, loop, dock, image=image)
super().__init__(config, loop, api, image=image)
self.stop_callback = stop_callback
@property
@@ -21,20 +24,38 @@ class DockerSupervisor(DockerBase):
"""Return name of docker container."""
return os.environ['SUPERVISOR_NAME']
def _attach(self):
"""Attach to running docker container.
Need run inside executor.
"""
try:
container = self.docker.containers.get(self.name)
except docker.errors.DockerException:
return False
self.process_metadata(container.attrs)
_LOGGER.info("Attach to supervisor %s with version %s",
self.image, self.version)
# if already attach
if container in self.docker.network.containers:
return True
# attach to network
return self.docker.network.attach_container(
container, alias=['hassio'], ipv4=self.docker.network.supervisor)
@docker_process
async def update(self, tag):
"""Update a supervisor docker image."""
if self._lock.locked():
_LOGGER.error("Can't excute update while a task is in progress")
return False
_LOGGER.info("Update supervisor docker to %s:%s", self.image, tag)
async with self._lock:
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
return True
if await self.loop.run_in_executor(None, self._install, tag):
self.loop.create_task(self.stop_callback(RESTART_EXIT_CODE))
return True
return False
return False
async def run(self):
"""Run docker image."""

View File

@@ -1,10 +1,12 @@
"""HassIO docker utilitys."""
import logging
import re
from ..const import ARCH_AARCH64, ARCH_ARMHF, ARCH_I386, ARCH_AMD64
_LOGGER = logging.getLogger(__name__)
RESIN_BASE_IMAGE = {
HASSIO_BASE_IMAGE = {
ARCH_ARMHF: "homeassistant/armhf-base:latest",
ARCH_AARCH64: "homeassistant/aarch64-base:latest",
ARCH_I386: "homeassistant/i386-base:latest",
@@ -17,12 +19,14 @@ TMPL_IMAGE = re.compile(r"%%BASE_IMAGE%%")
def dockerfile_template(dockerfile, arch, version, meta_type):
"""Prepare a Hass.IO dockerfile."""
buff = []
resin_image = RESIN_BASE_IMAGE[arch]
hassio_image = HASSIO_BASE_IMAGE[arch]
custom_image = re.compile(r"^#{}:FROM".format(arch))
# read docker
with dockerfile.open('r') as dock_input:
for line in dock_input:
line = TMPL_IMAGE.sub(resin_image, line)
line = TMPL_IMAGE.sub(hassio_image, line)
line = custom_image.sub("FROM", line)
buff.append(line)
# add metadata
@@ -38,3 +42,19 @@ def create_metadata(version, arch, meta_type):
return ('LABEL io.hass.version="{}" '
'io.hass.arch="{}" '
'io.hass.type="{}"').format(version, arch, meta_type)
# pylint: disable=protected-access
def docker_process(method):
"""Wrap function with only run once."""
async def wrap_api(api, *args, **kwargs):
"""Return api wrapper."""
if api._lock.locked():
_LOGGER.error(
"Can't excute %s while a task is in progress", method.__name__)
return False
async with api._lock:
return await method(api, *args, **kwargs)
return wrap_api

109
hassio/hardware.py Normal file
View File

@@ -0,0 +1,109 @@
"""Read hardware info from system."""
from datetime import datetime
import logging
from pathlib import Path
import re
import pyudev
from .const import ATTR_NAME, ATTR_TYPE, ATTR_DEVICES
_LOGGER = logging.getLogger(__name__)
ASOUND_CARDS = Path("/proc/asound/cards")
RE_CARDS = re.compile(r"(\d+) \[(\w*) *\]: (.*\w)")
ASOUND_DEVICES = Path("/proc/asound/devices")
RE_DEVICES = re.compile(r"\[.*(\d+)- (\d+).*\]: ([\w ]*)")
PROC_STAT = Path("/proc/stat")
RE_BOOT_TIME = re.compile(r"btime (\d+)")
class Hardware(object):
"""Represent a interface to procfs, sysfs and udev."""
def __init__(self):
"""Init hardware object."""
self.context = pyudev.Context()
@property
def serial_devices(self):
"""Return all serial and connected devices."""
dev_list = set()
for device in self.context.list_devices(subsystem='tty'):
if 'ID_VENDOR' in device:
dev_list.add(device.device_node)
return list(dev_list)
@property
def input_devices(self):
"""Return all input devices."""
dev_list = set()
for device in self.context.list_devices(subsystem='input'):
if 'NAME' in device:
dev_list.add(device['NAME'].replace('"', ''))
return list(dev_list)
@property
def disk_devices(self):
"""Return all disk devices."""
dev_list = set()
for device in self.context.list_devices(subsystem='block'):
if device.device_node.startswith('/dev/sd'):
dev_list.add(device.device_node)
return list(dev_list)
@property
def audio_devices(self):
"""Return all available audio interfaces."""
try:
with ASOUND_CARDS.open('r') as cards_file:
cards = cards_file.read()
with ASOUND_DEVICES.open('r') as devices_file:
devices = devices_file.read()
except OSError as err:
_LOGGER.error("Can't read asound data -> %s", err)
return
audio_list = {}
# parse cards
for match in RE_CARDS.finditer(cards):
audio_list[match.group(1)] = {
ATTR_NAME: match.group(3),
ATTR_TYPE: match.group(2),
ATTR_DEVICES: {},
}
# parse devices
for match in RE_DEVICES.finditer(devices):
try:
audio_list[match.group(1)][ATTR_DEVICES][match.group(2)] = \
match.group(3)
except KeyError:
_LOGGER.warning("Wrong audio device found %s", match.group(0))
continue
return audio_list
@property
def last_boot(self):
"""Return last boot time."""
try:
with PROC_STAT.open("r") as stat_file:
stats = stat_file.read()
except OSError as err:
_LOGGER.error("Can't read stat data -> %s", err)
return
# parse stat file
found = RE_BOOT_TIME.search(stats)
if not found:
_LOGGER.error("Can't found last boot time!")
return
return datetime.utcfromtimestamp(int(found.group(1)))

206
hassio/homeassistant.py Normal file
View File

@@ -0,0 +1,206 @@
"""HomeAssistant control object."""
import asyncio
import logging
import os
import re
from .const import (
FILE_HASSIO_HOMEASSISTANT, ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION,
ATTR_VERSION, ATTR_BOOT)
from .dock.homeassistant import DockerHomeAssistant
from .tools import JsonConfig, convert_to_ascii
from .validate import SCHEMA_HASS_CONFIG
_LOGGER = logging.getLogger(__name__)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
class HomeAssistant(JsonConfig):
"""Hass core object for handle it."""
def __init__(self, config, loop, docker, updater):
"""Initialize hass object."""
super().__init__(FILE_HASSIO_HOMEASSISTANT, SCHEMA_HASS_CONFIG)
self.config = config
self.loop = loop
self.updater = updater
self.docker = DockerHomeAssistant(config, loop, docker, self)
async def prepare(self):
"""Prepare HomeAssistant object."""
if not await self.docker.exists():
_LOGGER.info("No HomeAssistant docker %s found.", self.image)
if self.is_custom_image:
await self.install()
else:
await self.install_landingpage()
else:
await self.docker.attach()
@property
def version(self):
"""Return version of running homeassistant."""
return self.docker.version
@property
def last_version(self):
"""Return last available version of homeassistant."""
if self.is_custom_image:
return self._data.get(ATTR_LAST_VERSION)
return self.updater.version_homeassistant
@property
def image(self):
"""Return image name of hass containter."""
if ATTR_IMAGE in self._data:
return self._data[ATTR_IMAGE]
return os.environ['HOMEASSISTANT_REPOSITORY']
@property
def is_custom_image(self):
"""Return True if a custom image is used."""
return ATTR_IMAGE in self._data
@property
def devices(self):
"""Return extend device mapping."""
return self._data[ATTR_DEVICES]
@devices.setter
def devices(self, value):
"""Set extend device mapping."""
self._data[ATTR_DEVICES] = value
self.save()
@property
def boot(self):
"""Return True if home-assistant boot is enabled."""
return self._data[ATTR_BOOT]
@boot.setter
def boot(self, value):
"""Set home-assistant boot options."""
self._data[ATTR_BOOT] = value
self.save()
def set_custom(self, image, version):
"""Set a custom image for homeassistant."""
# reset
if image is None and version is None:
self._data.pop(ATTR_IMAGE, None)
self._data.pop(ATTR_VERSION, None)
self.docker.image = self.image
else:
if image:
self._data[ATTR_IMAGE] = image
self.docker.image = image
if version:
self._data[ATTR_VERSION] = version
self.save()
async def install_landingpage(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant landingpage")
while True:
if await self.docker.install('landingpage'):
break
_LOGGER.warning("Fails install landingpage, retry after 60sec")
await asyncio.sleep(60, loop=self.loop)
async def install(self):
"""Install a landingpage."""
_LOGGER.info("Setup HomeAssistant")
while True:
# read homeassistant tag and install it
if not self.last_version:
await self.updater.fetch_data()
tag = self.last_version
if tag and await self.docker.install(tag):
break
_LOGGER.warning("Error on install HomeAssistant. Retry in 60sec")
await asyncio.sleep(60, loop=self.loop)
# store version
_LOGGER.info("HomeAssistant docker now installed")
await self.docker.cleanup()
async def update(self, version=None):
"""Update HomeAssistant version."""
version = version or self.last_version
running = await self.docker.is_running()
if version == self.docker.version:
_LOGGER.warning("Version %s is already installed", version)
return False
try:
return await self.docker.update(version)
finally:
if running:
await self.docker.run()
def run(self):
"""Run HomeAssistant docker.
Return a coroutine.
"""
return self.docker.run()
def stop(self):
"""Stop HomeAssistant docker.
Return a coroutine.
"""
return self.docker.stop()
def restart(self):
"""Restart HomeAssistant docker.
Return a coroutine.
"""
return self.docker.restart()
def logs(self):
"""Get HomeAssistant docker logs.
Return a coroutine.
"""
return self.docker.logs()
def is_running(self):
"""Return True if docker container is running.
Return a coroutine.
"""
return self.docker.is_running()
def is_initialize(self):
"""Return True if a docker container is exists.
Return a coroutine.
"""
return self.docker.is_initialize()
@property
def in_progress(self):
"""Return True if a task is in progress."""
return self.docker.in_progress
async def check_config(self):
"""Run homeassistant config check."""
exit_code, log = await self.docker.execute_command(
"python3 -m homeassistant -c /config --script check_config"
)
# if not valid
if exit_code is None:
return (False, "")
# parse output
log = convert_to_ascii(log)
if exit_code != 0 or RE_YAML_ERROR.search(log):
return (False, log)
return (True, log)

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -37,7 +37,7 @@ class SnapshotsManager(object):
def _create_snapshot(self, name, sys_type):
"""Initialize a new snapshot object from name."""
date_str = str(datetime.utcnow())
date_str = datetime.utcnow().isoformat()
slug = create_slug(name, date_str)
tar_file = Path(self.config.path_backup, "{}.tar".format(slug))
@@ -46,8 +46,7 @@ class SnapshotsManager(object):
snapshot.create(slug, name, date_str, sys_type)
# set general data
snapshot.homeassistant_version = self.homeassistant.version
snapshot.homeassistant_devices = self.config.homeassistant_devices
snapshot.snapshot_homeassistant(self.homeassistant)
snapshot.repositories = self.config.addons_repositories
return snapshot
@@ -198,8 +197,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders()
# start homeassistant restore
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
snapshot.restore_homeassistant(self.homeassistant)
task_hass = self.loop.create_task(
self.homeassistant.update(snapshot.homeassistant_version))
@@ -281,8 +279,7 @@ class SnapshotsManager(object):
await snapshot.restore_folders(folders)
if homeassistant:
self.config.homeassistant_devices = \
snapshot.homeassistant_devices
snapshot.restore_homeassistant(self.homeassistant)
tasks.append(self.homeassistant.update(
snapshot.homeassistant_version))

View File

@@ -13,7 +13,8 @@ from .validate import SCHEMA_SNAPSHOT, ALL_FOLDERS
from .util import remove_folder
from ..const import (
ATTR_SLUG, ATTR_NAME, ATTR_DATE, ATTR_ADDONS, ATTR_REPOSITORIES,
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES)
ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_VERSION, ATTR_TYPE, ATTR_DEVICES,
ATTR_IMAGE)
from ..tools import write_json_file
_LOGGER = logging.getLogger(__name__)
@@ -90,6 +91,16 @@ class Snapshot(object):
"""Set snapshot homeassistant devices."""
self._data[ATTR_HOMEASSISTANT][ATTR_DEVICES] = value
@property
def homeassistant_image(self):
"""Return snapshot homeassistant custom image."""
return self._data[ATTR_HOMEASSISTANT].get(ATTR_IMAGE)
@homeassistant_image.setter
def homeassistant_image(self, value):
"""Set snapshot homeassistant custom image."""
self._data[ATTR_HOMEASSISTANT][ATTR_IMAGE] = value
@property
def size(self):
"""Return snapshot size."""
@@ -111,6 +122,24 @@ class Snapshot(object):
self._data[ATTR_REPOSITORIES] = []
self._data[ATTR_FOLDERS] = []
def snapshot_homeassistant(self, homeassistant):
"""Read all data from homeassistant object."""
self.homeassistant_version = homeassistant.version
self.homeassistant_devices = homeassistant.devices
# custom image
if homeassistant.is_custom_image:
self.homeassistant_image = homeassistant.image
def restore_homeassistant(self, homeassistant):
"""Write all data to homeassistant object."""
homeassistant.devices = self.homeassistant_devices
# custom image
if self.homeassistant_image:
homeassistant.set_custom(
self.homeassistant_image, self.homeassistant_version)
async def load(self):
"""Read snapshot.json from tar file."""
if not self.tar_file.is_file():

View File

@@ -5,7 +5,7 @@ import voluptuous as vol
from ..const import (
ATTR_REPOSITORIES, ATTR_ADDONS, ATTR_NAME, ATTR_SLUG, ATTR_DATE,
ATTR_VERSION, ATTR_HOMEASSISTANT, ATTR_FOLDERS, ATTR_TYPE, ATTR_DEVICES,
FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
ATTR_IMAGE, FOLDER_SHARE, FOLDER_HOMEASSISTANT, FOLDER_ADDONS, FOLDER_SSL,
SNAPSHOT_FULL, SNAPSHOT_PARTIAL)
from ..validate import HASS_DEVICES
@@ -20,6 +20,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
vol.Required(ATTR_HOMEASSISTANT): vol.Schema({
vol.Required(ATTR_VERSION): vol.Coerce(str),
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
}),
vol.Optional(ATTR_FOLDERS, default=[]): [vol.In(ALL_FOLDERS)],
vol.Optional(ATTR_ADDONS, default=[]): [vol.Schema({

View File

@@ -13,7 +13,7 @@ def api_sessions_cleanup(config):
now = datetime.now()
for session, until_valid in config.security_sessions.items():
if now >= until_valid:
config.security_sessions = (session, None)
config.drop_security_session(session)
return _api_sessions_cleanup
@@ -27,8 +27,14 @@ def addons_update(loop, addons):
if not addon.is_installed or not addon.auto_update:
continue
if addon.version_installed != addon.version:
if addon.version_installed == addon.last_version:
continue
if addon.test_udpate_schema():
tasks.append(addon.update())
else:
_LOGGER.warning(
"Addon %s will be ignore, schema tests fails", addon.slug)
if tasks:
_LOGGER.info("Addon auto update process %d tasks", len(tasks))
@@ -37,21 +43,21 @@ def addons_update(loop, addons):
return _addons_update
def hassio_update(config, supervisor, websession):
def hassio_update(supervisor, updater):
"""Create scheduler task for update of supervisor hassio."""
async def _hassio_update():
"""Check and run update of supervisor hassio."""
await config.fetch_update_infos(websession)
if config.last_hassio == supervisor.version:
await updater.fetch_data()
if updater.version_hassio == supervisor.version:
return
# don't perform a update on beta/dev channel
if config.upstream_beta:
if updater.beta_channel:
_LOGGER.warning("Ignore Hass.IO update on beta upstream!")
return
_LOGGER.info("Found new HassIO version %s.", config.last_hassio)
await supervisor.update(config.last_hassio)
_LOGGER.info("Found new HassIO version %s.", updater.version_hassio)
await supervisor.update(updater.version_hassio)
return _hassio_update
@@ -60,26 +66,14 @@ def homeassistant_watchdog(loop, homeassistant):
"""Create scheduler task for montoring running state."""
async def _homeassistant_watchdog():
"""Check running state and start if they is close."""
# if Home-Assistant is active
if not await homeassistant.is_initialize():
return
# If Home-Assistant is running
if homeassistant.in_progress or await homeassistant.is_running():
return
loop.create_task(homeassistant.run())
return _homeassistant_watchdog
async def homeassistant_setup(config, loop, homeassistant, websession):
"""Install a homeassistant docker container."""
while True:
# read homeassistant tag and install it
if not config.last_homeassistant:
await config.fetch_update_infos(websession)
tag = config.last_homeassistant
if tag and await homeassistant.install(tag):
break
_LOGGER.warning("Error on setup HomeAssistant. Retry in 60.")
await asyncio.sleep(60, loop=loop)
# store version
_LOGGER.info("HomeAssistant docker now installed.")

View File

@@ -1,60 +1,32 @@
"""Tools file for HassIO."""
import asyncio
from contextlib import suppress
from datetime import datetime, timedelta, timezone
import json
import logging
import socket
import re
import aiohttp
import async_timeout
import pytz
import voluptuous as vol
from .const import URL_HASSIO_VERSION, URL_HASSIO_VERSION_BETA
from voluptuous.humanize import humanize_error
_LOGGER = logging.getLogger(__name__)
FREEGEOIP_URL = "https://freegeoip.io/json/"
RE_STRING = re.compile(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))")
async def fetch_last_versions(websession, beta=False):
"""Fetch current versions from github.
Is a coroutine.
"""
url = URL_HASSIO_VERSION_BETA if beta else URL_HASSIO_VERSION
try:
with async_timeout.timeout(10, loop=websession.loop):
async with websession.get(url) as request:
return await request.json(content_type=None)
except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err:
_LOGGER.warning("Can't fetch versions from %s! %s", url, err)
except json.JSONDecodeError as err:
_LOGGER.warning("Can't parse versions from %s! %s", url, err)
def get_local_ip(loop):
"""Retrieve local IP address.
Return a future.
"""
def local_ip():
"""Return local ip."""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Use Google Public DNS server to determine own IP
sock.connect(('8.8.8.8', 80))
return sock.getsockname()[0]
except socket.error:
return socket.gethostbyname(socket.gethostname())
finally:
sock.close()
return loop.run_in_executor(None, local_ip)
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
DATETIME_RE = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
)
def write_json_file(jsonfile, data):
@@ -75,19 +47,6 @@ def read_json_file(jsonfile):
return json.loads(cfile.read())
def validate_timezone(timezone):
"""Validate voluptuous timezone."""
try:
pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise vol.Invalid(
"Invalid time zone passed in. Valid options can be found here: "
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \
from None
return timezone
async def fetch_timezone(websession):
"""Read timezone from freegeoip."""
data = {}
@@ -98,3 +57,109 @@ async def fetch_timezone(websession):
data = await request.json()
return data.get('time_zone', 'UTC')
def convert_to_ascii(raw):
"""Convert binary to ascii and remove colors."""
return RE_STRING.sub("", raw.decode())
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
def parse_datetime(dt_str):
"""Parse a string and return a datetime.datetime.
This function supports time zone offsets. When the input contains one,
the output uses a timezone with a fixed offset from UTC.
Raises ValueError if the input is well formatted but not a valid datetime.
Returns None if the input isn't well formatted.
"""
match = DATETIME_RE.match(dt_str)
if not match:
return None
kws = match.groupdict() # type: Dict[str, Any]
if kws['microsecond']:
kws['microsecond'] = kws['microsecond'].ljust(6, '0')
tzinfo_str = kws.pop('tzinfo')
tzinfo = None # type: Optional[dt.tzinfo]
if tzinfo_str == 'Z':
tzinfo = pytz.utc
elif tzinfo_str is not None:
offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0
offset_hours = int(tzinfo_str[1:3])
offset = timedelta(hours=offset_hours, minutes=offset_mins)
if tzinfo_str[0] == '-':
offset = -offset
tzinfo = timezone(offset)
else:
tzinfo = None
kws = {k: int(v) for k, v in kws.items() if v is not None}
kws['tzinfo'] = tzinfo
return datetime(**kws)
class JsonConfig(object):
"""Hass core object for handle it."""
def __init__(self, json_file, schema):
"""Initialize hass object."""
self._file = json_file
self._schema = schema
self._data = {}
# init or load data
if self._file.is_file():
try:
self._data = read_json_file(self._file)
except (OSError, json.JSONDecodeError):
_LOGGER.warning("Can't read %s", self._file)
self._data = {}
# validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse %s -> %s",
self._file, humanize_error(self._data, ex))
def save(self):
"""Store data to config file."""
# validate
try:
self._data = self._schema(self._data)
except vol.Invalid as ex:
_LOGGER.error("Can't parse data -> %s",
humanize_error(self._data, ex))
return False
# write
if not write_json_file(self._file, self._data):
_LOGGER.error("Can't store config in %s", self._file)
return False
return True
class AsyncThrottle(object):
"""
Decorator that prevents a function from being called more than once every
time period.
"""
def __init__(self, delta):
"""Initialize async throttle."""
self.throttle_period = delta
self.time_of_last_call = datetime.min
def __call__(self, method):
"""Throttle function"""
async def wrapper(*args, **kwargs):
"""Throttle function wrapper"""
now = datetime.now()
time_since_last_call = now - self.time_of_last_call
if time_since_last_call > self.throttle_period:
self.time_of_last_call = now
return await method(*args, **kwargs)
return wrapper

86
hassio/updater.py Normal file
View File

@@ -0,0 +1,86 @@
"""Fetch last versions from webserver."""
import asyncio
from datetime import timedelta
import json
import logging
import aiohttp
import async_timeout
from .const import (
URL_HASSIO_VERSION, FILE_HASSIO_UPDATER, ATTR_HOMEASSISTANT, ATTR_HASSIO,
ATTR_BETA_CHANNEL)
from .tools import AsyncThrottle, JsonConfig
from .validate import SCHEMA_UPDATER_CONFIG
_LOGGER = logging.getLogger(__name__)
class Updater(JsonConfig):
"""Fetch last versions from version.json."""
def __init__(self, config, loop, websession):
"""Initialize updater."""
super().__init__(FILE_HASSIO_UPDATER, SCHEMA_UPDATER_CONFIG)
self.config = config
self.loop = loop
self.websession = websession
@property
def version_homeassistant(self):
"""Return last version of homeassistant."""
return self._data.get(ATTR_HOMEASSISTANT)
@property
def version_hassio(self):
"""Return last version of hassio."""
return self._data.get(ATTR_HASSIO)
@property
def upstream(self):
"""Return Upstream branch for version."""
if self.beta_channel:
return 'dev'
return 'master'
@property
def beta_channel(self):
"""Return True if we run in beta upstream."""
return self._data[ATTR_BETA_CHANNEL]
@beta_channel.setter
def beta_channel(self, value):
"""Set beta upstream mode."""
self._data[ATTR_BETA_CHANNEL] = bool(value)
self.save()
@AsyncThrottle(timedelta(seconds=60))
async def fetch_data(self):
"""Fetch current versions from github.
Is a coroutine.
"""
url = URL_HASSIO_VERSION.format(self.upstream)
try:
_LOGGER.info("Fetch update data from %s", url)
with async_timeout.timeout(10, loop=self.loop):
async with self.websession.get(url) as request:
data = await request.json(content_type=None)
except (aiohttp.ClientError, asyncio.TimeoutError, KeyError) as err:
_LOGGER.warning("Can't fetch versions from %s -> %s", url, err)
return
except json.JSONDecodeError as err:
_LOGGER.warning("Can't parse versions from %s -> %s", url, err)
return
# data valid?
if not data:
_LOGGER.warning("Invalid data from %s", url)
return
# update versions
self._data[ATTR_HOMEASSISTANT] = data.get('homeassistant')
self._data[ATTR_HASSIO] = data.get('hassio')
self.save()

View File

@@ -1,8 +1,31 @@
"""Validate functions."""
import voluptuous as vol
import pytz
from .const import (
ATTR_DEVICES, ATTR_IMAGE, ATTR_LAST_VERSION, ATTR_SESSIONS, ATTR_PASSWORD,
ATTR_TOTP, ATTR_SECURITY, ATTR_BETA_CHANNEL, ATTR_TIMEZONE,
ATTR_ADDONS_CUSTOM_LIST, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
ATTR_HOMEASSISTANT, ATTR_HASSIO, ATTR_BOOT, ATTR_LAST_BOOT)
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
HASS_DEVICES = [vol.Match(r"^[^/]*$")]
ALSA_CHANNEL = vol.Match(r"\d+,\d+")
def validate_timezone(timezone):
"""Validate voluptuous timezone."""
try:
pytz.timezone(timezone)
except pytz.exceptions.UnknownTimeZoneError:
raise vol.Invalid(
"Invalid time zone passed in. Valid options can be found here: "
"http://en.wikipedia.org/wiki/List_of_tz_database_time_zones") \
from None
return timezone
def convert_to_docker_ports(data):
@@ -30,3 +53,35 @@ DOCKER_PORTS = vol.Schema({
vol.All(vol.Coerce(str), vol.Match(r"^\d+(?:/tcp|/udp)?$")):
convert_to_docker_ports,
})
# pylint: disable=no-value-for-parameter
SCHEMA_HASS_CONFIG = vol.Schema({
vol.Optional(ATTR_DEVICES, default=[]): HASS_DEVICES,
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): vol.Coerce(str),
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
})
# pylint: disable=no-value-for-parameter
SCHEMA_UPDATER_CONFIG = vol.Schema({
vol.Optional(ATTR_BETA_CHANNEL, default=False): vol.Boolean(),
vol.Optional(ATTR_HOMEASSISTANT): vol.Coerce(str),
vol.Optional(ATTR_HASSIO): vol.Coerce(str),
})
# pylint: disable=no-value-for-parameter
SCHEMA_HASSIO_CONFIG = vol.Schema({
vol.Optional(ATTR_TIMEZONE, default='UTC'): validate_timezone,
vol.Optional(ATTR_LAST_BOOT): vol.Coerce(str),
vol.Optional(ATTR_ADDONS_CUSTOM_LIST, default=[]): [vol.Url()],
vol.Optional(ATTR_SECURITY, default=False): vol.Boolean(),
vol.Optional(ATTR_TOTP): vol.Coerce(str),
vol.Optional(ATTR_PASSWORD): vol.Coerce(str),
vol.Optional(ATTR_SESSIONS, default={}):
vol.Schema({vol.Coerce(str): vol.Coerce(str)}),
vol.Optional(ATTR_AUDIO_OUTPUT): ALSA_CHANNEL,
vol.Optional(ATTR_AUDIO_INPUT): ALSA_CHANNEL,
}, extra=vol.REMOVE_EXTRA)

View File

@@ -46,6 +46,8 @@ setup(
'gitpython',
'pyotp',
'pyqrcode',
'pytz'
'pytz',
'pyudev',
'deepmerge'
]
)

View File

@@ -1,7 +1,8 @@
{
"hassio": "0.41",
"homeassistant": "0.48.1",
"resinos": "0.8",
"resinhup": "0.1",
"generic": "0.3"
"hassio": "0.62",
"homeassistant": "0.53",
"resinos": "1.0",
"resinhup": "0.3",
"generic": "0.3",
"cluster": "0.1"
}